계속해서 Ash Framework 에서 리소스간 관계를 설정하고 다루는 방법에 대해 알아 본다.
새 리소스를 만든다. Representative 로 담당자 정보에 대한 리소스이다. 티켓은 한 담당자에 속해 있을 수 있고 한 담당자는 여러 개의 티켓을 가질 수 있다. 1 (담당자) : 다 (티켓) 구조 이다. 먼저 담당자 리소스를 추가로 선언한다.
# lib/helpdesk/support/representative.ex
defmodule Helpdesk.Support.Representative do
use Ash.Resource,
domain: Helpdesk.Support,
data_layer: Ash.DataLayer.Ets
actions do
defaults [:read]
create :create do
accept [:name]
end
end
attributes do
uuid_primary_key :id
attribute :name, :string do
public? true
end
end
relationships do
has_many :tickets, Helpdesk.Support.Ticket
end
end
다른 선언은 Ticket 예제와 크게 다르지 않다. relationships 선언이 추가되었다. has_many 를 선언함으로 이 리소스는 여러개의 Ticket 을 가질 수 있음을 선언한다. 일대다 관계에서는 두 리소스를 연결할 기준 필드가 필요한데 Ash 에서는 source_attribute, destination_attribute 라 부른다. 기본적으로 source_attribute는 :id로 가정되며, destination_attribute는 <모듈명의_마지막_부분을_스네이크케이스로_변환>_id 형태로 기본값이 설정된다. 예제 코드에서는 source_attribute -> :id, destination_attribute -> :representative_id 인 것이다.
Ticket 에는 Ticket이 Representative 에 속한다는 것을 선언 한다. Ticket 에도 relationships 를 추가 한다.
# lib/helpdesk/support/ticket.ex
relationships do
belongs_to :representative, Helpdesk.Support.Representative
end
belongs_to 도 has_many 와 마찬가지로 source_attribute, destination_attribute 가 있다. 기본 값 규칙은 다음과 같다.
belongs_to :owner, MyApp.User do
# 기본값 :<relationship_name>_id (i.e. :owner_id)
source_attribute :custom_attribute_name # 직접 설정할 수 도 있다
# 기본값 :id
destination_attribute :custom_attribute_name # 직접 설정할 수 도 있다
end
예제 코드는 관계명을 :representative 로 하였으므로 source_attribute 는 :representative_id destination_attribute 는 :id 가 된다.
마지막으로 추가된 Representative 리소스를 Support 도메인에 추가 한다.
# lib/helpdesk/support.ex
resources do
...
resource Helpdesk.Support.Representative
end
Representative 가 Support 도메인의 리소스로 등록되고 Representative 와 Ticket 은 일대다 관계가 설정되었다. 관계가 제대로 설정되었는지 어떻게 다루는지 코드로 확인해 보자.
Representative 와 Ticket 이 관계를 맺는 것은 티켓에 담당자를 할당하는 액션과 같다. Ticket 에 :assign 액션을 추가 하자.
# lib/helpdesk/support/ticket.ex
actions do
...
update :assign do
accept [:representative_id]
end
end
이제 코드를 통해 관계를 설정하고 조회하면서 어떻게 동작하는지 확인해 보자.
iex> ticket = (
Helpdesk.Support.Ticket
|> Ash.Changeset.for_create(:open, %{subject: "I can't find my hand!"})
|> Ash.create!()
)
%Helpdesk.Support.Ticket{
id: "5f8e4bf2-851b-4d42-af02-77e90848afa7",
subject: "I can't find my hand!",
status: :open,
representative_id: nil,
representative: #Ash.NotLoaded<:relationship, field: :representative>,
__meta__: #Ecto.Schema.Metadata<:loaded>
}
Ticket 리소스를 만들었다. representative_id 필드가 있고 nil 인 것을 확인할 수 있다.
iex> ticket2 = (
Helpdesk.Support.Ticket
|> Ash.Changeset.for_create(:open, %{subject: "Please fix it."})
|> Ash.create!()
)
%Helpdesk.Support.Ticket{
id: "442d09a4-f13d-47bf-9bce-1ab5db148696",
subject: "Please fix it.",
status: :open,
representative_id: nil,
representative: #Ash.NotLoaded<:relationship, field: :representative>,
__meta__: #Ecto.Schema.Metadata<:loaded>
}
또 다른 Ticket 리소스. 한 담당자에게 두 개의 티켓을 할당 할 것이다.
iex> representative = (
Helpdesk.Support.Representative
|> Ash.Changeset.for_create(:create, %{name: "Joe Armstrong"})
|> Ash.create!()
)
%Helpdesk.Support.Representative{
id: "16209c46-5df7-4aa7-9ec7-2b599d786f96",
name: "Joe Armstrong",
tickets: #Ash.NotLoaded<:relationship, field: :tickets>,
__meta__: #Ecto.Schema.Metadata<:loaded>
}
Representative 리소스. tickets 필드가 있고 지연로드 때문에 데이터가 보이지 않는다. 지금 시점에선 로드한다 하더라도 티켓을 할당하지 않았기 때문에 빈 리스트 일 것이다.
iex> ticket
|> Ash.Changeset.for_update(:assign, %{representative_id: representative.id})
|> Ash.update!()
%Helpdesk.Support.Ticket{
id: "5f8e4bf2-851b-4d42-af02-77e90848afa7",
subject: "I can't find my hand!",
status: :open,
representative_id: "16209c46-5df7-4aa7-9ec7-2b599d786f96",
representative: #Ash.NotLoaded<:relationship, field: :representative>,
__meta__: #Ecto.Schema.Metadata<:loaded>
}
iex> ticket2
|> Ash.Changeset.for_update(:assign, %{representative_id: representative.id})
|> Ash.update!()
%Helpdesk.Support.Ticket{
id: "442d09a4-f13d-47bf-9bce-1ab5db148696",
subject: "Please fix it.",
status: :open,
representative_id: "16209c46-5df7-4aa7-9ec7-2b599d786f96",
representative: #Ash.NotLoaded<:relationship, field: :representative>,
__meta__: #Ecto.Schema.Metadata<:loaded>
}
두 티켓 리소스에 같은 담당자를 할당 했다. 둘 다 같은 representative_id 가 할당 된 것을 확인할 수 있다.
iex> Helpdesk.Support.Representative |> Ash.Query.load(:tickets) |> Ash.read_one!()
%Helpdesk.Support.Representative{
id: "16209c46-5df7-4aa7-9ec7-2b599d786f96",
name: "Joe Armstrong",
tickets: [
%Helpdesk.Support.Ticket{
id: "442d09a4-f13d-47bf-9bce-1ab5db148696",
subject: "Please fix it.",
status: :open,
representative_id: "16209c46-5df7-4aa7-9ec7-2b599d786f96",
representative: #Ash.NotLoaded<:relationship, field: :representative>,
__meta__: #Ecto.Schema.Metadata<:loaded>
},
%Helpdesk.Support.Ticket{
id: "5f8e4bf2-851b-4d42-af02-77e90848afa7",
subject: "I can't find my hand!",
status: :open,
representative_id: "16209c46-5df7-4aa7-9ec7-2b599d786f96",
representative: #Ash.NotLoaded<:relationship, field: :representative>,
__meta__: #Ecto.Schema.Metadata<:loaded>
}
],
__meta__: #Ecto.Schema.Metadata<:loaded>
}
Representative 를 그냥 로드하면 relationships 필드는 즉시 로딩되지 않는다. 명시적으로 해당 필드를 로드(Ash.Query.load(:tickets))해야 데이터를 가져오게 된다. 담당자 데이터에 할당 된 티켓 목록을 확인할 수 있다.
공식 튜토리얼의 예제는 여기까지지만 지금까지의 내용은 정말 기초에 불과하고 살펴봐야 할 내용과 기능이 많은 것 같다. 앞으로 계속 살펴보고 배워두면 Elixir/Phoenix 환경에서의 개발에 도움이 많이 될 것 같다.