DEV Community

Marcos Vinicius O. Silveira
Marcos Vinicius O. Silveira

Posted on

4 3

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

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

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.

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more