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

Asociaciones

En esta sección vamos a aprender como usar Ecto para definir y trabajar con asociaciones entre nuestros esquemas.

Configuración

Vamos a comenzar con la aplicación Ejemplo de la lección anterior. Puedes ir a la configuración aquí para recordarlo rápidamente.

Tipos de asociaciones

Hay tres tipos de asociaciones que podemos definir entre nuestros esquemas. Vamos a ver lo que son y como implementar cada tipo de relación.

Belongs To/Has Many (Pertenece a/Tiene muchos)

Vamos a agregar algunas entidades nuevas al dominio de nuestra app de ejemplo para que podamos catalogar nuestras películas favoritas. Vamos a empezar con dos esquemas: Movie (Película) y Character (Personaje). Vamos a implementar una relación “has many/belongs to” entre nuestros dos esquemas: Una película tiene muchos personajes, y un personaje pertenece a una película.

La Migración Has Many

Vamos a crear una migración para Movie:

mix ecto.gen.migration create_movies

Abre el nuevo archivo generado de migración y define tu función change para crear la tabla movies con algunos atributos nuevos:

# 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

El esquema Has Many

Vamos a agregar un esquema que especifica la relación “has many” entre una película y sus personajes.

# 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

El macro has_many/3 no agrega nada a la base de datos en sí. Lo que hace es usar la llave foranea en el esquema asociado, characters, para hacer que esten disponibles los personajes asociados a una película. Esto es lo que nos permite llamar movie.characters.

La migración Belongs To

Ahora estamos listos para construir nuestra migración y esquema de Character. Un personaje pertenece a una película, así que vamos a definir una migración y un esquema que especifique esta relación.

Primero, creamos la migración:

mix ecto.gen.migration create_characters

Para declarar que un personaje pertenece a una película, nececitamos que la tabla characters tenga una columna movie_id. Queremos que esta columna funcione como una llave foranea. Podemos lograr esto con la siguiente linea en nuestra función create table/1:

add :movie_id, references(:movies)

Entonces nuestra migración debe verse así:

# 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

El Esquema Belongs To

Nuestro esquema igualmente necesita definir la relación “belongs to” entre un personaje y su película.

# 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 a echar un vistazo más de cerca a lo que el macro belongs_to/3 hace por nosotros. Además de agregar la llave foranea movie_id a nuestro esquema, también nos da la habilidad de acceder al esquema asociado movies a tráves de characters. Éste usa la llave foranea para hacer disponible una película asociada a un personaje cuando los consultamos. Esto es lo que nos permite llamar character.movie

Ahora estamos listos para ejecutar nuestras migraciones:

mix ecto.migrate

Belongs To/Has One (Pertenece a/Tiene uno)

Digamos que una película tiene un distribuidor, por ejemplo, Netflix es el distribuidor de su película original “Bright”

Vamos a definir la migración del Distributor (Distribuidor) y su esquema con la relación belongs_to. Primero, vamos a generar la migración:

mix ecto.gen.migration create_distributors

Debemos agregar una llave foranea de movie_id a la migración de la tabla distributors que acabamos de crear así como un índice único para asegurarnos de que una película tiene únicamente un 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

Y el esquema Distributor debería usar el macro belongs_to/3 que nos permite llamar distributor.movie y buscar la película asociada al distribuidor usando esta llave foranea.

# lib/friends/distributor.ex

defmodule Friends.Distributor do
  use Ecto.Schema

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

Después, vamos a agregar la relación has_one a el 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 # I'm new!
  end
end

El macro has_one/3 funciona justo como el macro has_many/3. Usa la llave foranea del esquema asociado para buscar y exponer el distribuidor de la película. Esto nos permite llamar movie.distributor

Ya estamos listos para ejecutar nuestras migraciones:

mix ecto.migrate

Muchos a Muchos

Digamos que una película tiene muchos actores, y que un actor puede pertenecer a más de una película. Vamos a construir una tabla de asociación que referencía ambas películas y actores para implementar esta relación.

