💧 Elixir: Trabalhando com Nested Associations
Tags: elixir, ecto, braziliandevs, database
Published: 2022-03-27 20:11:02

Esse post é uma continuação da série sobre __associações com Ecto__ no __Elixir__. Para acompanhar este post seria bacana você ver os anteriores antes desse, assim o seu entendimento será melhor.

Nesse post vamos construir um projeto bem simples apenas pra entender sobre __associações aninhadas__, sobre as funções `cast_assoc`, `put_assoc`, `preload`...

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

# Nested Associations

Da mesma forma que usamos os `changesets` para manipular os `embeds`, também podemos usá-los para mudar as associações dos filhos, em simultâneo, em que estamos manipulando os pais.

Um dos benefícios deste recurso é que podemos usá-los para construir __formas aninhadas__ em uma aplicação Phoenix. Enquanto os formulários aninhados em outras línguas e estruturas podem ser confusos e complexos, o Ecto usa `changesets` e validações explícitas para fornecer uma maneira simples e direta de manipular __múltiplas estruturas de uma só vez__.

# Criando o projeto

Vamos ver um exemplo de como usar o que vimos nos posts anteriores para trabalhar com __associações aninhadas__ em Phoenix.

Nota: você precisará do `Phoenix_ecto 3.0` para poder seguir este exemplo.

Primeiro, vamos criar uma aplicação Phoenix:

```bash
$ mix phx.new my_todo_list && cd my_todo_list
```

Não se esqueça de criar o banco de dados:

```bash
$ mix ecto.create
```

O exemplo que construiremos é um clássico para fazer lista, onde uma lista tem muitos itens. Vamos gerar o recurso `TodoList`:

```bash
$ mix phx.gen.html TodoLists TodoList todo_lists title
```

Você pode ver mais sobre o comando acima na documentação: https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html.

Vamos gerar um modelo `TodoItem`:

```bash
mix phx.gen.schema TodoItem todo_items body:text todo_list_id:references:todo_lists
```

Você pode ver mais sobre o comando acima na documentação:
https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Schema.html



# Adicionando as rotas



Em "lib/my_todo_list_web/router.ex":

```elixir
...
pipeline :api do
  plug :accepts, ["json"]
end

scope "/", MyTodoListWeb do
  pipe_through :browser

  get "/", PageController, :index

  # adicione essa linha
  resources "/todolists", TodoListController
end
...
```



### Testando as rotas



Suba o servidor:
```bash
$ mix phx.server
```

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

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

Observe que faltam os `todo_items`, então vamos fazer isso agora.



# Adicionando a associação dos itens



Vamos agora associar os `todo_items` a um `todo_list`.

Abra o módulo `MyTodoList.TodoLists.TodoList` em "lib/my_todo_list/todo_lists/todo_list.ex" e adicione a definição `has_many` no bloco do `schema`:

```elixir
defmodule MyTodoList.TodoLists.TodoList do
  use Ecto.Schema
  import Ecto.Changeset

  schema "todo_lists" do
    field :title, :string

    # adicione essa linha
    has_many :todo_items, MyTodoList.TodoItem

    timestamps()
  end

  @doc false
  def changeset(todo_list, attrs) do
    todo_list
    |> cast(attrs, [:title])
    |> validate_required([:title])
  end
end
```

Em seguida, vamos também __incluir/fundir (cast)__ "todo_items" na função changeset do `TodoList`:
```elixir
def changeset(todo_list, params \\ %{}) do
  todo_list
  |> cast(params, [:body])
  |> cast_assoc(:todo_items, required: true)
end
```

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

Ou seja, uma `todo_list` tem muitos `todo_items`.



### cast_assoc e cast_embed X put_assoc e put_embed



Note que estamos usando `cast_assoc` em vez de `put_assoc` neste exemplo. Ambas as funções são definidas no __Ecto.Changeset__. `cast_assoc (ou cast_embed)` é usado quando se deseja gerenciar associações ou incorporações (embeds) __com base em parâmetros externos__, tais como os dados recebidos através dos formulários Phoenix. Nesses casos, o Ecto irá comparar os dados existentes na estrutura com os dados enviados através do formulário e gerar as operações adequadas. Por outro lado, usamos `put_assoc (ou put_embed)` quando já temos as associações (ou embeds) como __structs e changesets__, e queremos simplesmente dizer ao Ecto para pegar essas entradas como estão.



# Cadastrando os todo_items



Como adicionamos `todo_items` como um __campo obrigatório__, estamos prontos para submetê-los através do formulário. Portanto, vamos mudar nosso modelo para enviar todos os itens também. Abra "lib/my_todo_list_web/templates/todo_list/form.html.heex" e adicione o seguinte entre o __input do título__ e o __botão submit__:

```elixir
<%= inputs_for f, :todo_items, fn i -> %>
  <div class="form-group">
    <%= label i, :body, "Task ##{i.index + 1}", class: "control-label" %>
    <%= text_input i, :body, class: "form-control" %>
    <%= if message = i.errors[:body] do %>
      <span class="help-block"><%= message %></span>
    <% end %>
  </div>
<% end %>
```

