O Elixir fornece um módulo chamado Ecto.Query
para que possa ser possível efetuar consultas e alterações nos dados de uma aplicação.
Dessa forma podemos escrever queries sem a necessidade da escrita de SQLs, dando maior facilidade e flexibilidade na escrita de consultas.
Porém em projetos grandes, podemos acabar tendo queries
mais complexas e maiores, o que pode tornar a utilização do Ecto.Query
um problema se não tiver um certo cuidado.
Para exemplificar vamos pegar um exemplo onde fazemos algumas consultas de pessoas usuárias de um determinado sistema:
defmodule MyApp.Users do
import Ecto.Query, only: [from: 2, where: 2]
alias MyApp.Users.User
def get_user_by_email(email) do
Repo.get_by(User, email: email)
end
def get_users_by_city_name(city_name) do
from(user in User, join: city in City, on: city.id == user.city_id, where: city == ^city_name)
|> Repo.all()
end
def get_user_by_email_and_city(email, city_name) do
from(user in User,
join: city in City,
on: city.id == user.city_id,
where: city.name == ^city_name,
where: user.email == ^email
)
|> Repo.one()
end
end
Perceba que temos aqui 3 buscas, uma por email, uma por city, e uma terceira que busca uma pessoa por email e pela city registrada. Se analisarmos com calma, podemos ver que a terceira consulta é na verdade uma junção das 2 primeiras. Então podemos utilizar Query composition para diminuir a duplicação de código:
defmodule MyApp.Users do
import Ecto.Query, only: [from: 2]
alias MyApp.Repo
def get_user_by_email(email) do
email
|> user_by_email()
|> Repo.one()
end
def get_users_by_city_name(city_name) do
city_name
|> users_by_city_name()
|> Repo.all()
end
def get_user_by_email_and_city(email, city) do
email
|> user_by_email()
|> users_by_city_name(city)
|> Repo.one()
end
defp user_by_email(query \\ base(), email) do
from(user in query,
where: user.email == ^email
)
end
defp users_by_city_name(query \\ base(), city_name) do
from(user in query,
join: city in City,
on: city.id == user.city_id,
where: city.name == ^city_name
)
end
defp base, do: __MODULE__
end
Mas agora vamos supor que esse sistema vá crescendo e o contexto fique cada vez maior, com mais funções e mais queries. Para resolver esse problema podemos criar um módulo específico para as queries desse contexto:
# my_app/lib/my_app/users/user_queries.ex
defmodule MyApp.Users.UserQueries do
import Ecto.Query, only: [from: 2]
alias MyApp.Users.User
def user_by_email(query \\ base(), email) do
from(user in query,
where: user.email == ^email
)
end
def users_by_city_name(query \\ base(), city_name) do
from(user in query,
join: city in City,
on: city.id == user.city_id,
where: city.name == ^city_name
)
end
defp base, do: User
end
# my_app/lib/my_app/users.ex
defmodule MyApp.Users do
alias MyApp.Repo
alias MyApp.Users.UserQueries
def get_user_by_email(email) do
email
|> UserQueries.user_by_email()
|> Repo.one()
end
def get_users_by_city_name(city) do
city
|> UserQueries.users_by_city_name()
|> Repo.all()
end
def get_user_by_email_and_city(email, city_name) do
email
|> UserQueries.user_by_email()
|> UserQueries.users_by_city_name(city_name)
|> Repo.one()
end
end
A função Repo.one() retorna o registro, caso encontrado, ou nil. Para não deixarmos o tratamento desse erro para camadas superiores do projeto, podemos fazer uma última refatoração no contexto de Users , tratando esses erros e deixar os retornos mais nítidos:
defmodule MyApp.Users do
alias MyApp.Users.{User, UserQueries}
def get_user_by_email(email) do
case do_get_user_by_email(email) do
%User{} = user -> {:ok, user}
nil -> {:error, {:not_found, "User not found"}}
end
end
def get_users_by_city_name(city) do
city
|> UserQueries.users_by_city()
|> Repo.all()
end
def get_user_by_email_and_city_name(email, city_name) do
case do_get_user_by_email_and_city_name(email, city_name) do
%User{} = user -> {:ok, user}
nil -> {:error, {:not_found, "User not found"}}
end
end
defp do_get_user_by_email(email) do
email
|> UserQueries.user_by_email()
|> Repo.one()
end
defp do_get_user_by_email_and_city_name(email, city_name) do
email
|> UserQueries.user_by_email()
|> UserQueries.users_by_city_name(city_name)
|> Repo.one()
end
end
Top comments (2)
Muito bom essa aplicação para Query Composition, e é bacana demais ver como o Ecto simplifica isso!
Obrigado por compartilhar conosco <3
Muito bom, parabéns!
Qual seria a melhor abordagem em casos em que o campo pode vir nulo ou limpo? Por exemplo em consultas que tem muitos filtros e nem todos são obrigatórios.