Ash Framework 튜토리얼 따라하기 (6)

계속해서 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 환경에서의 개발에 도움이 많이 될 것 같다.

댓글 남기기