1 minute read

Through

schema "artists" do
  field(:name)
  field(:birth_date, :date)
  field(:death_date, :date)
  timestamps()

  has_many(:albums, Album, on_replace: :nilify)
end

schema "albums" do
  field(:title, :string)
  timestamps()

  belongs_to(:artist, Artist)
  has_many(:tracks, Track)
end

schema "tracks" do
  field(:title, :string)
  field(:duration, :integer)
  field(:duration_string, :string, virtual: true)
  field(:index, :integer)
  field(:number_of_plays, :integer)
  timestamps()

  belongs_to(:album, Album)
end

DB 예제 단골이시다. 아티스트와 앨범은 일대다(one-to-many) 관계이고 앨범과 트랙의 관계도 일대다 관계다. 아티스트의 트랙 전부를 쿼리하려면 어떻게 하면 될까?

iex> import Ecto.Query
iex> (from(a in MusicDB.Artist, where: a.name == "Bobby Hutcherson")
...> |> MusicDB.Repo.all()
...> |> MusicDB.Repo.preload([:albums])
...> |> Enum.flat_map(& &1.albums)
...> |> MusicDB.Repo.preload([:tracks])
...> |> Enum.flat_map(& &1.tracks))

[
  %MusicDB.Track{
    __meta__: #Ecto.Schema.Metadata<:loaded, "tracks">,
    album: #Ecto.Association.NotLoaded<association :album is not loaded>,
    album_id: 5,
    duration: 761,
    duration_string: nil,
    id: 30,
    index: 1,
    inserted_at: ~N[2020-07-20 11:04:28],
    number_of_plays: 0,
    title: "Anton's Ball",
    updated_at: ~N[2020-07-20 11:04:28]
  },
  # ...
  %MusicDB.Track{
    __meta__: #Ecto.Schema.Metadata<:loaded, "tracks">,
    album: #Ecto.Association.NotLoaded<association :album is not loaded>,
    album_id: 5,
    duration: 844,
    duration_string: nil,
    id: 33,
    index: 4,
    inserted_at: ~N[2020-07-20 11:04:29],
    number_of_plays: 0,
    title: "Song Of Songs",
    updated_at: ~N[2020-07-20 11:04:29]
  }
]

아티스트와 관계(relationship)된 앨범을 읽어오고 다시 앨범과 관계된 트랙을 읽어오면 된다. Ecto.Repo.preload/2 함수는 관계 데이터를 읽어오는 함수다. 아티스트를 쿼리해서 읽어온다고 하더라도 preload 함수를 호출해 읽어오기 전까진 관계된 앨범 데이터를 읽어오지 않는다.

schema "artists" do
  field(:name)
  field(:birth_date, :date)
  field(:death_date, :date)
  timestamps()

  has_many(:albums, Album, on_replace: :nilify)
  has_many(:tracks, through: [:albums, :tracks]) #-- 1
end

1번 코드처럼 has_many/3, has_one/3 함수 through 옵션을 사용하면 관계를 타고 들어가야 하는 데이터를 쉽게 쿼리할 수 있다. 아티스트의 tracks 관계를 가져오면 :albums, :tracks 관계 데이터를 차례로 가져온다.

iex> import Ecto.Query
iex> (from(a in MusicDB.Artist, where: a.name == "Bobby Hutcherson")
...> |> MusicDB.Repo.all()
...>|> MusicDB.Repo.preload([:tracks])
...>|> Enum.flat_map(& &1.tracks))

MusicDB.Repo.preload([:tracks]) 한 번으로 가져올 수 있다. programming ecto 책에서 배웠다.