Do you want to pick up from where you left of?
Take me there

Associações

Nessa seção vamos aprender a utilizar o Ecto para definir e trabalhar com associações entre esquemas.

Configuração

Nós vamos utilizar a mesma aplicação Friends, das últimas lições. Você pode referir-se a configuração aqui para uma breve recapitulação.

Tipos de Associações

Existem três tipos de associações que podem ser definidas entre nossos esquemas. Vamos dar atenção ao que elas são e como implementar cada um dos tipos.

Belongs To/Has Many

Nós estamos adicionando algumas novas entidades ao modelo de domínio da nossa aplicação Friends para que seja possível categorizar nossos filmes favoritos. Vamos iniciar com dois esquemas: Movie e Character. Vamos implementar uma relação “has many/belongs to” entre os dois: Um filme tem vários (has many) personagens e um personagem pertence a (belongs to) um filme.

A Migração Has Many

Vamos gerar uma migração para Movie:

mix ecto.gen.migration create_movies

Abra o arquivo da migração recém gerada e defina a sua função change, com o intuito de criar a tabela movies:

# priv/repo/migrations/*_create_movies.exs
defmodule Friends.Repo.Migrations.CreateMovies do
  use Ecto.Migration

  def change do
    create table(:movies) do
      add :title, :string
      add :tagline, :string
    end
  end
end

O Schema Has Many

Nós vamos adicionar um esquema que especifica a relação “has many” entre um filme e os seus personagens.

# lib/friends/movie.ex
defmodule Friends.Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :tagline, :string
    has_many :characters, Friends.Character
  end
end

A macro has_many/3 não adiciona dados ao banco de dados por si só. O que ela faz é utilizar uma chave estrangeira no esquema associado (characters) para tornar as associações de personagens de um filme disponíveis. Isso é o que nos permite realizar chamadas como movie.characters.

A migração Belongs

Agora nós estamos prontos para construir nossa migração e schema para Character. Um personagem pertence(belongs to) a um filme, então vamos definir uma migração que especifique o relacionamento.

Primeiro, precisamos gerar a migração:

mix ecto.gen.migration create_characters

Para declarar que um personagem pertence a um filme, precisamos da tabela characters e que ela possua uma coluna movie_id. Nós queremos que essa coluna funcione como uma chave estrangeira. Podemos alcançar isso com a seguinte linha, na chamada para create table/1:

add :movie_id, references(:movies)

Assim, nossa migração deve ser algo como:

# priv/migrations/*_create_characters.exs
defmodule Friends.Repo.Migrations.CreateCharacters do
  use Ecto.Migration

  def change do
    create table(:characters) do
      add :name, :string
      add :movie_id, references(:movies)
    end
  end
end

O Schema Belongs To

Nosso esquema precisa definir a relação belongs to entre um personagem e seu filme.

# lib/friends/character.ex

defmodule Friends.Character do
  use Ecto.Schema

  schema "characters" do
    field :name, :string
    belongs_to :movie, Friends.Movie
  end
end

Vamos dar uma olhada mais a fundo no que a macro belongs_to/3 faz por nós. Além de adicionar a coluna movie_id ao nosso esquema, ela também nos permite acessar os esquemas de movies associados através de characters. Ela utiliza a chave estrangeira para tornar o filme associado a um personagem disponível quando executamos a consulta sobre os personagens. Isso nos permite chamar character.movie.

Agora nós estamos prontos para executar as migrações:

mix ecto.migrate

Belong To/Has One

Digamos que um filme tenha um distribuidor. Por exemplo, o Netflix é o distribuidor do filme original “Bright”.

Vamos definir a migração e o esquema Distributor com o relacionamento “belongs to”. Primeiro, é preciso gerar a migração:

mix ecto.gen.migration create_distributors

Nós devemos adicionar uma chave estrangeira de movie_id à migração da tabela distributors que acabamos de gerar, bem como um índice único (unique) para garantir que um filme tenha apenas um distribuidor:

# priv/repo/migrations/*_create_distributors.exs

defmodule Friends.Repo.Migrations.CreateDistributors do
  use Ecto.Migration

  def change do
    create table(:distributors) do
      add :name, :string
      add :movie_id, references(:movies)
    end

    create unique_index(:distributors, [:movie_id])
  end
end

E o esquema Distributor deve usar a macro belongs_to/3 para nos permitir chamar distributor.movie e procurar o filme associado a um distribuidor usando esta chave estrangeira.

# lib/friends/distributor.ex

defmodule Friends.Distributor do
  use Ecto.Schema

  schema "distributors" do
    field :name, :string
    belongs_to :movie, Friends.Movie
  end
end

Em seguida, adicionaremos o relacionamento “has one” ao esquema Movie:

# lib/friends/movie.ex

defmodule Friends.Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :tagline, :string
    has_many :characters, Friends.Character
    has_one :distributor, Friends.Distributor # Eu sou novo!
  end
end

A macro has_one/3 funciona como a macro has_many/3. Ela usa a chave estrangeira do esquema para procurar e expor o distribuidor do filme. Isso nos permitirá chamar, por exemplo, movie.distributor.

