💧🔎 Sintaxes de consulta em Elixir: pipe X palavras-chave

maiquitome

Dev Maiqui 🇧🇷

Posted on May 10, 2022

💧🔎 Sintaxes de consulta em Elixir: pipe X palavras-chave

Existem duas sintaxes de consulta em banco de dados usadas na linguagem de programação Elixir. Neste post vamos comparar a sintaxe baseada em pipe (macro) e a sintaxe baseada em palavras-chave. Se você não souber ainda fazer as consultas, fique tranquilo, você vai conseguir aprender com os exemplos. Você pode encontrar sobre isso na documentação mas, neste post; vamos abordar exemplos em uma pequena API que criaremos.

A Jornada do Autodidata em Inglês

⚙️ Criando o Projeto

Link do projeto no github: https://github.com/maiquitome/learning_queries

Criando o projeto:

$ mix phx.new learning_queries --app my_blog
Enter fullscreen mode Exit fullscreen mode

Entrando na pasta do projeto:

$ cd learning_queries
Enter fullscreen mode Exit fullscreen mode

🗂 Criando as tabelas

Vamos criar as seguintes tabelas:

Image description

Para cada tabela, vamos criar o schema e a migration em cada comando abaixo:

$ mix phx.gen.schema User users first_name last_name 
Enter fullscreen mode Exit fullscreen mode
$ mix phx.gen.schema Post posts title body:text user_id:references:users
Enter fullscreen mode Exit fullscreen mode
$ mix phx.gen.schema Comment comments post_id:references:posts body:text 
Enter fullscreen mode Exit fullscreen mode

➕ Inserindo os dados

Para inserir as chaves estrangeiras corretamente lembre de adicioná-las a função cast, senão esses campos ficarão sempre nulos:

Image description

No Post não coloque o user_id no validate_required, pois precisamos de um user_id nulo para testarmos o left join mais para frente, ou seja, um Post sem usuário atrelado.

Vamos também remover a obrigatoriedade do last_name, para testarmos o coalesce mais pra frente:
Image description

Copie para o seu arquivo seeds: https://github.com/maiquitome/learning_queries/blob/main/priv/repo/seeds.exs

Execute o comando abaixo para criar o banco de dados, rodar as migrations (criar as tabelas no banco) e inserir os dados do arquivo seeds:

$ mix ecto.setup
Enter fullscreen mode Exit fullscreen mode

🔎 Consultando usuários ("users" X MyBlog.User)

Vamos criar as consultas em dois arquivos separados:

Em lib/my_blog/users_query_with_keyword.ex vamos colocar as consultas usando a sintaxe de palavra-chave (keyword):

defmodule MyBlog.UsersQueryWithKeyword do
  import Ecto.Query

  alias MyBlog.Repo

  def find_all_by_first_name(first_name) do
    # DICA: Podemos usar "users" ou MyBlog.User.

    # query =
    #   from u in "users",
    #     where: u.first_name == ^first_name,
    #     select: u.last_name

    query =
      from u in MyBlog.User,
        where: u.first_name == ^first_name

    Repo.all(query)
  end
end
Enter fullscreen mode Exit fullscreen mode
  • Com "users": é obrigatório usar o select.
  • Com MyBlog.User: não é obrigatório usar o select, e sem o select retorna os schemas.

Em lib/my_blog/users_query_with_pipe.ex vamos colocar as consultas usando a sintaxe de pipe (macro):

defmodule MyBlog.UsersQueryWithPipe do
  import Ecto.Query

  alias MyBlog.Repo

  def find_all_by_first_name(first_name) do
    # "users"
    # |> where([u], u.first_name == ^first_name)
    # |> select([u], u.last_name)
    # |> Repo.all()

    MyBlog.User
    |> where([u], u.first_name == ^first_name)
    |> Repo.all()
  end
end
Enter fullscreen mode Exit fullscreen mode

Vamos testar usando o iex:

$ iex -S mix
Enter fullscreen mode Exit fullscreen mode

Resultado usando "users" com select:

iex> MyBlog.UsersQueryWithKeyword.find_all_by_first_name "Mike"
...
["Mago", "Shinoda", "Candys", "Tyson"]
Enter fullscreen mode Exit fullscreen mode

Resultado usando MyBlog.User com select:

iex> MyBlog.UsersQueryWithKeyword.find_all_by_first_name "Mike"
...
["Mago", "Shinoda", "Candys", "Tyson"]
Enter fullscreen mode Exit fullscreen mode

Resultado usando MyBlog.User sem select (retorna schema):

iex> MyBlog.UsersQueryWithKeyword.find_all_by_first_name "Maiqui"
...
[
  %MyBlog.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    first_name: "Maiqui",
    id: 1,
    inserted_at: ~N[2022-05-08 00:15:03],
    last_name: "Tomé",
    updated_at: ~N[2022-05-08 00:15:03]
  }
]
Enter fullscreen mode Exit fullscreen mode

