DEV Community

Maiqui TomĂ© đŸ‡§đŸ‡·
Maiqui TomĂ© đŸ‡§đŸ‡·

Posted on • Updated on

 

💧Elixir: Trabalhando com AssociaçÔes no Ecto

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

Criando o projeto e entrando na pasta do projeto:

$ mix phx.new blog --binary-id && cd blog
Enter fullscreen mode Exit fullscreen mode

Criando o banco de dados:

$ mix ecto.create
Enter fullscreen mode Exit fullscreen mode

Criando os schemas e migrations:

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

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).

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Inserindo registros

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

Criando as tabelas no banco (rodando as migrations):

$ mix ecto.migrate 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
$ mix run priv/repo/seeds.exs
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

Resultado:
Image description

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!"}
  ]
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!"}
Enter fullscreen mode Exit fullscreen mode

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

e ao criar um post, farĂ­amos:

Ecto.build_assoc(current_user, :post)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Embeds

Acesse a continuação Elixir: Trabalhando com Ecto Embeds (Campos Json)

Top comments (0)