A função `inputs_for/4` vem do [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#inputs_for/4) e nos permite gerar campos para uma associação ou um embed, emitindo uma nova estrutura de formulário (representada pela variável i no exemplo acima) para podermos trabalhar com ela. Na função `inputs_for/4`, nós geramos uma entrada de texto para cada ‘item’.

Agora que mudamos o template (modelo), o passo final é mudar a nova ação no controlador para incluir dois itens todos vazios por padrão na lista de todos:

Em "lib/my_todo_list_web/controllers/todo_list_controller.ex":

```elixir
...
def new(conn, _params) do
  # remova essa linha
  # changeset = TodoLists.change_todo_list(%TodoList{})

  # adicione essa linha
  changeset = TodoList.changeset(%TodoList{todo_items: [%MyTodoList.TodoItem{}, %MyTodoList.TodoItem{}]})


  render(conn, "new.html", changeset: changeset)
end
...
```

Agora é possível cadastrar os itens:

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


### Entendendo mais o cast_assoc

Vamos colocar um `IO.inspect`:

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

Antes de salvar, clicando no `New Todo list`:

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

Após salvar:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zaohkcbgmd7vgkioz7ms.png)
 
#### Padrão do cast_assoc

E enquanto o padrão é chamar `MyTodoList.TodoItem.changeset/2`, é possível personalizar a função a ser invocada que vai colocar `todo_items` em `todo_list` através da opção `:with`:

```elixir
|> cast_assoc(:todo_items, required: true, with: &custom_changeset/2)
```

Portanto, se uma associação tem regras de validação diferentes, dependendo se ela é enviada como parte de uma associação aninhada ou quando administrada diretamente, podemos facilmente manter essas regras comerciais separadas, fornecendo duas funções diferentes de conjunto de mudanças. E como usamos apenas funções, até o fim, elas são fáceis de compor e testar.



### Editando



Entretanto, se você tentar editar a lista de todos recém-criada, você deverá receber um __erro__:

```
using inputs_for for association `todo_items` from `MyTodoList.TodoLists.TodoList` but it was not loaded. Please preload your associations before using them in inputs_for
```
 
Como a mensagem de erro diz que precisamos __pré-carregar__ todos os itens
para ações de edição e atualização em `MyApp.TodoListController`.
Abra seu controlador e mude a seguinte linha em ambas as ações:

Em "lib/my_todo_list_web/controllers/todo_list_controller.ex", podemos perceber que o `edit` e o `update` usam a função `TodoLists.get_todo_list!(id)` e ela que precisamos adicionar o `preload`.

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

Em "lib/my_todo_list/todo_lists.ex" adicione o `preload`:

```elixir
def get_todo_list!(id), do: Repo.get!(TodoList, id) |> Repo.preload(:todo_items)
```

Agora também deve ser possível atualizar todos os itens com a todo_list:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fpgfpyimdi3ra2shnrxz.png)
 
Tanto as operações de inserção como as de atualização são alimentadas por `changesets`, como podemos ver em nossas ações de controle:

```elixir
changeset = TodoList.changeset(todo_list, todo_list_params)
```

Todos os benefícios que discutimos a respeito de `changesets` ainda se aplicam aqui. Ao inspecionar o `changeset` antes de chamar o `Repo.insert` ou `Repo.update`, é possível ver um retrato de todas as mudanças que vão acontecer no banco de dados.

Não apenas isso, o processo de validação por trás dos `changesets` é explícito. Como adicionamos `todo_items` como um __campo obrigatório__ no schema da `todo_list`, toda vez que chamarmos `MyTodoList.TodoLists.TodoList.changeset/2`, `MyTodoList.TodoItem.changeset/2` será chamado para cada item enviado através do formulário. Os `changesets` devolvidos para cada `todo_item` são então armazenados no __changeset principal__ do `todo_list` (isso é efetivamente uma árvore de alterações).

Para nos ajudar a construir nossa intuição em relação às mudanças, vamos acrescentar algumas validações a todos os itens e também permitir que eles sejam apagados.



# Deletando todo_items


Ao tentar deletar uma lista, recebemos o seguinte erro de constraint:

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

Vamos lembrar que no primeiro post da série, falamos sobre isso. Podemos colocar no schema `on_delete: :delete_all` ou podemos colocar na migration. __Lembrando que o mais indicado seria colocar na migration.__ Apenas para um teste rápido vamos colocar no schema.

```elixir
defmodule MyTodoList.TodoLists.TodoList do
  use Ecto.Schema
  import Ecto.Changeset

  schema "todo_lists" do
    field :title, :string

    # adicione aqui o on_delete
    has_many :todo_items, MyTodoList.TodoItem, on_delete: :delete_all

    timestamps()
  end

  @doc false
  def changeset(todo_list, params \\ %{}) do
    todo_list
    |> cast(params, [:title])
    |> validate_required([:title])
    |> cast_assoc(:todo_items, required: true)
  end
end
```

Agora sim conseguimos deletar, onde ao deletar uma `todo_list` serão removidos também todos os `todo_items`:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5fmthxjd79hc3ypmkaef.png)
 

Lembrando que o projeto final você pode encontrar aqui: https://github.com/maiquitome/my_todo_list