Primeiramente, este artigo foi feito tendo como base o post em inglês do José Valim, criador da linguagem Elixir.
Resolvi fazer esse post, para traduzir o post citado acima para o português e adicionar mais detalhes, fazendo um projeto do zero, podendo assim, ajudar programadores iniciantes da linguagem.
O código do projeto você pode encontrar aqui: https://github.com/maiquitome/blog
Neste post, vamos aprender a trabalhar com associações no Ecto, como ler, inserir, atualizar e excluir associações
e incorporações (embeds)
.
- Setup do Projeto
- Associações
- Inserindo Registros
- Querying Associations
- Manipulando Associações
- Deletando Associações
- Embeds (Campos Json)
Setup do Projeto
Criando o projeto e entrando na pasta do projeto:
$ mix phx.new blog --binary-id && cd blog
Criando o banco de dados:
$ mix ecto.create
Criando os schemas e migrations:
$ mix phx.gen.schema Post posts title body:text
$ mix phx.gen.schema Comment comments post_id:references:posts body:text
Associações
As associações no Ecto são usadas quando duas fontes diferentes (tabelas) são ligadas através de chaves estrangeiras (foreign keys).
Um exemplo clássico desta configuração é um post que tem muitos comentários:
defmodule Blog.Post do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "posts" do
field :body, :string
field :title, :string
# adicione essa linha
has_many :comments, Blog.Comment
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:title, :body])
end
end
E um comentário pertence a um post:
defmodule Blog.Comment do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "comments" do
field :body, :string
# remova essa linha
# field :post_id, :binary_id
# adicione essa linha
belongs_to :post, Blog.Post
timestamps()
end
@doc false
def changeset(comment, attrs) do
comment
# adicione o campo :post_id
|> cast(attrs, [:body, :post_id])
|> validate_required([:body])
end
end
Inserindo registros
Para gente testar as consultas a seguir, vamos inserir alguns registros.
Criando as tabelas no banco (rodando as migrations):
$ mix ecto.migrate
Vamos criar alguns posts e comentários. Adicione o conteúdo abaixo ao arquivo priv/repo/seeds.ex
:
# Primeiro Post
{:ok, post1} = %Blog.Post{}
|> Blog.Post.changeset(%{title: "Primeiro Post", body: "Conteúdo do primeiro post"})
|> Blog.Repo.insert
%Blog.Comment{}
|> Blog.Comment.changeset(%{body: "comentário do Primeiro Post", post_id: post1.id})
|> Blog.Repo.insert
# Segundo Post
{:ok, post2} = %Blog.Post{}
|> Blog.Post.changeset(%{title: "Segundo Post", body: "Conteúdo do segundo post"})
|> Blog.Repo.insert
%Blog.Comment{}
|> Blog.Comment.changeset(%{body: "comentário do Segundo Post", post_id: post2.id})
|> Blog.Repo.insert
# Terceiro Post
{:ok, post3} = %Blog.Post{}
|> Blog.Post.changeset(%{title: "Terceiro Post", body: "Conteúdo do terceiro post"})
|> Blog.Repo.insert
%Blog.Comment{}
|> Blog.Comment.changeset(%{body: "comentário do Terceiro Post", post_id: post3.id})
|> Blog.Repo.insert
$ mix run priv/repo/seeds.exs
Associações de consulta (Querying associations)
Uma das vantagens de definir associações é que elas podem ser usadas em consultas. Por exemplo:
iex> import Ecto.Query
iex> Blog.Repo.all(from p in Blog.Post, preload: [:comments])
Agora todos os posts serão buscados no banco de dados com seus comentários associados. O exemplo acima realizará duas consultas: uma para carregar todos os posts e outra para carregar todos os comentários. Esta é frequentemente a forma eficiente de carregar associações do banco de dados (mesmo que duas consultas sejam realizadas), pois precisamos receber e analisar apenas os resultados dos POSTS + COMENTÁRIOS.
Também é possível pré-carregar (preload) as associações usando as uniões enquanto se realizam consultas mais complexas. Por exemplo, imagine que tanto os posts como os comentários têm votos e você quer apenas comentários com mais votos do que o próprio post:
Blog.Repo.all from p in Blog.Post,
join: c in assoc(p, :comments),
where: c.votes > p.votes,
preload: [comments: c]
O exemplo acima agora realizará uma única consulta, encontrando todos os posts e os respectivos comentários que correspondam aos critérios. Como esta consulta realiza um JOIN, o número de resultados retornados pelo banco de dados é POSTS * COMMENTS, onde o Ecto então processa e associa todos os comentários no post apropriado.
Finalmente, o Ecto também permite que os dados sejam pré-carregados em estruturas (structs) após terem sido carregados através da função Repo.preload/3
:
Blog.Repo.preload posts, :comments
Isto é especialmente útil porque o Ecto não suporta carregamento preguiçoso (lazy loading). Se você invocar post.comments
e comentários posteriores não tiverem sido pré-carregados, vai retornar Ecto.Association.NotLoaded
. O carregamento preguiçoso é frequentemente uma fonte de confusão e problemas de desempenho e o Ecto pressiona os desenvolvedores a fazerem o que é correto. Portanto, o Repo.preload/3
permite que as associações sejam explicitamente carregadas em qualquer lugar, a qualquer momento.
Manipulando Associações
Enquanto o Ecto 2.0 permite inserir um post com múltiplos comentários em uma única operação, por exemplo:
Repo.insert!(%Post{
title: "Hello",
body: "world",
comments: [
%Comment{body: "Excellent!"}
]
})
Muitas vezes você pode querer dividi-lo em etapas diferentes para ter mais flexibilidade no gerenciamento dessas entradas. Por exemplo, você poderia usar conjuntos de mudanças (changesets)
para construir seus posts e comentários ao longo do caminho:
Preste atenção no Ecto.Changeset.put_assoc
.
post = Ecto.Changeset.change(%Post{}, title: "Hello", body: "world")
comment = Ecto.Changeset.change(%Comment{}, body: "Excellent!")
post_with_comments = Ecto.Changeset.put_assoc(post, :comments, [comment])
Repo.insert!(post_with_comments)
Ou manuseando cada entrada individualmente dentro de uma transação:
Preste atenção no Ecto.build_assoc
.
Repo.transaction fn ->
post = Repo.insert!(%Post{title: "Hello", body: "world"})
# Build a comment from the post struct
comment = Ecto.build_assoc(post, :comments, body: "Excellent!")
Repo.insert!(comment)
end
Ecto.build_assoc/3
constrói o comentário utilizando a identificação atualmente definida na estrutura do post. É equivalente a:
%Comment{post_id: post.id, body: "Excellent!"}
A função Ecto.build_assoc/3
é especialmente útil nos controladores (controllers) do Phoenix. Por exemplo, poderíamos ter uma tabela de usuário...
e ao criar um post, faríamos:
Ecto.build_assoc(current_user, :post)
Pois, provavelmente queremos associar o post ao usuário atualmente logado na aplicação.
Em outro controlador, poderíamos construir um comentário para um post existente:
Ecto.build_assoc(post, :comments)
O Ecto não fornece funções como post.comments << comment
que permite misturar dados persistidos com dados não-persistidos. O único mecanismo para mudar tanto o post como os comentários em simultâneo, é por changesets
que iremos explorar quando falarmos sobre incorporações (embeds)
e associações aninhadas (nested associations).
Deletando Associações
Quando definimos um has_many/3
ou has_one/3
, você também pode passar uma opção :on_delete
que especifica qual ação deve ser executada nas associações quando o pai é excluído. Por exemplo, se um post for excluído, então os comentários associados a ele também serão excluídos:
has_many :comments, Blog.Comment, on_delete: :delete_all
Modificando o Schema do post:
defmodule Blog.Post do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "posts" do
field :body, :string
field :title, :string
# modifique aqui adicionando `on_delete: :delete_all`
has_many :comments, Blog.Comment, on_delete: :delete_all
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:title, :body])
end
end
Além disso, :nilify_all
também é suportado, sendo que :nothing
é o padrão. Verifique has_many/3
na documentação para mais informações.
O uso desta opção é DESENCORAJADA para a maioria dos bancos de dados relacionais. Ao invés disso, em sua migração, defina references(:parent_id, on_delete: :delete_all)
:
defmodule Blog.Repo.Migrations.CreateComments do
use Ecto.Migration
def change do
create table(:comments, primary_key: false) do
add :id, :binary_id, primary_key: true
add :body, :text
# modifique aqui adicionando :delete_all
add :post_id, references(:posts, on_delete: :delete_all, type: :binary_id)
timestamps()
end
create index(:comments, [:post_id])
end
end
Embeds
Acesse a continuação Elixir: Trabalhando com Ecto Embeds (Campos Json)
Top comments (0)