👍 Like

keyword:

def find_all_by_letter(letter) do
    like = "%" <> letter <> "%"

    from(u in MyBlog.User,
      where: like(u.first_name, ^like),
      or_where: like(u.last_name, ^like),
      select: [u.first_name, u.last_name]
    )
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

pipe:

def find_all_by_letter(letter) do
    like = "%" <> letter <> "%"

    MyBlog.User
    |> where([u], like(u.first_name, ^like))
    |> or_where([u], like(u.last_name, ^like))
    |> select([u], [u.first_name, u.last_name])
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

Resultado (lista de listas):

iex> MyBlog.UsersQueryWithPipe.find_all_by_letter "a"   
...
[["Maiqui", "Tomé"], ["Mike", "Mago"], ["Mike", "Shinoda"], ["Mike", "Candys"]]
Enter fullscreen mode Exit fullscreen mode

Também podemos ter o resultado de uma lista de mapas:
Image description

Resultado (lista de mapas):

iex> MyBlog.UsersQueryWithPipe.find_all_by_letter "a"
...
[
  %{first_name: "Maiqui", last_name: "Tomé"},
  %{first_name: "Mike", last_name: "Mago"},
  %{first_name: "Mike", last_name: "Shinoda"},
  %{first_name: "Mike", last_name: "Candys"}
]
Enter fullscreen mode Exit fullscreen mode

🧩 Fragment com coalesce

Vamos pesquisar todos os últimos nomes dos usuários, mas para o usuário que o last_name estiver vazio, vamos substituir por "não cadastrado".

No Postgres a função que faz isso se chama coalesce, no MySQL é o ifnull e no Oracle nvl. Para usar essas funções existe o fragment do Ecto. O fragment é usado sempre para injetar fragmentos SQL (e não SQL) nas consultas. Como estou usando o Postgres vou usar o coalesce neste exemplo:

O sinal de interrogação ? é substituído pelos valores passados após separados por ,.

keyword:

def find_all_and_if_null_replaces() do
    from(u in MyBlog.User,
      select: fragment("coalesce(?, ?)", u.last_name, "não cadastrado")
    )
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

pipe:

def find_all_and_if_null_replaces() do
    MyBlog.User
    |> select([u], fragment("coalesce(?, ?)", u.last_name, "não cadastrado"))
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

resultado:

iex> MyBlog.UsersQueryWithKeyword.find_all_and_if_null_replaces
...
["Tomé", "Mago", "Shinoda", "Candys", "Tyson", "não cadastrado"]
Enter fullscreen mode Exit fullscreen mode

🔎 Consultando posts do usuário (join)

Vamos mostrar os posts de um usuário pelo último nome dele.

Em lib/my_blog/posts_query_with_keyword.ex:

keyword:

defmodule MyBlog.PostsQueryWithKeyword do
  import Ecto.Query

  alias MyBlog.{Post, Repo, User}

  def find_all_by_user_last_name(user_last_name) do
    from(p in Post,
      join: u in User, on: p.user_id == u.id,
      where: u.last_name == ^user_last_name
    )
    |> Repo.all()
  end
end
Enter fullscreen mode Exit fullscreen mode

Em lib/my_blog/posts_query_with_pipe.ex:

pipe:

def find_all_by_user_last_name(user_last_name) do
    Post
    |> join(:inner, [p], u in User, on: p.user_id == u.id)
    |> where([p, u], u.last_name == ^user_last_name)
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

Resultado:

iex> MyBlog.PostsQueryWithKeyword.find_all_by_user_last_name "Tomé"
...
[
  %MyBlog.Post{
    __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
    body: "Olá, esse é o primeiro post do Maiqui Tomé...",
    id: 1,
    inserted_at: ~N[2022-05-08 03:14:21],
    title: "Primeiro Post do Maiqui Tomé",
    updated_at: ~N[2022-05-08 03:14:21],
    user_id: 1
  },
  %MyBlog.Post{
    __meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
    body: "Olá, esse é o segundo post do Maiqui Tomé...",
    id: 2,
    inserted_at: ~N[2022-05-08 03:14:21],
    title: "Segundo Post do Maiqui Tomé",
    updated_at: ~N[2022-05-08 03:14:21],
    user_id: 1
  }
]
Enter fullscreen mode Exit fullscreen mode

👤 Posts sem usuário (left join)

Vamos escrever duas funções, uma para trazer todos os posts somente que contenham usuário atrelado (usando o inner) e uma função para trazer todos os posts incluindo sem usuário (usando o left).

keyword:

def find_all_with_user_only() do
    # DICA: inner_join == join

    from(p in Post,
      inner_join: u in User, on: p.user_id == u.id
    )
    |> Repo.all()

    # from(p in Post,
    #   join: u in User, on: p.user_id == u.id
    # )
    # |> Repo.all()
  end

  def find_all_even_no_user() do
    from(p in Post,
      left_join: u in User, on: p.user_id == u.id
    )
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

