Zapytania

W tej lekcji będziemy kontynuować budowanie aplikacji Friends i modułu do katalogowania filmów, nad którym pracowaliśmy w poprzedniej lekcji.

Pobieranie rekordów przy pomocy Ecto.Repo

Jak być może pamiętasz, „repozytorium” w Ecto jest interfejsem do miejsca, w którym trzymamy dane, takiego jak nasza baza danych Postgres. Cała komunikacja z bazą danych będzie dokonywana za pośrednictwem repozytorium.

Jest kilka funkcji, dzięki którym możemy wykonać proste zapytania bezpośrednio do Friends.Repo.

Pobieranie rekordów na podstawie ID

Możemy użyć funkcji Repo.get/3, aby pobrać z bazy danych rekord o danym ID. Funkcja ta wymaga dwóch argumentów: “odpytywalnej” struktury danych i ID rekordu, który chcemy znaleźć w bazie. Zwraca strukturę opisującą znaleziony rekord (jeśli ów istnieje), a gdy takiego nie znajdzie, zwraca nil.

Spójrzmy na poniższy przykład. Pobierzemy film, którego ID to 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"
}

Zauważ, że pierwszym argumentem, który przekazujemy do Repo.get/3 jest nasz moduł Movie. Movie jest „odpytywalny”, gdyż używa modułu Ecto.Schema i definiuje schemat swojej struktury danych. To daje modułowi Movie dostęp do protokołu Ecto.Queryable, który konwertuje strukturę danych do Ecto.Query. Zapytania Ecto są używane do pozyskiwania danych z bazy — ale o nich później.

Pobieranie rekordów na podstawie atrybutu

Rekordy spełniające zadane kryteria możemy pobierać również za pomocą funkcji Repo.get_by/3. Wymaga ona dwóch argumentów: „odpytywalnej” struktury danych i warunku, który będzie użyty w zapytaniu. Repo.get_by/3 zwraca pojedynczy wynik z repozytorium. Spójrzmy na przykład:

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"
}

Jeśli chcemy napisać bardziej skomplikowane zapytania czy też zwrócić wszystkie rekordy spełniające dany warunek, będziemy potrzebować modułu Ecto.Query.

Pisanie zapytań z Ecto.Query

Moduł Ecto.Query dostarcza nam język zapytań, który możemy wykorzystać do tworzenia zapytań i pozyskiwania danych z repozytorium aplikacji.

Zapytania oparte o słowa kluczowe z Ecto.Query.from/2

Możemy tworzyć zapytania przy pomocy makra Ecto.Query.from/2. Przyjmuje ono dwa argumenty: wyrażenie i opcjonalną listę asocjacyjną (listę typu klucz-wartość). Stwórzmy najprostsze zapytanie, aby wybrać wszystkie filmy z naszego repozytorium:

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

Aby wykonać nasze zapytanie, użyjemy funkcji Repo.all/2. Wymaganym argumentem przez nią przyjmowanym jest zapytanie Ecto, zwracana jest natomiast lista wszystkich rekordów spełniających warunki tego zapytania.

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"
  }
]

Zapytania bez przypisań z użyciem makra from

W przedstawionym wyżej przykładzie brakuje najciekawszych elementów zapytań SQL. Często chcemy odpytać bazę jedynie o konkretne pola bądź przefiltrować rekordy na podstawie jakiegoś warunku. Pobierzmy wartości title i tagline wszystkich filmów o tytule "Ready Player One":

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"
  }
]

Zauważ, że zwrócona struktura ma niepuste wartości jedynie dla pól tagline i title — jest to efektem wyrażenia select: w naszym zapytaniu.

Zapytania takie jak to nazywane są zapytaniami bez przypisań, gdyż są na tyle proste, że nie wymagają przypisań.

Przypisania w zapytaniach

Jak dotąd używaliśmy modułu implementującego protokół Ecto.Queryable (tj. Movie) jako pierwszego argumentu dla makra from. Możemy jednak użyć również wyrażenia in, jak w tym przykładzie:

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

W tym przypadku m nazywamy przypisaniem (ang. binding). Przypisania są niezwykle przydatne, ponieważ pozwalają nam odnosić się do danego modułu w innych częściach zapytania. Pobierzmy teraz z bazy wszystkie filmy, które mają ID mniejsze od 2:

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"]

Bardzo istotne jest, jak zmieniła się tu wartość wyjściowa zapytania. Użycie wyrażenia z przypisaniem w części select: pozwala nam na dokładne wskazanie, w jakiej formie mają być zwrócone wybrane pola. Możemy na przykład chcieć, by zapytanie zwracało krotki, jak w tym przykładzie:

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

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

Dobrym pomysłem jest, aby zaczynać zawsze z prostymi zapytaniami bez przypisań, a przypisania wprowadzać wtedy, kiedy faktycznie potrzebujemy odnieść się do struktury danych. Więcej na ten temat możesz znaleźć w dokumentacji Ecto.

