💧 Elixir: Trabalhando com Associações no Ecto
Tags: elixir, ecto, braziliandevs, database
Published: 2022-03-20 20:08:54

Primeiramente, este artigo foi feito tendo como base o [post em inglês do José Valim](http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/), 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](#setup)
* [Associações](#associacoes)
* [Inserindo Registros](#inserindo-registros)
* [Querying Associations](#querying-associations)
* [Manipulando Associações](#manipulando-associacoes)
* [Deletando Associações](#deletando-associacoes)
* [Embeds (Campos Json)](#embeds)

[![A Jornada do Autodidata em Inglês](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pkoqkmh9hic17tnddjf6.png)](https://go.hotmart.com/R93865620U)

# Setup do Projeto <a name="setup"></a>

Criando o projeto e entrando na pasta do projeto:
```bash
$ mix phx.new blog --binary-id && cd blog
```

Criando o banco de dados:
```bash
$ mix ecto.create
```

Criando os schemas e migrations:

```bash
$ mix phx.gen.schema Post posts title body:text 
```

```bash
$ mix phx.gen.schema Comment comments post_id:references:posts body:text 
```

# Associações <a name="associacoes"></a>

As associações no Ecto são usadas quando duas fontes diferentes (tabelas) são ligadas através de chaves estrangeiras (foreign keys).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8s1sayjd6ohovm3os3g8.png) 

Um exemplo clássico desta configuração é um post que __tem muitos__ comentários:

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

```elixir
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 <a name="inserindo-registros"></a>

Para gente testar as consultas a seguir, vamos inserir alguns registros.

Criando as tabelas no banco (rodando as migrations):
```bash
$ mix ecto.migrate 
```

Vamos criar alguns posts e comentários. Adicione o conteúdo abaixo ao arquivo `priv/repo/seeds.ex`:

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

```bash
$ mix run priv/repo/seeds.exs
```

# Associações de consulta (Querying associations) <a name="querying-associations"></a>

Uma das vantagens de definir associações é que elas podem ser usadas em consultas. Por exemplo:

```elixir
iex> import Ecto.Query

iex> Blog.Repo.all(from p in Blog.Post, preload: [:comments])
```
Resultado:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gon01qx3x6fqcs9o83ey.png)
 
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:

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

```elixir
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 <a name="manipulando-associacoes"></a>


Enquanto o __Ecto 2.0__ permite inserir um post com múltiplos comentários em uma única operação, por exemplo:

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

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

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

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vtnvb2k1b7srudubt56t.png)

e ao criar um post, faríamos:

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

```elixir
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 <a name="deletando-associacoes"></a>



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:

```elixir
has_many :comments, Blog.Comment, on_delete: :delete_all
```

Modificando o Schema do post:
```elixir
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`](https://hexdocs.pm/ecto/Ecto.Schema.html#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)`:

```elixir
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 <a name="embeds"></a>


Acesse a continuação [Elixir: Trabalhando com Ecto Embeds (Campos Json)](https://dev.to/maiquitome/elixir-trabalhando-com-ecto-embeds-1laf)