pipe:

def find_all_with_user_only() do
    Post
    |> join(:inner, [p], u in User, on: p.user_id == u.id)
    |> Repo.all()
  end

  def find_all_even_no_user() do
    Post
    |> join(:left, [p], u in User, on: p.user_id == u.id)
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

🔛 on X assoc()

Vamos usar o assoc() ao invés do on e vamos tentar buscar todos os posts apenas que tenham usuário atrelado:

keyword:

def find_all_with_user_only() do
    # from(p in Post,
    #   join: u in User, on: p.user_id == u.id
    # )
    # |> Repo.all()

    from(p in Post,
      join: u in assoc(p, :users)
    )
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

pipe:

def find_all_with_user_only() do
    # Post
    # |> join(:inner, [p], u in User, on: p.user_id == u.id)
    # |> Repo.all()

    Post
    |> join(:inner, [p], u in assoc(p, :users))
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

Ao testarmos receberemos um erro nos avisando que não foi possível encontrar a associação com users no schema MyBlog.Post:

iex> MyBlog.PostsQueryWithKeyword.find_all_with_user_only
** (Ecto.QueryError) lib/my_blog/posts_query_with_keyword.ex:17: could not find association `users` on schema MyBlog.Post in query:

from p0 in MyBlog.Post,
  join: u1 in assoc(p0, :users),
  select: p0

    (ecto 3.8.2) lib/ecto/repo/queryable.ex:205: Ecto.Repo.Queryable.execute/4
    (ecto 3.8.2) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
Enter fullscreen mode Exit fullscreen mode

E se invertêssemos? Vamos buscar todos os usuários apenas que tenham posts:

keyword:

def find_all_with_user_only() do
    # from(p in Post,
    #   join: u in User, on: p.user_id == u.id
    # )
    # |> Repo.all()

    # from(p in Post,
    #   join: u in assoc(p, :users)
    # )
    # |> Repo.all()

    from(u in User,
      join: p in assoc(u, :posts)
    )
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

pipe:

def find_all_with_user_only() do
    # Post
    # |> join(:inner, [p], u in User, on: p.user_id == u.id)
    # |> Repo.all()

    # Post
    # |> join(:inner, [p], u in assoc(p, :users))
    # |> Repo.all()

    User
    |> join(:inner, [u], p in assoc(u, :posts))
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

Ao testarmos receberemos um erro avisando que não foi possível encontrar a associação com posts no schema MyBlog.User:

iex> MyBlog.PostsQueryWithKeyword.find_all_with_user_only
** (Ecto.QueryError) lib/my_blog/posts_query_with_keyword.ex:22: could not find association `posts` on schema MyBlog.User in query:

from u0 in MyBlog.User,
  left_join: p1 in assoc(u0, :posts),
  select: u0

    (ecto 3.8.2) lib/ecto/repo/queryable.ex:205: Ecto.Repo.Queryable.execute/4
    (ecto 3.8.2) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
Enter fullscreen mode Exit fullscreen mode

Então vamos adicionar as associações:
Image description

Ao tentarmos buscar todos os usuários apenas que tenham posts vamos receber os dados certinho:

pipe:

def find_all_with_user_only() do
    # Post
    # |> join(:inner, [p], u in User, on: p.user_id == u.id)
    # |> Repo.all()

    # Post
    # |> join(:inner, [p], u in assoc(p, :users))
    # |> Repo.all()

    User
    |> join(:inner, [u], p in assoc(u, :posts))
    |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

Image description

Mas ao tentarmos buscar todos os posts apenas que tenham usuário continuamos com o mesmo erro:

pipe:

def find_all_with_user_only() do
    # Post
    # |> join(:inner, [p], u in User, on: p.user_id == u.id)
    # |> Repo.all()

    Post
    |> join(:inner, [p], u in assoc(p, :users))
    |> Repo.all()

    # User
    # |> join(:inner, [u], p in assoc(u, :posts))
    # |> Repo.all()
  end
Enter fullscreen mode Exit fullscreen mode

Image description

E agora? Como vamos solucionar esse problema? Na verdade deixamos um bug no nosso código, um pequeno detalhe. Como um Post pertence a um User, ao invés de :users precisamos tirar o s e deixar apenas :user:

Image description

Esse bug foi interesante para mostrar que podemos ficar confusos do porque não está funcionando. Precisamos estar atentos de como as associações estão construidas ao usar o assoc().

🎯 Conclusão

Conseguimos abordar vários exemplos e claro que existem muito mais, mas acredito que conseguimos pegar a essência neste post. Ao se deparar com uma consulta que não esteja aqui neste post, acredito que facilmente você encontre a solução na documentação.

Agora podemos partir para o estudo das composições das consultas e, para isso, deixarei duas dicas de leitura:

💖 💪 🙅 🚩
maiquitome
Dev Maiqui 🇧🇷

Posted on May 10, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related