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:
- La estructura del registro que queremos guardar.
- El nombre de la asociación.
- Cualquier atributo que queremos asignar a el registro asociado que estamos guardando.
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!