Do you want to pick up from where you left of?
Take me there

Querying

이 단원에서는 Friends 애플리케이션과 이전 단원에서 세팅한 영화-카탈로그 도메인을 이어서 만들어보겠습니다.

Ecto.Repo로 DB 레코드 조회

이전 단원에서 Ecto의 “레포지토리”는 Postgres 데이터베이스 같은 데이터 저장소에 매핑된다고 했었습니다. 데이터베이스와의 모든 상호작용은 이 레포지토리를 통해 이뤄집니다.

Friends.Repo의 함수들을 사용하여 간단한 쿼리들을 직접 실행해볼 수 있습니다.

ID로 레코드 조회

데이터베이스에서 레코드를 ID로 조회하기 위해 Repo.get/3 함수를 사용할 수 있습니다. 이 함수는 2개의 인자로 “쿼리가능한(queryable)” 자료구조와 조회할 레코드의 ID를 받습니다. 레코드를 찾았다면 해당 구조체를 반환하고, 아니면 nil을 반환합니다.

예제를 한번 봅시다. 아래와 같이 ID가 1인 영화를 조회합니다.

iex> alias Friends.{Repo, Movie}
iex> Repo.get(Movie, 1)
%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
  actors: %Ecto.Association.NotLoaded<association :actors is not loaded>,
  characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
  distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

Repo.get/3에 전달하는 첫 번째 인자는 Movie 모듈입니다. Movie는 “쿼리가능”합니다. 모듈이 Ecto.Schema를 사용하고 자료구조를 스키마로 정의하고 있기 때문입니다. 그러면 MovieEcto.Queryable 프로토콜에 접근할 수 있는데, 이 프로토콜은 자료 구조를 Ecto.Query로 전환합니다. Ecto 쿼리는 레포지토리에서 데이터를 조회할 때 쓰입니다. 쿼리에 관해서는 추후 좀 더 알아보겠습니다.

속성으로 레코드 조회

주어진 조건을 만족하는 레코드를 조회하는 것도 Repo.get_by/3 함수로 가능합니다. 이 함수는 2개의 인자로 “쿼리가능한” 자료구조와 쿼리할 절을 요구합니다. Repo.get_by/3은 레포지토리에서 받은 결과 하나를 반환합니다. 예제를 보겠습니다.

iex> Repo.get_by(Movie, title: "Ready Player One")
%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
  actors: %Ecto.Association.NotLoaded<association :actors is not loaded>,
  characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
  distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

더 복잡한 쿼리를 작성하거나 특정 조건을 만족하는 모든 레코드를 조회하고 싶다면 Ecto.Query 모듈을 사용해야 합니다.

Ecto.Query로 쿼리 작성하기

Ecto.Query 모듈에서 제공하는 Query DSL로 애플리케이션 레포지토리에서 데이터를 조회하는 쿼리를 작성할 수 있습니다.

Ecto.Query.from/2와 키워드 기반 쿼리

Ecto.Query.from/2 매크로로 쿼리를 만들 수 있습니다. 이 함수는 인자로 표현식 하나와 옵셔널 인자로 키워드 리스트를 받습니다. 레포지토리에서 모든 영화를 조회하는 가장 단순한 쿼리를 만들어 봅시다.

iex> import Ecto.Query
iex> query = from(Movie)
#Ecto.Query<from m0 in Friends.Movie>

쿼리를 실행하기 위해 Repo.all/2 함수를 사용합니다. 이 함수는 필수 인자인 Ecto 쿼리를 가지고 쿼리 조건을 충족하는 모든 레코드를 반환합니다.

iex> Repo.all(query)

14:58:03.187 [debug] QUERY OK source="movies" db=1.7ms decode=4.2ms
[
  %Friends.Movie{
    __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
    actors: %Ecto.Association.NotLoaded<association :actors is not loaded>,
    characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
    distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
    id: 1,
    tagline: "Something about video games",
    title: "Ready Player One"
  }
]

from과 바인딩없는 쿼리

위 예제는 SQL문의 가장 흥미로운 부분이 빠졌습니다. 보통은 특정 필드만 쿼리하거나 특정 조건으로 필터링하여 쿼리하는 경우가 많습니다. 모든 영화 중에 Ready Player One 제목을 가진 영화만 조회하되 titletagline필드만 불러옵시다.

iex> query = from(Movie, where: [title: "Ready Player One"], select: [:title, :tagline])
#Ecto.Query<from m0 in Friends.Movie, where: m0.title == "Ready Player One",
 select: [:title, :tagline]>

iex> Repo.all(query)
SELECT m0."title", m0."tagline" FROM "movies" AS m0 WHERE (m0."title" = 'Ready Player One') []
[
  %Friends.Movie{
    __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
    actors: %Ecto.Association.NotLoaded<association :actors is not loaded>,
    characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
    id: nil,
    tagline: "Something about video games",
    title: "Ready Player One"
  }
]

반환된 구조체는 taglinetitle 필드만 값이 들어있음에 유의하세요. 이는 select: 부분의 결과입니다.

이런 쿼리는 바인딩이 필요 없을 정도로 간단하므로 바인딩없는 쿼리라 불립니다.

