DEV Community

Marcos Vinicius O. Silveira
Marcos Vinicius O. Silveira

Posted on

Organizando queries com query composition no elixir

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
wlsf profile image
Willian Frantz

Muito bom essa aplicação para Query Composition, e é bacana demais ver como o Ecto simplifica isso!

Obrigado por compartilhar conosco <3

Collapse
 
tporto profile image
Thiago Porto

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.