Zapytania oparte o makra

W pokazanych wyżej przykładach używaliśmy słów kluczowych select: i where: w makrze from, aby zbudować zapytanie — są to tak zwane zapytania oparte o słowa kluczowe. Jest jednak również inny sposób tworzenia zapytań — zapytania oparte o makra. Ecto dostarcza makra dla każdego ze słów kluczowych, jak na przykład select/3 lub where/3. Każde z makr przyjmuje odpytywalną wartość, listę konkretnych przypisań i takie samo wyrażenie, jakie podalibyśmy w analogicznym zapytaniu ze słowami kluczowymi:

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"]

Niewątpliwą zaletą makr jest to, że bardzo dobrze działają z potokami funkcji:

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

Zwróć uwagę na to, że aby kontynuować pisanie zapytania po znaku nowej linii, użyliśmy ukośnika wstecznego — \.

Użycie where z interpolacją wartości

Aby użyć interpolowanych wartości lub Elixirowych wyrażeń w naszych klauzulach where, musimy użyć operatora przypięcia — ^. Pozwala nam to przypiąć wartość do zmiennej i odnosić się do tejże przypiętej wartości, zamiast zmieniać wartość przypisaną tej zmiennej.

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"]

Pobieranie pierwszych i ostatnich rekordów

Możemy pobrać pierwszy lub ostatni rekord z naszego repozytorium odpowiednio za pomocą funkcji Ecto.Query.first/2 i Ecto.Query.last/2.

Najpierw stwórzmy wyrażenie zapytania, używając funkcji first/2:

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

Następnie możemy przekazać nasze zapytanie do funkcji Repo.one/2, aby pobrać wynik:

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"
}

Funkcji Ecto.Query.last/2 używa się w dokładnie taki sam sposób:

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

Zapytania o powiązane dane

Ładowanie powiązanych rekordów

Aby mieć dostęp do powiązanych rekordów, które udostępniają nam makra belongs_to, has_many i has_one, musimy załadować odpowiednie schematy.

Spójrzmy najpierw, co się stanie, jeśli spróbujemy dostać się do rekordów aktorów związanych z danym filmem:

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

Nie mamy dostępu do tych danych, dopóki ich nie załadujemy. Istnieje kilka sposobów na ładowanie takich danych w Ecto.

Ładowanie z dwoma zapytaniami

Poniższe zapytanie załaduje powiązane dane w oddzielnym zapytaniu.

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"
  }
]

Możesz zauważyć, że powyższa linia kodu uruchomiła dwa zapytania do bazy danych — pierwsze dla wszystkich filmów, a drugie dla wszystkich aktorów powiązanych z filmem o danym ID.

Ładowanie z jednym zapytaniem

Możemy ograniczyć liczbę zapytań do bazy w następujący sposób:

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"
  }
]

Pozwala to na wykonanie tylko jednego zapytania zamiast dwóch. Opcja ta ma też inną zaletę — możemy dzięki niej wybierać pola i filtrować według wartości nie tylko filmy, ale i aktorów w tym samym zapytaniu. Przykładowo, to podejście umożliwia nam odpytanie bazy o wszystkie filmy powiazane z aktorami spełniającymi odpowiednie warunki za pomocą wyrażenia join, jak poniżej:

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

Więcej o wyrażeniach join powiemy nieco później.

Ładowanie danych dla pobranych wcześniej rekordów

Możemy również załadować powiązane schematy dla pobranych już rekordów:

iex> movie = Repo.get(Movie, 1)
%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
  actors: %Ecto.Association.NotLoaded<association :actors is not loaded>, # aktorzy NIE SĄ ZAŁADOWANI!!!
  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"
    }
  ], # aktorzy SĄ ZAŁADOWANI!!!
  characters: [],
  distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

Teraz możemy odpytać film o aktorów:

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"
  }
]

Wyrażenia join

Możemy wykonywać zapytania zawierające wyrażenia join dzięki funkcji Ecto.Query.join/5.

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"}]

Wyrażenie on może przyjąć jako argument również listę asocjacyjną:

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

W powyższym przykładzie dokonujemy łączenia ze schematem Ecto, m in Movie. Możemy też używać łączeń do zapytań Ecto. Powiedzmy, że nasza tabela z filmami ma kolumnę stars, gdzie przechowujemy ocenę filmu w postaci liczby „gwiazdek”, będącą liczbą od 1 do 5.

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

Język zapytań Ecto jest potężnym narzędziem, dostarczającym nam wszystkiego, czego potrzebujemy, by tworzyć nawet bardzo złożone zapytania. W tym wprowadzeniu pokazaliśmy kilka podstawowych elementów, dzięki którym możesz zacząć komponowanie własnych zapytań.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!