Organizando queries com query composition no elixir
Tags: elixir, ecto, query
Published: 2021-07-29 13:16:14

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:

```elixir
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:

```elixir
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:

```elixir
# 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:

```elixir
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
```