Primero, vamos a generar la migración Actors (Actores):

mix ecto.gen.migration create_actors

Define la migración:

# 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 a generar nuestra migración de la tabla de asociación:

mix ecto.gen.migration create_movies_actors

Vamos a definir nuestra migración de forma que la tabla tiene dos llaves foraneas. También vamos a añadir un índice único para asegurarnos de que existan pares únicos de actores y películas:

# priv/migrations/*_create_movies_actors.ex

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

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

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

Después, vamos a agregar el macro many_to_many a nuestro 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" # I'm new!
  end
end

Finalmente, definiremos nuestro esquema Actor con el mismo macro many_to_may.

# 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 listos para ejecutar nuestras migraciones:

mix ecto.migrate

Guardando Datos Asociados

La manera en la que guardamos registros junto con sus datos asociados, depende de la naturaleza de la relación entre los registros. Vamos a comenzar con la relación “Belongs to/has many”.

Belongs To

Guardando con Ecto.build_assoc/3

Con una relación “belongs to”, podemos aprovechar la función de Ecto build_assoc/3.

build_assoc/3 toma tres argumentos:

Vamos a guardar una película y un personaje asociado. Primero, vamos a crear un registro de una película:

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)

Ahora construiremos nuestro personaje asociado y lo insertaremos en la base de datos:

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"
}
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"
}

Nota que como el macro has_many/3 del esquema Movie especifica que una película tiene muchos :characters, el nombre de la asociación que pasamos como segundo argumento a build_assoc/3 es exactamente ese: :characters. Podemos ver que hemos creado un personaje que tiene su movie_id propiamente establecido como ID de la película asociada.

Para poder usar build_assoc/3 para guardar un distribuidor asociado a una película, tomamos el mismo enfoque de pasar el nombre de la relación de la película al distribuidor como el segundo argumento de build_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"
}

Muchos a Muchos

Guardando con Ecto.Changeset.put_assoc/4

La estrategia build_assoc/3 no funcionará para nuestra relación muchos-a-muchos. Esto es porque ni el actor ni la película contienen una llave foranea. En su lugar, necesitamos aprovechar los Changesets de Ecto y la función put_assoc/4

Asumiendo que ya tenemos el registro de la película que creamos arriba, vamos a crear un registro de actror:

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"
}

Ahora estamos listos para asociar nuestra película a nuestro actor a travez de la tabal de asociación.

Primero, ten en cuenta que para trabajar con changesets, necesitamos asegurarnos de que nuestra estructura movie tiene precargados datos asociados. Vamos a hablar más acerca de precargar los datos en un momento. Por ahora, es suficiente con entender que podemos precargar nuestra asociación así:

iex> movie = Repo.preload(movie, [:distributor, :characters, :actors])
%Friends.Movie{
  __meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
  actors: [],
  characters: [],
  distributor: nil,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

Después, vamos a crear un changeset para nuestro registro película:

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

Ahora vamos a pasar nuestro changeset como el primer argumento a 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
>

Esto nos da un nuevo changeset que representa el siguiente cambio: agregar los actores en la lista de actores a el registro de película proporcionado.

Por último, actualizaremos los registros de película y actor proporcionados usando nuestro último changeset:

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: "Bob"
    }
  ],
  characters: [],
  distributor: nil,
  id: 1,
  tagline: "Something about video games",
  title: "Ready Player One"
}

Ahora podemos ver que esto nos devuelve un registro de película con el nuevo actor propiamente asociado y ya precargado para nosotros en movie.actors.

Podemos usar la misma estrategia para crear un nuevo actor que está asociado con la película proporcionada. En lugar de pasar una estructura de actor guardada a put_assoc/4, nosotros simplemente pasamos una estructura de actor describiendo un nuevo actor que queremos crear:

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 un nuevo actor fue creado, con un ID “2” y los atributos que le asignamos.

En la siguiente sección, aprenderemos como consultar nuestros registros asociados.

¿Encontraste un error o quieres contribuir a la lección? ¡Edita esta lección en GitHub!