Agora podemos executar nossas migrações:

mix ecto.migrate

Muitos para muitos(many to many)

Digamos que um filme tenha muitos atores e que um ator possa pertencer a mais de um filme. Vamos construir uma tabela de relação que faça referência a ambos filmes(movies) e atores(actors) para implementar esse relacionamento.

Primeiro, precisamos gerar a migração dos atores:

mix ecto.gen.migration create_actors

Defina a migração:

# priv/migrations/*_create_actors.ex

defmodule Friends.Repo.Migrations.CreateActors do
  use Ecto.Migration

  def change do
    create table(:actors) do
      add :name, :string
    end
  end
end

Vamos gerar nossa migração da tabela de relacionamento:

mix ecto.gen.migration create_movies_actors

Vamos definir nossa migração de forma que a tabela tenha duas chaves estrangeiras. Também adicionaremos um índice exclusivo para impor pares únicos de atores e filmes:

# priv/migrations/*_create_movies_actors.ex

defmodule Friends.Repo.Migrations.CreateMoviesActors do
  use Ecto.Migration

  def change do
    create table(:movies_actors, primary_key: false) do
      add :movie_id, references(:movies)
      add :actor_id, references(:actors)
    end

    create unique_index(:movies_actors, [:movie_id, :actor_id])
  end
end

Em seguida, vamos adicionar a macro many_to_many ao nosso esquema Movie:

# lib/friends/movie.ex

defmodule Friends.Movie do
  use Ecto.Schema

  schema "movies" do
    field :title, :string
    field :tagline, :string
    has_many :characters, Friends.Character
    has_one :distributor, Friends.Distributor
    many_to_many :actors, Friends.Actor, join_through: "movies_actors" # Eu sou novo!
  end
end

Finalmente, definiremos nosso esquema Actor com a mesma macro many_to_many.

# lib/friends/actor.ex

defmodule Friends.Actor do
  use Ecto.Schema

  schema "actors" do
    field :name, :string
    many_to_many :movies, Friends.Movie, join_through: "movies_actors"
  end
end

Estamos prontos para executar nossas migrações:

mix ecto.migrate

Salvando Dados Associados

A maneira como salvamos registros junto dos dados associados depende da natureza do relacionamento entre os registros. Vamos começar com o relacionamento “Belongs to/has many”.

Belongs To

Salvando com o Ecto.build_assoc/3

Com um relacionamento “belongs to”, podemos alavancar a função build_assoc/3 do Ecto.

build_assoc/3 aceita três argumentos:

Vamos salvar um filme e um personagem associado. Primeiro, vamos criar um registro de filme:

iex> alias Friends.{Movie, Character, Repo}
iex> movie = %Movie{title: "Ready Player One", tagline: "Something about video games"}

%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:built, "movies">,
  actors: %Ecto.Association.NotLoaded<association :actors is not loaded>,
  characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
  distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
  id: nil,
  tagline: "Something about video games",
  title: "Ready Player One"
}

iex> movie = Repo.insert!(movie)

Agora vamos construir nosso personagem associado e inseri-lo no banco de dados:

iex> character = Ecto.build_assoc(movie, :characters, %{name: "Wade Watts"})
%Friends.Character{
  __meta__: %Ecto.Schema.Metadata<:built, "characters">,
  id: nil,
  movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
  movie_id: 1,
  name: "Wade Watts"
}
iex> Repo.insert!(character)
%Friends.Character{
  __meta__: %Ecto.Schema.Metadata<:loaded, "characters">,
  id: 1,
  movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
  movie_id: 1,
  name: "Wade Watts"
}

Observe que, como a macro has_many/3 do esquema Movie especifica que um filme possui muitos :characters, o nome da associação que passamos como segundo argumento para build_assoc/3 é exatamente isso: :characters. Podemos ver que criamos um personagem que tem seu movie_id definido corretamente para o ID do filme associado.

Para usar build_assoc/3 com o intuito de salvar o distribuidor associado a um filme, adotamos a mesma abordagem de passar o nome do relacionamento do filme com o distribuidor como o segundo argumento parabuild_assoc/3:

iex> distributor = Ecto.build_assoc(movie, :distributor, %{name: "Netflix"})
%Friends.Distributor{
  __meta__: %Ecto.Schema.Metadata<:built, "distributors">,
  id: nil,
  movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
  movie_id: 1,
  name: "Netflix"
}
iex> Repo.insert!(distributor)
%Friends.Distributor{
  __meta__: %Ecto.Schema.Metadata<:loaded, "distributors">,
  id: 1,
  movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
  movie_id: 1,
  name: "Netflix"
}

Many to Many

Salvando com Ecto.Changeset.put_assoc/4

A abordagem build_assoc/3 não funcionará para o nosso relacionamento muitos-para-muitos(many-to-many). Isso ocorre porque nem as tabelas de filme nem de ator contêm uma chave estrangeira. Em vez disso, precisamos usar o Ecto Changesets e a função put_assoc/4.