쿼리의 바인딩

지금까지 Ecto.Queryable 프로토콜을 구현한 모듈 하나(예: Movie)를 from 매크로의 첫 번째 인자로 사용했습니다. 하지만 다음처럼 in 표현식도 사용가능합니다.

iex> query = from(m in Movie)
#Ecto.Query<from m0 in Friends.Movie>

이 경우 m바인딩 이라고 표현합니다. 바인딩을 사용하면 쿼리의 다른 부분에서 모듈들을 참조할 수 있어 매우 유용합니다. 영화의 제목 필드만 선택하고 id2보다 작은 영화들만 조회해봅시다.

iex> query = from(m in Movie, where: m.id < 2, select: m.title)
#Ecto.Query<from m0 in Friends.Movie, where: m0.id < 2, select: m0.title>

iex> Repo.all(query)
SELECT m0."title" FROM "movies" AS m0 WHERE (m0."id" < 2) []
["Ready Player One"]

여기서 쿼리의 출력이 변경된 것에 주목하세요. select: 부분에서 _표현식_을 바인딩과 사용함으로써 선택된 필드들이 반환될 형태를 정확하게 명시할 수 있습니다. 예를 들면 다음처럼 튜플로 반환시킬 수 있습니다.

iex> query = from(m in Movie, where: m.id < 2, select: {m.title})

iex> Repo.all(query)
[{"Ready Player One"}]

처음엔 바인딩 없는 간단한 쿼리로 시작하고 데이터 구조를 참조해야 할 때 바인딩을 쓰는 것이 좋습니다. 쿼리의 바인딩에 관한 더 많은 정보는 Ecto 공식 문서를 참고하세요.

매크로기반 쿼리

위 예제에서 쿼리를 작성하기 위해 from매크로 안에서 select:where:같은 키워드를 사용하는걸 키워드기반 쿼리_라고 합니다. 쿼리를 조합하는 다른 방법은 매크로 기반 쿼리입니다. Ecto는 select/3이나 where/3같은 각 키워드에 해당하는 매크로들을 제공합니다. 각 매크로는 _쿼리가능한 값과 명시된 바인딩 리스트 그리고 키워드문에 썼던 동일한 표현식을 인자로 받습니다.

iex> query = select(Movie, [m], m.title)
#Ecto.Query<from m0 in Friends.Movie, select: m0.title>

iex> Repo.all(query)
SELECT m0."title" FROM "movies" AS m0 []
["Ready Player One"]

매크로의 좋은 점은 파이프로 깔끔하게 연결된다는 점입니다.

iex> Movie \
...>  |> where([m], m.id < 2) \
...>  |> select([m], {m.title}) \
...>  |> Repo.all
[{"Ready Player One"}]

위 iex에서 다음줄에 이어서 작성하기 위해 \ 문자를 썼음에 유의하세요.

보간(Interpolated)값과 where 사용

where절 안에서 보간값이나 Elixir 표현식을 사용하기 위해 핀 연산자 ^이 필요합니다. 이것은 핀 꽂듯이 값을 변수에 고정해서 변수가 재 바인딩 되지 않고 해당 값을 가리키게 합니다.

iex> title = "Ready Player One"
"Ready Player One"
iex> query = from(m in Movie, where: m.title == ^title, select: m.tagline)
%Ecto.Query<from m in Friends.Movie, where: m.title == ^"Ready Player One",
 select: m.tagline>
iex> Repo.all(query)

15:21:46.809 [debug] QUERY OK source="movies" db=3.8ms
["Something about video games"]

첫 레코드나 마지막 레코드 조회

Ecto.Query.first/2Ecto.Query.last/2 함수로 레포지토리에서 첫 레코드나 마지막 레코드만 불러올 수 있습니다.

우선 first/2 함수를 이용해 쿼리 표현식을 작성합니다.

iex> first(Movie)
#Ecto.Query<from m0 in Friends.Movie, order_by: [asc: m0.id], limit: 1>

그런 다음 결과를 얻기 위해 Repo.one/2 함수에 쿼리를 전달합니다.

iex> Movie |> first() |> Repo.one()