Supondo que já tenhamos o registro do filme que criamos acima, vamos criar um registro de ator:

iex> alias Friends.Actor
iex> actor = %Actor{name: "Tyler Sheridan"}
%Friends.Actor{
  __meta__: %Ecto.Schema.Metadata<:built, "actors">,
  id: nil,
  movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
  name: "Tyler Sheridan"
}
iex> actor = Repo.insert!(actor)
%Friends.Actor{
  __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
  id: 1,
  movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
  name: "Tyler Sheridan"
}

Agora estamos prontos para associar nosso filme ao nosso ator por meio da tabela de relacionamento.

Primeiro, note que para trabalhar com Changesets, precisamos ter certeza de que nossa estrutura movie pré-carregou seus esquemas associados. Falaremos mais sobre pré-carregar dados a frente. Por enquanto, é suficiente entender que podemos pré-carregar nossas associações assim:

iex> movie = Repo.preload(movie, [:distributor, :characters, :actors])
 %Friends.Movie{
  __meta__: #Ecto.Schema.Metadata<:loaded, "movies">,
  actors: [],
  characters: [
    %Friends.Character{
      __meta__: #Ecto.Schema.Metadata<:loaded, "characters">,
      id: 1,
      movie: #Ecto.Association.NotLoaded<association :movie is not loaded>,
      movie_id: 1,
      name: "Wade Watts"
    }
  ],
  distributor: %Friends.Distributor{
    __meta__: #Ecto.Schema.Metadata<:loaded, "distributors">,
    id: 1,
    movie: #Ecto.Association.NotLoaded<association :movie is not loaded>,
    movie_id: 1,
    name: "Netflix"
  },
  id: 1,
  tagline: "Something about video game",
  title: "Ready Player One"
}

Em seguida, criaremos um conjunto de alterações para nosso registro de filme:

iex> movie_changeset = Ecto.Changeset.change(movie)
%Ecto.Changeset<action: nil, changes: %{}, errors: [], data: %Friends.Movie<>,
 valid?: true>

Agora vamos passar nosso changeset como o primeiro argumento para Ecto.Changeset.put_assoc/4:

iex> movie_actors_changeset = movie_changeset |> Ecto.Changeset.put_assoc(:actors, [actor])
%Ecto.Changeset<
  action: nil,
  changes: %{
    actors: [
       %Ecto.Changeset<action: :update, changes: %{}, errors: [],
       data: %Friends.Actor<>, valid?: true>
    ]
  },
  errors: [],
  data: %Friends.Movie<>,
  valid?: true
>

Isso nos dá um novo changeset, representando a seguinte mudança: adicione os atores nesta lista de atores ao registro de filme dado.

Por fim, atualizaremos os registros de filme e ator fornecidos usando nosso changeset mais recente:

iex> Repo.update!(movie_actors_changeset)
%Friends.Movie{
  __meta__: #Ecto.Schema.Metadata<:loaded, "movies">,
  actors: [
    %Friends.Actor{
      __meta__: #Ecto.Schema.Metadata<:loaded, "actors">,
      id: 1,
      movies: #Ecto.Association.NotLoaded<association :movies is not loaded>,
      name: "Tyler Sheridan"
    }
  ],
  characters: [
    %Friends.Character{
      __meta__: #Ecto.Schema.Metadata<:loaded, "characters">,
      id: 1,
      movie: #Ecto.Association.NotLoaded<association :movie is not loaded>,
      movie_id: 1,
      name: "Wade Watts"
    }
  ],
  distributor: %Friends.Distributor{
    __meta__: #Ecto.Schema.Metadata<:loaded, "distributors">,
    id: 1,
    movie: #Ecto.Association.NotLoaded<association :movie is not loaded>,
    movie_id: 1,
    name: "Netflix"
  },
  id: 1,
  tagline: "Something about video game",
  title: "Ready Player One"
}

Podemos ver que isso nos dá um registro de filme com o novo ator apropriadamente associado e já pré-carregado para nós em movie.actors.

Podemos usar essa mesma abordagem para criar um novo ator associado ao filme em questão. Em vez de passar uma estrutura de ator salva para put_assoc/4, simplesmente passamos uma struct de ator, descrevendo um novo ator que queremos criar:

iex> changeset = movie_changeset |> Ecto.Changeset.put_assoc(:actors, [%{name: "Gary"}])
%Ecto.Changeset<
  action: nil,
  changes: %{
    actors: [
      %Ecto.Changeset<
        action: :insert,
        changes: %{name: "Gary"},
        errors: [],
        data: %Friends.Actor<>,
        valid?: true
      >
    ]
  },
  errors: [],
  data: %Friends.Movie<>,
  valid?: true
>
iex>  Repo.update!(changeset)
%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
  actors: [
    %Friends.Actor{
      __meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
      id: 2,
      movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
      name: "Gary"
    }
  ],
  characters: [],
  distributor: nil,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

Podemos ver que um novo ator foi criado com um ID “2” e os atributos que atribuímos a ele.

Na próxima seção, aprenderemos a consultar nossos registros associados.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!