SELECT m0."id", m0."title", m0."tagline" FROM "movies" AS m0 ORDER BY m0."id" LIMIT 1 []
%Friends.Movie{
  __meta__: #Ecto.Schema.Metadata<:loaded, "movies">,
  actors: #Ecto.Association.NotLoaded<association :actors is not loaded>,
  characters: #Ecto.Association.NotLoaded<association :characters is not loaded>,
  distributor: #Ecto.Association.NotLoaded<association :distributor is not loaded>,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

Ecto.Query.last/2 함수도 똑같이 사용합니다.

iex> Movie |> last() |> Repo.one()

연관 데이터 쿼리

프리로드

belongs_to, has_many, has_one 매크로로 정의된 연관 레코드에 접근 가능하게 하기 위해 연관된 스키마를 프리로드 해야 합니다.

한 영화에 연관된 배우들을 요청하려고 할 때 어떤일이 발생하는지 보겠습니다.

iex> movie = Repo.get(Movie, 1)
iex> movie.actors
%Ecto.Association.NotLoaded<association :actors is not loaded>

프리로드 하지 않고서는 연관된 배우들에 접근할 수 없습니다. Ecto로 레코드를 프리로드하는 몇 가지 방법이 있습니다.

두 쿼리로 프리로드

다음 쿼리는 연관 레코드를 별개의 쿼리로 프리로드 합니다.

iex> Repo.all(from m in Movie, preload: [:actors])

13:17:28.354 [debug] QUERY OK source="movies" db=2.3ms queue=0.1ms
13:17:28.357 [debug] QUERY OK source="actors" db=2.4ms
[
  %Friends.Movie{
    __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
    actors: [
      %Friends.Actor{
        __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
        id: 1,
        movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
        name: "Tyler Sheridan"
      },
      %Friends.Actor{
        __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
        id: 2,
        movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
        name: "Gary"
      }
    ],
    characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
    distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
    id: 1,
    tagline: "Something about video games",
    title: "Ready Player One"
  }
]

위 코드라인에서 두 데이터베이스 쿼리를 실행했음을 알 수 있습니다. 하나는 모든 영화를, 다른 하나는 해당 영화들의 ID로 연관된 모든 배우들을 조회했습니다.

한 쿼리로 프리로드하기

다음처럼 하면 데이터베이스 쿼리를 줄일 수 있습니다.

iex> query = from(m in Movie, join: a in assoc(m, :actors), preload: [actors: a])
iex> Repo.all(query)

13:18:52.053 [debug] QUERY OK source="movies" db=3.7ms
[
  %Friends.Movie{
    __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
    actors: [
      %Friends.Actor{
        __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
        id: 1,
        movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
        name: "Tyler Sheridan"
      },
      %Friends.Actor{
        __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
        id: 2,
        movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
        name: "Gary"
      }
    ],
    characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
    distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
    id: 1,
    tagline: "Something about video games",
    title: "Ready Player One"
  }
]

이렇게 하면 데이터베이스 호출이 한 번만 됩니다. 또한 영화와 연관된 배우들 둘 다에 대해 한 쿼리에서 필드 선택과 필터링이 가능합니다. 예를 들어 join 문을 써서 모든 영화 중에 연관된 배우들이 특정 조건만 만족하는 영화들만 조회할 수 있습니다.

Repo.all from m in Movie,
  join: a in assoc(m, :actors),
  where: a.name == "John Wayne",
  preload: [actors: a]

join문에 대해서는 조금 뒤에 더 살펴보겠습니다.

조회된 레코드 프리로드

데이터베이스에서 이미 쿼리되어 나온 레코드의 연관 스키마도 프리로드 할 수 있습니다.

iex> movie = Repo.get(Movie, 1)
%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
  actors: %Ecto.Association.NotLoaded<association :actors is not loaded>, # actors are NOT LOADED!!
  characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
  distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}
iex> movie = Repo.preload(movie, :actors)
%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
  actors: [
    %Friends.Actor{
      __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
      id: 1,
      movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
      name: "Tyler Sheridan"
    },
    %Friends.Actor{
      __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
      id: 2,
      movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
      name: "Gary"
    }
  ], # actors are LOADED!!
  characters: [],
  distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

이제 영화의 배우들을 다음처럼 요청합니다.

iex> movie.actors
[
  %Friends.Actor{
    __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
    id: 1,
    movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
    name: "Tyler Sheridan"
  },
  %Friends.Actor{
    __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
    id: 2,
    movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
    name: "Gary"
  }
]

Join문 사용하기

Ecto.Query.join/5 함수를 이용해 join문을 포함한 쿼리를 실행할 수 있습니다.

iex> alias Friends.Character
iex> query = from m in Movie,
              join: c in Character,
              on: m.id == c.movie_id,
              where: c.name == "Wade Watts",
              select: {m.title, c.name}
iex> Repo.all(query)
15:28:23.756 [debug] QUERY OK source="movies" db=5.5ms
[{"Ready Player One", "Wade Watts"}]

on 표현식은 키워드 리스트도 쓸 수 있습니다.

from m in Movie,
  join: c in Character,
  on: [id: c.movie_id], # keyword list
  where: c.name == "Wade Watts",
  select: {m.title, c.name}

위 예제에서 m in Movie로 Ecto 스키마를 조인했습니다. Ecto query에도 조인할 수 있습니다. 영화 테이블에 1에서 5까지의 숫자로 된 “별점”을 나타내는 stars 컬럼이 있다고 해봅시다.

movies = from m in Movie, where: [stars: 5]
from c in Character,
  join: ^movies,
  on: [id: c.movie_id], # keyword list
  where: c.name == "Wade Watts",
  select: {m.title, c.name}

Ecto Query DSL은 복잡한 데이터베이스 쿼리를 만드는 데 필요한 모든 것을 제공하는 강력한 도구입니다. 이 단원에서는 쿼리를 시작하는데 필요한 기본 초석 정도를 소개했습니다.

강의에 실수가 있거나 기여하고 싶은 부분이 있으신가요? GitHub에서 이 강의를 수정해보세요!