1 ¿Por qué asociaciones?
En Rails, una asociación es una conexión entre dos modelos de Active Record. ¿Por qué necesitamos asociaciones entre modelos? Porque hacen que las operaciones comunes sean más simples y fáciles en tu código.
Por ejemplo, considera una aplicación Rails simple que incluye un modelo para autores y un modelo para libros. Cada autor puede tener muchos libros.
Sin asociaciones, las declaraciones de los modelos se verían así:
class Author < ApplicationRecord
end
class Book < ApplicationRecord
end
Ahora, supongamos que queremos agregar un nuevo libro para un autor existente. Necesitaríamos hacer algo como esto:
@book = Book.create(published_at: Time.now, author_id: @author.id)
O considera eliminar un autor y asegurarte de que todos sus libros también se eliminen:
@books = Book.where(author_id: @author.id)
@books.each do |book|
book.destroy
end
@author.destroy
Con las asociaciones de Active Record, podemos simplificar estas - y otras - operaciones diciéndole declarativamente a Rails que hay una conexión entre los dos modelos. Aquí está el código revisado para configurar autores y libros:
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
class Book < ApplicationRecord
belongs_to :author
end
Con este cambio, crear un nuevo libro para un autor en particular es más fácil:
@book = @author.books.create(published_at: Time.now)
Eliminar un autor y todos sus libros es mucho más fácil:
@author.destroy
Para obtener más información sobre los diferentes tipos de asociaciones, lee la siguiente sección de esta guía. A continuación, se presentan algunos consejos y trucos para trabajar con asociaciones, y luego una referencia completa a los métodos y opciones para asociaciones en Rails.
2 Los tipos de asociaciones
Rails admite seis tipos de asociaciones, cada una con un caso de uso particular en mente.
Aquí hay una lista de todos los tipos admitidos con un enlace a su documentación de API para obtener información más detallada sobre cómo usarlos, sus parámetros de método, etc.
Las asociaciones se implementan utilizando llamadas de estilo macro, para que puedas agregar características declarativamente a tus modelos. Por ejemplo, al declarar que un modelo belongs_to
a otro, le indicas a Rails que mantenga la información de Primary Key-Foreign Key entre las instancias de los dos modelos, y también obtienes una serie de métodos de utilidad agregados a tu modelo.
En el resto de esta guía, aprenderás cómo declarar y usar las diferentes formas de asociaciones. Pero primero, una breve introducción a las situaciones en las que cada tipo de asociación es apropiado.
2.1 La asociación belongs_to
Una asociación belongs_to
establece una conexión con otro modelo, de modo que cada instancia del modelo declarante "pertenece a" una instancia del otro modelo. Por ejemplo, si tu aplicación incluye autores y libros, y cada libro puede ser asignado a exactamente un autor, declararías el modelo de libro de esta manera:
class Book < ApplicationRecord
belongs_to :author
end
NOTA: Las asociaciones belongs_to
deben usar el término en singular. Si usaste la forma en plural en el ejemplo anterior para la asociación author
en el modelo Book
e intentaste crear la instancia mediante Book.create(authors: author)
, se te informaría que hay una "constante no inicializada Book::Authors". Esto se debe a que Rails infiere automáticamente el nombre de la clase a partir del nombre de la asociación. Si el nombre de la asociación está incorrectamente en plural, entonces la clase inferida también estará incorrectamente en plural.
La migración correspondiente podría verse así:
class CreateBooks < ActiveRecord::Migration[7.1]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
end
end
Cuando se usa solo, belongs_to
produce una conexión unidireccional uno a uno. Por lo tanto, cada libro en el ejemplo anterior "conoce" a su autor, pero los autores no conocen sus libros.
Para configurar una asociación bidireccional, usa belongs_to
en combinación con un has_one
o has_many
en el otro modelo, en este caso el modelo Author.
belongs_to
no garantiza la consistencia de referencia si optional
se establece en true, por lo que dependiendo del caso de uso, es posible que también necesites agregar una restricción de clave externa a nivel de base de datos en la columna de referencia, como esta:
ruby
create_table :books do |t|
t.belongs_to :author, foreign_key: true
# ...
end
2.2 La asociación has_one
Una asociación has_one
indica que otro modelo tiene una referencia a este modelo. Ese modelo se puede obtener a través de esta asociación.
Por ejemplo, si cada proveedor en tu aplicación tiene solo una cuenta, declararías el modelo de proveedor de la siguiente manera:
class Supplier < ApplicationRecord
has_one :account
end
La diferencia principal con belongs_to
es que la columna de enlace supplier_id
se encuentra en la otra tabla:
La migración correspondiente podría verse así:
class CreateSuppliers < ActiveRecord::Migration[7.1]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
end
end
Dependiendo del caso de uso, es posible que también necesites crear un índice único y/o una restricción de clave externa en la columna de proveedor para la tabla de cuentas. En este caso, la definición de la columna podría verse así:
create_table :accounts do |t|
t.belongs_to :supplier, index: { unique: true }, foreign_key: true
# ...
end
Esta relación puede ser bidireccional cuando se utiliza en combinación con belongs_to
en el otro modelo.
2.3 La asociación has_many
Una asociación has_many
es similar a has_one
, pero indica una conexión de uno a muchos con otro modelo. A menudo encontrarás esta asociación en el "lado contrario" de una asociación belongs_to
. Esta asociación indica que cada instancia del modelo tiene cero o más instancias de otro modelo. Por ejemplo, en una aplicación que contiene autores y libros, el modelo de autor se podría declarar de la siguiente manera:
class Author < ApplicationRecord
has_many :books
end
NOTA: El nombre del otro modelo se pluraliza al declarar una asociación has_many
.
La migración correspondiente podría verse así:
class CreateAuthors < ActiveRecord::Migration[7.1]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
end
end
Dependiendo del caso de uso, generalmente es una buena idea crear un índice no único y opcionalmente una restricción de clave externa en la columna de autor para la tabla de libros:
create_table :books do |t|
t.belongs_to :author, index: true, foreign_key: true
# ...
end
2.4 La asociación has_many :through
Una asociación has_many :through
se utiliza a menudo para establecer una conexión de muchos a muchos con otro modelo. Esta asociación indica que el modelo declarante se puede relacionar con cero o más instancias de otro modelo al pasar a través de un tercer modelo. Por ejemplo, considera una práctica médica donde los pacientes hacen citas para ver a los médicos. Las declaraciones de asociación relevantes podrían verse así:
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
La migración correspondiente podría verse así:
class CreateAppointments < ActiveRecord::Migration[7.1]
def change
create_table :physicians do |t|
t.string :name
t.timestamps
end
create_table :patients do |t|
t.string :name
t.timestamps
end
create_table :appointments do |t|
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
end
end
La colección de modelos de unión se puede gestionar a través de los métodos de asociación has_many
.
Por ejemplo, si asignas:
physician.patients = patients
Se crearán automáticamente nuevos modelos de unión para los objetos recién asociados. Si algunos que existían previamente ahora faltan, entonces sus filas de unión se eliminan automáticamente.
ADVERTENCIA: La eliminación automática de modelos de unión es directa, no se activan los callbacks de eliminación.
La asociación has_many :through
también es útil para configurar "atajos" a través de asociaciones anidadas has_many
. Por ejemplo, si un documento tiene muchas secciones y una sección tiene muchos párrafos, a veces es posible que desees obtener una colección simple de todos los párrafos en el documento. Puedes configurarlo de la siguiente manera:
class Document < ApplicationRecord
has_many :sections
has_many :paragraphs, through: :sections
end
class Section < ApplicationRecord
belongs_to :document
has_many :paragraphs
end
class Paragraph < ApplicationRecord
belongs_to :section
end
Con through: :sections
especificado, Rails ahora entenderá:
@document.paragraphs
2.5 La asociación has_one :through
Una asociación has_one :through
establece una conexión de uno a uno con otro modelo. Esta asociación indica que el modelo declarante se puede relacionar con una instancia de otro modelo al pasar a través de un tercer modelo.
Por ejemplo, si cada proveedor tiene una cuenta y cada cuenta está asociada con un historial de cuenta, entonces el modelo de proveedor podría verse así:
```ruby
class Supplier < ApplicationRecord
has_one :account
has_one :account_history, through: :account
end
class Account < ApplicationRecord belongs_to :supplier has_one :account_history end
class AccountHistory < ApplicationRecord belongs_to :account end ```
La migración correspondiente podría verse así:
class CreateAccountHistories < ActiveRecord::Migration[7.1]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
create_table :account_histories do |t|
t.belongs_to :account
t.integer :credit_rating
t.timestamps
end
end
end
2.6 La asociación has_and_belongs_to_many
Una asociación has_and_belongs_to_many
crea una conexión directa de muchos a muchos con otro modelo, sin un modelo intermedio.
Esta asociación indica que cada instancia del modelo declarante se refiere a cero o más instancias de otro modelo.
Por ejemplo, si tu aplicación incluye ensamblajes y piezas, con cada ensamblaje teniendo muchas piezas y cada pieza apareciendo en muchos ensamblajes, podrías declarar los modelos de esta manera:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
La migración correspondiente podría verse así:
class CreateAssembliesAndParts < ActiveRecord::Migration[7.1]
def change
create_table :assemblies do |t|
t.string :name
t.timestamps
end
create_table :parts do |t|
t.string :part_number
t.timestamps
end
create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly
t.belongs_to :part
end
end
end
2.7 Elegir entre belongs_to
y has_one
Si deseas establecer una relación uno a uno entre dos modelos, deberás agregar belongs_to
a uno y has_one
al otro. ¿Cómo sabes cuál es cuál?
La distinción radica en dónde colocas la clave externa (se coloca en la tabla para la clase que declara la asociación belongs_to
), pero también debes pensar en el significado real de los datos. La relación has_one
indica que uno de algo es tuyo, es decir, que algo apunta hacia ti. Por ejemplo, tiene más sentido decir que un proveedor es dueño de una cuenta que decir que una cuenta es dueña de un proveedor. Esto sugiere que las relaciones correctas son las siguientes:
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
La migración correspondiente podría verse así:
class CreateSuppliers < ActiveRecord::Migration[7.1]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.bigint :supplier_id
t.string :account_number
t.timestamps
end
add_index :accounts, :supplier_id
end
end
NOTA: El uso de t.bigint :supplier_id
hace que el nombre de la clave externa sea obvio y explícito. En las versiones actuales de Rails, puedes abstraer este detalle de implementación utilizando t.references :supplier
en su lugar.
2.8 Elegir entre has_many :through
y has_and_belongs_to_many
Rails ofrece dos formas diferentes de declarar una relación de muchos a muchos entre modelos. La primera forma es utilizar has_and_belongs_to_many
, que te permite hacer la asociación directamente:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
La segunda forma de declarar una relación de muchos a muchos es utilizar has_many :through
. Esto hace la asociación indirectamente, a través de un modelo de unión:
class Assembly < ApplicationRecord
has_many :manifests
has_many :parts, through: :manifests
end
class Manifest < ApplicationRecord
belongs_to :assembly
belongs_to :part
end
class Part < ApplicationRecord
has_many :manifests
has_many :assemblies, through: :manifests
end
La regla más simple es que debes configurar una relación has_many :through
si necesitas trabajar con el modelo de relación como una entidad independiente. Si no necesitas hacer nada con el modelo de relación, puede ser más sencillo configurar una relación has_and_belongs_to_many
(aunque deberás recordar crear la tabla de unión en la base de datos).
Debes usar has_many :through
si necesitas validaciones, callbacks o atributos adicionales en el modelo de unión.
2.9 Asociaciones polimórficas
Una variante ligeramente más avanzada de las asociaciones es la asociación polimórfica. Con las asociaciones polimórficas, un modelo puede pertenecer a más de un modelo, en una sola asociación. Por ejemplo, podrías tener un modelo de imagen que pertenece tanto a un modelo de empleado como a un modelo de producto. Así es como se podría declarar esto:
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
Puedes pensar en una declaración polimórfica belongs_to
como configurar una interfaz que cualquier otro modelo puede usar. Desde una instancia del modelo Employee
, puedes recuperar una colección de imágenes: @employee.pictures
.
De manera similar, puedes recuperar @product.pictures
.
Si tienes una instancia del modelo Picture
, puedes acceder a su padre a través de @picture.imageable
. Para que esto funcione, debes declarar tanto una columna de clave externa como una columna de tipo en el modelo que declara la interfaz polimórfica:
class CreatePictures < ActiveRecord::Migration[7.1]
def change
create_table :pictures do |t|
t.string :name
t.bigint :imageable_id
t.string :imageable_type
t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
end
end
Esta migración se puede simplificar utilizando la forma t.references
:
class CreatePictures < ActiveRecord::Migration[7.1]
def change
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true
t.timestamps
end
end
end
2.10 Autoasociaciones
Al diseñar un modelo de datos, a veces encontrarás un modelo que debería tener una relación consigo mismo. Por ejemplo, es posible que desees almacenar a todos los empleados en un solo modelo de base de datos, pero poder rastrear relaciones como la de gerente y subordinados. Esta situación se puede modelar con asociaciones de autoasociación:
class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee",
foreign_key: "manager_id"
belongs_to :manager, class_name: "Employee", optional: true
end
Con esta configuración, puedes recuperar @employee.subordinates
y @employee.manager
.
En tus migraciones/esquema, agregarás una columna de referencia al propio modelo.
class CreateEmployees < ActiveRecord::Migration[7.1]
def change
create_table :employees do |t|
t.references :manager, foreign_key: { to_table: :employees }
t.timestamps
end
end
end
NOTA: La opción to_table
pasada a foreign_key
y más se explican en SchemaStatements#add_reference
.
3 Consejos, trucos y advertencias
Aquí hay algunas cosas que debes saber para utilizar eficientemente las asociaciones de Active Record en tus aplicaciones de Rails:
- Controlar el almacenamiento en caché
- Evitar colisiones de nombres
- Actualizar el esquema
- Controlar el alcance de la asociación
- Asociaciones bidireccionales
3.1 Controlar el almacenamiento en caché
Todos los métodos de asociación se basan en el almacenamiento en caché, que mantiene el resultado de la consulta más reciente disponible para otras operaciones. La caché incluso se comparte entre métodos. Por ejemplo:
# recupera los libros de la base de datos
author.books.load
# utiliza la copia en caché de los libros
author.books.size
# utiliza la copia en caché de los libros
author.books.empty?
Pero, ¿qué sucede si quieres volver a cargar la caché porque los datos podrían haber cambiado en otra parte de la aplicación? Simplemente llama a reload
en la asociación:
# recupera los libros de la base de datos
author.books.load
# utiliza la copia en caché de los libros
author.books.size
# descarta la copia en caché de los libros y vuelve a la base de datos
author.books.reload.empty?
3.2 Evitar colisiones de nombres
No eres libre de usar cualquier nombre para tus asociaciones. Debido a que crear una asociación agrega un método con ese nombre al modelo, es una mala idea darle a una asociación un nombre que ya se utiliza para un método de instancia de ActiveRecord::Base
. El método de asociación anularía el método base y rompería las cosas. Por ejemplo, attributes
o connection
son malos nombres para asociaciones.
3.3 Actualizar el esquema
Las asociaciones son extremadamente útiles, pero no son mágicas. Eres responsable de mantener tu esquema de base de datos para que coincida con tus asociaciones. En la práctica, esto significa dos cosas, dependiendo del tipo de asociaciones que estés creando. Para las asociaciones belongs_to
, debes crear claves externas, y para las asociaciones has_and_belongs_to_many
, debes crear la tabla de unión correspondiente.
3.3.1 Crear claves externas para las asociaciones belongs_to
Cuando declaras una asociación belongs_to
, debes crear claves externas según corresponda. Por ejemplo, considera este modelo:
class Book < ApplicationRecord
belongs_to :author
end
Esta declaración debe respaldarse con una columna de clave externa correspondiente en la tabla de libros. Para una tabla completamente nueva, la migración podría verse así:
class CreateBooks < ActiveRecord::Migration[7.1]
def change
create_table :books do |t|
t.datetime :published_at
t.string :book_number
t.references :author
end
end
end
Mientras que para una tabla existente, podría verse así:
class AddAuthorToBooks < ActiveRecord::Migration[7.1]
def change
add_reference :books, :author
end
end
NOTA: Si deseas imponer la integridad referencial a nivel de base de datos, agrega la opción foreign_key: true
a las declaraciones de columna 'reference' anteriores.
3.3.2 Crear tablas de unión para las asociaciones has_and_belongs_to_many
Si creas una asociación has_and_belongs_to_many
, debes crear explícitamente la tabla de unión. A menos que el nombre de la tabla de unión se especifique explícitamente utilizando la opción :join_table
, Active Record crea el nombre utilizando el orden léxico de los nombres de clase. Por lo tanto, una unión entre los modelos de autor y libro dará el nombre de tabla de unión predeterminado "authors_books" porque "a" tiene más prioridad que "b" en el orden léxico.
ADVERTENCIA: La precedencia entre los nombres de los modelos se calcula utilizando el operador <=>
para String
. Esto significa que si las cadenas tienen longitudes diferentes y las cadenas son iguales cuando se comparan hasta la longitud más corta, entonces la cadena más larga se considera de mayor precedencia léxica que la más corta. Por ejemplo, se esperaría que las tablas "paper_boxes" y "papers" generen un nombre de tabla de unión "papers_paper_boxes" debido a la longitud del nombre "paper_boxes", pero de hecho genera un nombre de tabla de unión "paper_boxes_papers" (porque el guión bajo '_' es lexicográficamente menor que 's' en las codificaciones comunes).
Sea cual sea el nombre, debes generar manualmente la tabla de unión con una migración adecuada. Por ejemplo, considera estas asociaciones:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
Estas asociaciones deben ser respaldadas por una migración para crear la tabla assemblies_parts
. Esta tabla debe crearse sin una clave primaria:
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[7.1]
def change
create_table :assemblies_parts, id: false do |t|
t.bigint :assembly_id
t.bigint :part_id
end
add_index :assemblies_parts, :assembly_id
add_index :assemblies_parts, :part_id
end
end
Pasamos id: false
a create_table
porque esa tabla no representa un modelo. Eso es necesario para que la asociación funcione correctamente. Si observas algún comportamiento extraño en una asociación has_and_belongs_to_many
, como IDs de modelo alterados o excepciones sobre IDs conflictivos, es probable que hayas olvidado ese detalle.
Para mayor simplicidad, también puedes usar el método create_join_table
:
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[7.1]
def change
create_join_table :assemblies, :parts do |t|
t.index :assembly_id
t.index :part_id
end
end
end
3.4 Controlando el alcance de la asociación
Por defecto, las asociaciones buscan objetos solo dentro del alcance del módulo actual. Esto puede ser importante cuando declaras modelos de Active Record dentro de un módulo. Por ejemplo:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
Esto funcionará bien, porque tanto la clase Supplier
como la clase Account
están definidas dentro del mismo alcance. Pero lo siguiente no funcionará, porque Supplier
y Account
están definidos en alcances diferentes:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
end
module Billing
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
Para asociar un modelo con un modelo en un espacio de nombres diferente, debes especificar el nombre completo de la clase en tu declaración de asociación:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account,
class_name: "MyApplication::Billing::Account"
end
end
module Billing
class Account < ApplicationRecord
belongs_to :supplier,
class_name: "MyApplication::Business::Supplier"
end
end
end
3.5 Asociaciones bidireccionales
Es normal que las asociaciones funcionen en dos direcciones, requiriendo la declaración en dos modelos diferentes:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
Active Record intentará identificar automáticamente que estos dos modelos comparten una asociación bidireccional basada en el nombre de la asociación. Esta información permite a Active Record:
Evitar consultas innecesarias para datos que ya están cargados:
irb> author = Author.first irb> author.books.all? do |book| irb> book.author.equal?(author) # No se ejecutan consultas adicionales aquí irb> end => true
Evitar datos inconsistentes (ya que solo hay una copia del objeto
Author
cargado):irb> author = Author.first irb> book = author.books.first irb> author.name == book.author.name => true irb> author.name = "Nombre cambiado" irb> author.name == book.author.name => true
Guardar automáticamente las asociaciones en más casos:
irb> author = Author.new irb> book = author.books.new irb> book.save! irb> book.persisted? => true irb> author.persisted? => true
Validar la presencia y ausencia de asociaciones en más casos:
irb> book = Book.new irb> book.valid? => false irb> book.errors.full_messages => ["Author must exist"] irb> author = Author.new irb> book = author.books.new irb> book.valid? => true
Active Record admite la identificación automática de la mayoría de las asociaciones con nombres estándar. Sin embargo, las asociaciones bidireccionales que contienen las opciones :through
o :foreign_key
no se identificarán automáticamente.
Los ámbitos personalizados en la asociación opuesta también impiden la identificación automática, al igual que los ámbitos personalizados en la propia asociación a menos que se establezca config.active_record.automatic_scope_inversing
en true (el valor predeterminado para nuevas aplicaciones).
Por ejemplo, considera las siguientes declaraciones de modelos:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
Debido a la opción :foreign_key
, Active Record ya no reconocerá automáticamente la asociación bidireccional. Esto puede causar que tu aplicación:
* Ejecutar consultas innecesarias para los mismos datos (en este ejemplo, causando consultas N+1):
```irb
irb> author = Author.first
irb> author.books.any? do |book|
irb> book.author.equal?(author) # Esto ejecuta una consulta de autor por cada libro
irb> end
=> false
```
Hacer referencia a múltiples copias de un modelo con datos inconsistentes:
irb> author = Author.first irb> book = author.books.first irb> author.name == book.author.name => true irb> author.name = "Nombre cambiado" irb> author.name == book.author.name => false
Fallar al guardar automáticamente las asociaciones:
irb> author = Author.new irb> book = author.books.new irb> book.save! irb> book.persisted? => true irb> author.persisted? => false
Fallar al validar la presencia o ausencia:
irb> author = Author.new irb> book = author.books.new irb> book.valid? => false irb> book.errors.full_messages => ["Author must exist"]
Active Record proporciona la opción :inverse_of
para declarar explícitamente asociaciones bidireccionales:
class Author < ApplicationRecord
has_many :books, inverse_of: 'writer'
end
class Book < ApplicationRecord
belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
Al incluir la opción :inverse_of
en la declaración de la asociación has_many
,
Active Record reconocerá la asociación bidireccional y se comportará como en
los ejemplos iniciales mencionados.
4 Referencia detallada de asociaciones
Las siguientes secciones brindan detalles de cada tipo de asociación, incluyendo los métodos que agregan y las opciones que se pueden utilizar al declarar una asociación.
4.1 Referencia de la asociación belongs_to
En términos de base de datos, la asociación belongs_to
indica que la tabla de este modelo contiene una columna que representa una referencia a otra tabla.
Esto se puede utilizar para establecer relaciones uno a uno o uno a muchos, dependiendo de la configuración.
Si la tabla de la otra clase contiene la referencia en una relación uno a uno, entonces se debe usar has_one
en su lugar.
4.1.1 Métodos agregados por belongs_to
Cuando se declara una asociación belongs_to
, la clase que la declara automáticamente obtiene 8 métodos relacionados con la asociación:
association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
association_changed?
association_previously_changed?
En todos estos métodos, association
se reemplaza con el símbolo pasado como primer argumento a belongs_to
. Por ejemplo, dado la declaración:
class Book < ApplicationRecord
belongs_to :author
end
Cada instancia del modelo Book
tendrá estos métodos:
author
author=
build_author
create_author
create_author!
reload_author
reset_author
author_changed?
author_previously_changed?
NOTA: Al inicializar una nueva asociación has_one
o belongs_to
, se debe utilizar el prefijo build_
para construir la asociación, en lugar del método association.build
que se utilizaría para las asociaciones has_many
o has_and_belongs_to_many
. Para crear uno, se utiliza el prefijo create_
.
4.1.1.1 association
El método association
devuelve el objeto asociado, si existe. Si no se encuentra ningún objeto asociado, devuelve nil
.
@author = @book.author
Si el objeto asociado ya ha sido recuperado de la base de datos para este objeto, se devolverá la versión en caché. Para anular este comportamiento (y forzar una lectura de la base de datos), se llama a #reload_association
en el objeto padre.
@author = @book.reload_author
Para descargar la versión en caché del objeto asociado, lo que provoca que el próximo acceso, si lo hay, lo consulte desde la base de datos, se llama a #reset_association
en el objeto padre.
@book.reset_author
4.1.1.2 association=(associate)
El método association=
asigna un objeto asociado a este objeto. En segundo plano, esto significa extraer la clave primaria del objeto asociado y establecer la clave externa de este objeto con el mismo valor.
@book.author = @author
4.1.1.3 build_association(attributes = {})
El método build_association
devuelve un nuevo objeto del tipo asociado. Este objeto se instanciará a partir de los atributos pasados, y el enlace a través de la clave externa de este objeto se establecerá, pero el objeto asociado no se guardará todavía.
@author = @book.build_author(author_number: 123,
author_name: "John Doe")
4.1.1.4 create_association(attributes = {})
El método create_association
devuelve un nuevo objeto del tipo asociado. Este objeto se instanciará a partir de los atributos pasados, se establecerá el enlace a través de la clave externa de este objeto y, una vez que pase todas las validaciones especificadas en el modelo asociado, se guardará el objeto asociado.
@author = @book.create_author(author_number: 123,
author_name: "John Doe")
4.1.1.5 create_association!(attributes = {})
Hace lo mismo que create_association
anteriormente, pero lanza ActiveRecord::RecordInvalid
si el registro no es válido.
4.1.1.6 association_changed?
El método association_changed?
devuelve true si se ha asignado un nuevo objeto asociado y la clave externa se actualizará en el próximo guardado.
```ruby
@book.author # => #
@book.author = Author.second # => #
@book.save! @book.author_changed? # => false ```
4.1.1.7 association_previously_changed?
El método association_previously_changed?
devuelve true si la última vez que se guardó se actualizó la asociación para hacer referencia a un nuevo objeto asociado.
@book.author # => #<Author author_number: 123, author_name: "John Doe">
@book.author_previously_changed? # => false
@book.author = Author.second # => #<Author author_number: 456, author_name: "Jane Smith">
@book.save!
@book.author_previously_changed? # => true
4.1.2 Opciones para belongs_to
Aunque Rails utiliza valores predeterminados inteligentes que funcionarán bien en la mayoría de las situaciones, puede haber momentos en los que desee personalizar el comportamiento de la referencia de la asociación belongs_to
. Estas personalizaciones se pueden realizar fácilmente pasando opciones y bloques de alcance al crear la asociación. Por ejemplo, esta asociación utiliza dos opciones:
class Book < ApplicationRecord
belongs_to :author, touch: :books_updated_at,
counter_cache: true
end
La asociación belongs_to
admite estas opciones:
:autosave
:class_name
:counter_cache
:dependent
:foreign_key
:primary_key
:inverse_of
:polymorphic
:touch
:validate
:optional
4.1.2.1 :autosave
Si establece la opción :autosave
en true
, Rails guardará cualquier miembro de la asociación cargado y destruirá los miembros que estén marcados para su destrucción cada vez que guarde el objeto principal. Establecer :autosave
en false
no es lo mismo que no establecer la opción :autosave
. Si la opción :autosave
no está presente, entonces se guardarán los nuevos objetos asociados, pero no se guardarán los objetos asociados actualizados.
4.1.2.2 :class_name
Si el nombre del otro modelo no se puede derivar del nombre de la asociación, puede utilizar la opción :class_name
para proporcionar el nombre del modelo. Por ejemplo, si un libro pertenece a un autor, pero el nombre real del modelo que contiene a los autores es Patron
, se configuraría de la siguiente manera:
class Book < ApplicationRecord
belongs_to :author, class_name: "Patron"
end
4.1.2.3 :counter_cache
La opción :counter_cache
se puede utilizar para hacer más eficiente la búsqueda del número de objetos asociados. Considere estos modelos:
class Book < ApplicationRecord
belongs_to :author
end
class Author < ApplicationRecord
has_many :books
end
Con estas declaraciones, solicitar el valor de @author.books.size
requiere hacer una llamada a la base de datos para realizar una consulta COUNT(*)
. Para evitar esta llamada, puede agregar una caché de contador al modelo perteneciente:
class Book < ApplicationRecord
belongs_to :author, counter_cache: true
end
class Author < ApplicationRecord
has_many :books
end
Con esta declaración, Rails mantendrá actualizado el valor de la caché y luego devolverá ese valor en respuesta al método size
.
Aunque la opción :counter_cache
se especifica en el modelo que incluye la declaración belongs_to
, la columna real debe agregarse al modelo asociado (has_many
). En el caso anterior, debería agregar una columna llamada books_count
al modelo Author
.
Puede anular el nombre de columna predeterminado especificando un nombre de columna personalizado en la declaración counter_cache
en lugar de true
. Por ejemplo, para usar count_of_books
en lugar de books_count
:
class Book < ApplicationRecord
belongs_to :author, counter_cache: :count_of_books
end
class Author < ApplicationRecord
has_many :books
end
NOTA: Solo necesita especificar la opción :counter_cache
en el lado belongs_to
de la asociación.
Las columnas de caché de contador se agregan a la lista de atributos de solo lectura del modelo propietario a través de attr_readonly
.
Si por alguna razón cambia el valor de la clave principal de un modelo propietario y no actualiza también las claves externas de los modelos contados, entonces la caché de contador puede tener datos obsoletos. En otras palabras, cualquier modelo huérfano seguirá contando hacia el contador. Para solucionar una caché de contador obsoleta, use reset_counters
.
4.1.2.4 :dependent
Si establece la opción :dependent
en:
:destroy
, cuando se destruye el objeto, se llamará adestroy
en sus objetos asociados.:delete
, cuando se destruye el objeto, todos sus objetos asociados se eliminarán directamente de la base de datos sin llamar a su métododestroy
.:destroy_async
: cuando se destruye el objeto, se encola un trabajoActiveRecord::DestroyAssociationAsyncJob
que llamará adestroy
en sus objetos asociados. Active Job debe estar configurado para que esto funcione. No utilice esta opción si la asociación está respaldada por restricciones de clave externa en su base de datos. Las acciones de restricción de clave externa ocurrirán dentro de la misma transacción que elimina su propietario. ADVERTENCIA: No debe especificar esta opción en una asociaciónbelongs_to
que esté conectada con una asociaciónhas_many
en la otra clase. Hacerlo puede llevar a registros huérfanos en su base de datos.
4.1.2.5 :foreign_key
Por convención, Rails asume que la columna utilizada para almacenar la clave externa en este modelo es el nombre de la asociación con el sufijo _id
agregado. La opción :foreign_key
te permite establecer directamente el nombre de la clave externa:
class Book < ApplicationRecord
belongs_to :author, class_name: "Patron",
foreign_key: "patron_id"
end
CONSEJO: En cualquier caso, Rails no creará columnas de clave externa por ti. Necesitas definirlas explícitamente como parte de tus migraciones.
4.1.2.6 :primary_key
Por convención, Rails asume que la columna id
se utiliza para almacenar la clave primaria de sus tablas. La opción :primary_key
te permite especificar una columna diferente.
Por ejemplo, supongamos que tenemos una tabla users
con guid
como clave primaria. Si queremos una tabla separada todos
para almacenar la clave externa user_id
en la columna guid
, podemos usar primary_key
para lograr esto de la siguiente manera:
class User < ApplicationRecord
self.primary_key = 'guid' # la clave primaria es guid y no id
end
class Todo < ApplicationRecord
belongs_to :user, primary_key: 'guid'
end
Cuando ejecutamos @user.todos.create
, el registro @todo
tendrá su valor user_id
como el valor guid
de @user
.
4.1.2.7 :inverse_of
La opción :inverse_of
especifica el nombre de la asociación has_many
o has_one
que es la inversa de esta asociación. Consulta la sección de asociaciones bidireccionales para obtener más detalles.
class Author < ApplicationRecord
has_many :books, inverse_of: :author
end
class Book < ApplicationRecord
belongs_to :author, inverse_of: :books
end
4.1.2.8 :polymorphic
Pasando true
a la opción :polymorphic
indica que esta es una asociación polimórfica. Las asociaciones polimórficas se discutieron en detalle anteriormente en esta guía.
4.1.2.9 :touch
Si estableces la opción :touch
en true
, entonces la marca de tiempo updated_at
o updated_on
en el objeto asociado se establecerá en la hora actual cada vez que se guarde o se elimine este objeto:
class Book < ApplicationRecord
belongs_to :author, touch: true
end
class Author < ApplicationRecord
has_many :books
end
En este caso, guardar o eliminar un libro actualizará la marca de tiempo en el autor asociado. También puedes especificar un atributo de marca de tiempo particular para actualizar:
class Book < ApplicationRecord
belongs_to :author, touch: :books_updated_at
end
4.1.2.10 :validate
Si estableces la opción :validate
en true
, entonces los nuevos objetos asociados se validarán cada vez que guardes este objeto. Por defecto, esto es false
: los nuevos objetos asociados no se validarán cuando se guarde este objeto.
4.1.2.11 :optional
Si estableces la opción :optional
en true
, entonces no se validará la presencia del objeto asociado. Por defecto, esta opción está establecida en false
.
4.1.3 Alcances para belongs_to
Puede haber momentos en los que desees personalizar la consulta utilizada por belongs_to
. Estas personalizaciones se pueden lograr a través de un bloque de alcance. Por ejemplo:
class Book < ApplicationRecord
belongs_to :author, -> { where active: true }
end
Puedes utilizar cualquiera de los métodos de consulta estándar mencionados aquí dentro del bloque de alcance. Los siguientes se discuten a continuación:
where
includes
readonly
select
4.1.3.1 where
El método where
te permite especificar las condiciones que el objeto asociado debe cumplir.
class Book < ApplicationRecord
belongs_to :author, -> { where active: true }
end
4.1.3.2 includes
Puedes utilizar el método includes
para especificar asociaciones de segundo orden que deben cargarse de forma ansiosa cuando se utiliza esta asociación. Por ejemplo, considera estos modelos:
class Chapter < ApplicationRecord
belongs_to :book
end
class Book < ApplicationRecord
belongs_to :author
has_many :chapters
end
class Author < ApplicationRecord
has_many :books
end
Si frecuentemente recuperas autores directamente desde capítulos (@chapter.book.author
), puedes hacer tu código algo más eficiente incluyendo autores en la asociación de capítulos a libros:
class Chapter < ApplicationRecord
belongs_to :book, -> { includes :author }
end
class Book < ApplicationRecord
belongs_to :author
has_many :chapters
end
class Author < ApplicationRecord
has_many :books
end
NOTA: No es necesario utilizar includes
para asociaciones inmediatas, es decir, si tienes Book belongs_to :author
, entonces el autor se carga de forma ansiosa automáticamente cuando se necesita.
4.1.3.3 readonly
Si utilizas readonly
, entonces el objeto asociado será de solo lectura cuando se recupere a través de la asociación.
4.1.3.4 select
El método select
te permite anular la cláusula SELECT
de SQL que se utiliza para recuperar datos sobre el objeto asociado. Por defecto, Rails recupera todas las columnas.
CONSEJO: Si utilizas el método select
en una asociación belongs_to
, también debes establecer la opción :foreign_key
para garantizar los resultados correctos.
4.1.4 ¿Existen objetos asociados?
Puedes verificar si existen objetos asociados utilizando el método association.nil?
:
if @book.author.nil?
@msg = "No se encontró un autor para este libro"
end
4.1.5 ¿Cuándo se guardan los objetos?
Asignar un objeto a una asociación belongs_to
no guarda automáticamente el objeto. Tampoco guarda el objeto asociado.
4.2 Referencia de la asociación has_one
La asociación has_one
crea una coincidencia uno a uno con otro modelo. En términos de base de datos, esta asociación indica que la otra clase contiene la clave externa. Si esta clase contiene la clave externa, entonces debes usar belongs_to
en su lugar.
4.2.1 Métodos agregados por has_one
Cuando declaras una asociación has_one
, la clase que la declara automáticamente obtiene 6 métodos relacionados con la asociación:
association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
En todos estos métodos, association
se reemplaza con el símbolo pasado como primer argumento a has_one
. Por ejemplo, dada la declaración:
class Supplier < ApplicationRecord
has_one :account
end
Cada instancia del modelo Supplier
tendrá estos métodos:
account
account=
build_account
create_account
create_account!
reload_account
reset_account
NOTA: Al inicializar una nueva asociación has_one
o belongs_to
, debes utilizar el prefijo build_
para construir la asociación, en lugar del método association.build
que se utilizaría para las asociaciones has_many
o has_and_belongs_to_many
. Para crear uno, utiliza el prefijo create_
.
4.2.1.1 association
El método association
devuelve el objeto asociado, si existe alguno. Si no se encuentra ningún objeto asociado, devuelve nil
.
@account = @supplier.account
Si el objeto asociado ya se ha recuperado de la base de datos para este objeto, se devolverá la versión en caché. Para anular este comportamiento (y forzar una lectura de la base de datos), llama a #reload_association
en el objeto padre.
@account = @supplier.reload_account
Para descargar la versión en caché del objeto asociado, forzando que la próxima vez que se acceda a él, si corresponde, se consulte desde la base de datos, llama a #reset_association
en el objeto padre.
@supplier.reset_account
4.2.1.2 association=(associate)
El método association=
asigna un objeto asociado a este objeto. En segundo plano, esto significa extraer la clave primaria de este objeto y establecer la clave externa del objeto asociado con el mismo valor.
@supplier.account = @account
4.2.1.3 build_association(attributes = {})
El método build_association
devuelve un nuevo objeto del tipo asociado. Este objeto se instanciará a partir de los atributos pasados, y se establecerá el enlace a través de su clave externa, pero el objeto asociado no se guardará todavía.
@account = @supplier.build_account(terms: "Net 30")
4.2.1.4 create_association(attributes = {})
El método create_association
devuelve un nuevo objeto del tipo asociado. Este objeto se instanciará a partir de los atributos pasados, se establecerá el enlace a través de su clave externa y, una vez que pase todas las validaciones especificadas en el modelo asociado, se guardará el objeto asociado.
@account = @supplier.create_account(terms: "Net 30")
4.2.1.5 create_association!(attributes = {})
Hace lo mismo que create_association
anteriormente, pero genera una excepción ActiveRecord::RecordInvalid
si el registro no es válido.
4.2.2 Opciones para has_one
Si bien Rails utiliza valores predeterminados inteligentes que funcionarán bien en la mayoría de las situaciones, puede haber momentos en los que desees personalizar el comportamiento de la referencia de la asociación has_one
. Estas personalizaciones se pueden realizar fácilmente pasando opciones al crear la asociación. Por ejemplo, esta asociación utiliza dos opciones:
class Supplier < ApplicationRecord
has_one :account, class_name: "Billing", dependent: :nullify
end
La asociación has_one
admite estas opciones:
:as
:autosave
:class_name
:dependent
:foreign_key
:inverse_of
:primary_key
:source
:source_type
:through
:touch
:validate
4.2.2.1 :as
Establecer la opción :as
indica que esta es una asociación polimórfica. Las asociaciones polimórficas se discutieron en detalle anteriormente en esta guía.
4.2.2.2 :autosave
Si estableces la opción :autosave
en true
, Rails guardará los miembros de la asociación cargados y destruirá los miembros que estén marcados para su eliminación cada vez que guardes el objeto padre. Establecer :autosave
en false
no es lo mismo que no establecer la opción :autosave
. Si la opción :autosave
no está presente, los nuevos objetos asociados se guardarán, pero los objetos asociados actualizados no se guardarán.
4.2.2.3 :class_name
Si el nombre del otro modelo no se puede derivar del nombre de la asociación, puedes usar la opción :class_name
para proporcionar el nombre del modelo. Por ejemplo, si un proveedor tiene una cuenta, pero el nombre real del modelo que contiene las cuentas es Billing
, configurarías las cosas de esta manera:
class Supplier < ApplicationRecord
has_one :account, class_name: "Billing"
end
4.2.2.4 :dependent
Controla lo que sucede con el objeto asociado cuando su propietario es destruido:
:destroy
hace que el objeto asociado también sea destruido:delete
hace que el objeto asociado sea eliminado directamente de la base de datos (por lo que los callbacks no se ejecutarán):destroy_async
: cuando el objeto es destruido, se encola un trabajoActiveRecord::DestroyAssociationAsyncJob
que llamará a destroy en sus objetos asociados. Active Job debe estar configurado para que esto funcione. No utilices esta opción si la asociación está respaldada por restricciones de clave externa en tu base de datos. Las acciones de restricción de clave externa ocurrirán dentro de la misma transacción que elimina su propietario.:nullify
hace que la clave externa se establezca enNULL
. La columna de tipo polimórfico también se establece en nulo en las asociaciones polimórficas. Los callbacks no se ejecutan.:restrict_with_exception
provoca que se genere una excepciónActiveRecord::DeleteRestrictionError
si hay un registro asociado:restrict_with_error
provoca que se agregue un error al propietario si hay un objeto asociado
Es necesario no establecer o dejar la opción :nullify
para aquellas asociaciones que tienen restricciones de base de datos NOT NULL
. Si no estableces dependent
para destruir tales asociaciones, no podrás cambiar el objeto asociado porque la clave externa del objeto asociado inicial se establecerá en el valor NULL
no permitido.
4.2.2.5 :foreign_key
Por convención, Rails asume que la columna utilizada para almacenar la clave externa en el otro modelo es el nombre de este modelo con el sufijo _id
agregado. La opción :foreign_key
te permite establecer el nombre de la clave externa directamente:
class Supplier < ApplicationRecord
has_one :account, foreign_key: "supp_id"
end
CONSEJO: En cualquier caso, Rails no creará columnas de clave externa por ti. Debes definirlas explícitamente como parte de tus migraciones.
4.2.2.6 :inverse_of
La opción :inverse_of
especifica el nombre de la asociación belongs_to
que es la inversa de esta asociación.
Consulta la sección de asociación bidireccional para más detalles.
class Supplier < ApplicationRecord
has_one :account, inverse_of: :supplier
end
class Account < ApplicationRecord
belongs_to :supplier, inverse_of: :account
end
4.2.2.7 :primary_key
Por convención, Rails asume que la columna utilizada para almacenar la clave primaria de este modelo es id
. Puedes anular esto y especificar explícitamente la clave primaria con la opción :primary_key
.
4.2.2.8 :source
La opción :source
especifica el nombre de la asociación de origen para una asociación has_one :through
.
4.2.2.9 :source_type
La opción :source_type
especifica el tipo de asociación de origen para una asociación has_one :through
que procede a través de una asociación polimórfica.
class Author < ApplicationRecord
has_one :book
has_one :hardback, through: :book, source: :format, source_type: "Hardback"
has_one :dust_jacket, through: :hardback
end
class Book < ApplicationRecord
belongs_to :format, polymorphic: true
end
class Paperback < ApplicationRecord; end
class Hardback < ApplicationRecord
has_one :dust_jacket
end
class DustJacket < ApplicationRecord; end
4.2.2.10 :through
La opción :through
especifica un modelo de unión a través del cual realizar la consulta. Las asociaciones has_one :through
se discutieron en detalle anteriormente en esta guía.
4.2.2.11 :touch
Si estableces la opción :touch
en true
, la marca de tiempo updated_at
o updated_on
en el objeto asociado se establecerá en la hora actual cada vez que se guarde o destruya este objeto:
class Supplier < ApplicationRecord
has_one :account, touch: true
end
class Account < ApplicationRecord
belongs_to :supplier
end
En este caso, guardar o destruir un proveedor actualizará la marca de tiempo en la cuenta asociada. También puedes especificar un atributo de marca de tiempo particular para actualizar:
class Supplier < ApplicationRecord
has_one :account, touch: :suppliers_updated_at
end
4.2.2.12 :validate
Si estableces la opción :validate
en true
, los nuevos objetos asociados se validarán cada vez que guardes este objeto. Por defecto, esto es false
: los nuevos objetos asociados no se validarán cuando se guarde este objeto.
4.2.3 Alcances para has_one
Puede haber momentos en los que desees personalizar la consulta utilizada por has_one
. Estas personalizaciones se pueden lograr a través de un bloque de alcance. Por ejemplo:
class Supplier < ApplicationRecord
has_one :account, -> { where active: true }
end
Puedes usar cualquiera de los métodos de consulta estándar querying methods dentro del bloque de alcance. Los siguientes se discuten a continuación:
where
includes
readonly
select
4.2.3.1 where
El método where
te permite especificar las condiciones que debe cumplir el objeto asociado.
class Supplier < ApplicationRecord
has_one :account, -> { where "confirmed = 1" }
end
4.2.3.2 includes
Puedes usar el método includes
para especificar asociaciones de segundo orden que deben cargarse de forma ansiosa cuando se utiliza esta asociación. Por ejemplo, considera estos modelos:
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
belongs_to :representative
end
class Representative < ApplicationRecord
has_many :accounts
end
Si recuperas frecuentemente representantes directamente de los proveedores (@supplier.account.representative
), puedes hacer tu código un poco más eficiente incluyendo representantes en la asociación de proveedores a cuentas:
class Supplier < ApplicationRecord
has_one :account, -> { includes :representative }
end
class Account < ApplicationRecord
belongs_to :supplier
belongs_to :representative
end
class Representative < ApplicationRecord
has_many :accounts
end
4.2.3.3 readonly
Si usas el método readonly
, entonces el objeto asociado será de solo lectura cuando se recupere a través de la asociación.
4.2.3.4 select
El método select
te permite anular la cláusula SQL SELECT
que se utiliza para recuperar datos sobre el objeto asociado. Por defecto, Rails recupera todas las columnas.
4.2.4 ¿Existen objetos asociados?
Puedes ver si existen objetos asociados usando el método association.nil?
:
if @supplier.account.nil?
@msg = "No se encontró ninguna cuenta para este proveedor"
end
4.2.5 ¿Cuándo se guardan los objetos?
Cuando asignas un objeto a una asociación has_one
, ese objeto se guarda automáticamente (para actualizar su clave externa). Además, cualquier objeto que se reemplace también se guarda automáticamente, porque su clave externa también cambiará.
Si cualquiera de estos guardados falla debido a errores de validación, entonces la declaración de asignación devuelve false
y la asignación en sí se cancela.
Si el objeto padre (el que declara la asociación has_one
) no está guardado (es decir, new_record?
devuelve true
), entonces los objetos hijos no se guardan. Se guardarán automáticamente cuando se guarde el objeto padre.
Si quieres asignar un objeto a una asociación has_one
sin guardar el objeto, usa el método build_association
.
4.3 Referencia de la asociación has_many
La asociación has_many
crea una relación uno a muchos con otro modelo. En términos de base de datos, esta asociación indica que la otra clase tendrá una clave externa que se refiere a instancias de esta clase.
4.3.1 Métodos agregados por has_many
Cuando declaras una asociación has_many
, la clase que la declara automáticamente obtiene 17 métodos relacionados con la asociación:
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
En todos estos métodos, collection
se reemplaza con el símbolo pasado como primer argumento a has_many
, y collection_singular
se reemplaza con la versión singularizada de ese símbolo. Por ejemplo, dada la declaración:
class Author < ApplicationRecord
has_many :books
end
Cada instancia del modelo Author
tendrá estos métodos:
books
books<<(object, ...)
books.delete(object, ...)
books.destroy(object, ...)
books=(objects)
book_ids
book_ids=(ids)
books.clear
books.empty?
books.size
books.find(...)
books.where(...)
books.exists?(...)
books.build(attributes = {}, ...)
books.create(attributes = {})
books.create!(attributes = {})
books.reload
4.3.1.1 collection
El método collection
devuelve una relación de todos los objetos asociados. Si no hay objetos asociados, devuelve una relación vacía.
@books = @author.books
4.3.1.2 collection<<(object, ...)
El método collection<<
agrega uno o más objetos a la colección estableciendo sus claves externas en la clave primaria del modelo que llama.
@author.books << @book1
4.3.1.3 collection.delete(object, ...)
El método collection.delete
elimina uno o más objetos de la colección estableciendo sus claves externas en NULL
.
@author.books.delete(@book1)
ADVERTENCIA: Además, los objetos se destruirán si están asociados con dependent: :destroy
, y se eliminarán si están asociados con dependent: :delete_all
.
4.3.1.4 collection.destroy(object, ...)
El método collection.destroy
elimina uno o más objetos de la colección ejecutando destroy
en cada objeto.
@author.books.destroy(@book1)
ADVERTENCIA: Los objetos siempre se eliminarán de la base de datos, ignorando la opción :dependent
.
4.3.1.5 collection=(objects)
El método collection=
hace que la colección contenga solo los objetos suministrados, agregando y eliminando según corresponda. Los cambios se persisten en la base de datos.
4.3.1.6 collection_singular_ids
El método collection_singular_ids
devuelve un array de los ids de los objetos en la colección.
@book_ids = @author.book_ids
4.3.1.7 collection_singular_ids=(ids)
El método collection_singular_ids=
hace que la colección contenga solo los objetos identificados por los valores de clave primaria suministrados, agregando y eliminando según corresponda. Los cambios se guardan en la base de datos.
4.3.1.8 collection.clear
El método collection.clear
elimina todos los objetos de la colección según la estrategia especificada por la opción dependent
. Si no se proporciona ninguna opción, sigue la estrategia predeterminada. La estrategia predeterminada para las asociaciones has_many :through
es delete_all
, y para las asociaciones has_many
es establecer las claves foráneas en NULL
.
@author.books.clear
ADVERTENCIA: Los objetos se eliminarán si están asociados con dependent: :destroy
o dependent: :destroy_async
, al igual que con dependent: :delete_all
.
4.3.1.9 collection.empty?
El método collection.empty?
devuelve true
si la colección no contiene ningún objeto asociado.
<% if @author.books.empty? %>
No se encontraron libros
<% end %>
4.3.1.10 collection.size
El método collection.size
devuelve el número de objetos en la colección.
@book_count = @author.books.size
4.3.1.11 collection.find(...)
El método collection.find
encuentra objetos dentro de la tabla de la colección.
@available_book = @author.books.find(1)
4.3.1.12 collection.where(...)
El método collection.where
encuentra objetos dentro de la colección basados en las condiciones suministradas, pero los objetos se cargan de forma diferida, lo que significa que la base de datos se consulta solo cuando se accede al objeto(s).
@available_books = author.books.where(available: true) # No hay consulta aún
@available_book = @available_books.first # Ahora se consultará la base de datos
4.3.1.13 collection.exists?(...)
El método collection.exists?
verifica si existe un objeto que cumpla las condiciones suministradas en la tabla de la colección.
4.3.1.14 collection.build(attributes = {})
El método collection.build
devuelve uno o varios objetos nuevos del tipo asociado. El/los objeto(s) se instanciarán a partir de los atributos pasados, y se creará el enlace a través de su clave foránea, pero los objetos asociados no se guardarán todavía.
@book = author.books.build(published_at: Time.now,
book_number: "A12345")
@books = author.books.build([
{ published_at: Time.now, book_number: "A12346" },
{ published_at: Time.now, book_number: "A12347" }
])
4.3.1.15 collection.create(attributes = {})
El método collection.create
devuelve uno o varios objetos nuevos del tipo asociado. El/los objeto(s) se instanciarán a partir de los atributos pasados, se creará el enlace a través de su clave foránea y, una vez que pase todas las validaciones especificadas en el modelo asociado, el objeto asociado se guardará.
@book = author.books.create(published_at: Time.now,
book_number: "A12345")
@books = author.books.create([
{ published_at: Time.now, book_number: "A12346" },
{ published_at: Time.now, book_number: "A12347" }
])
4.3.1.16 collection.create!(attributes = {})
Hace lo mismo que collection.create
anteriormente, pero genera una excepción ActiveRecord::RecordInvalid
si el registro no es válido.
4.3.1.17 collection.reload
El método collection.reload
devuelve una relación de todos los objetos asociados, forzando una lectura de la base de datos. Si no hay objetos asociados, devuelve una relación vacía.
@books = author.books.reload
4.3.2 Opciones para has_many
Si bien Rails utiliza valores predeterminados inteligentes que funcionarán bien en la mayoría de las situaciones, puede haber momentos en los que desee personalizar el comportamiento de la referencia de asociación has_many
. Estas personalizaciones se pueden lograr fácilmente pasando opciones al crear la asociación. Por ejemplo, esta asociación utiliza dos opciones:
class Author < ApplicationRecord
has_many :books, dependent: :delete_all, validate: false
end
La asociación has_many
admite estas opciones:
:as
:autosave
:class_name
:counter_cache
:dependent
:foreign_key
:inverse_of
:primary_key
:source
:source_type
:through
:validate
4.3.2.1 :as
Establecer la opción :as
indica que esta es una asociación polimórfica, como se discutió anteriormente en esta guía.
4.3.2.2 :autosave
Si establece la opción :autosave
en true
, Rails guardará los miembros de la asociación cargados y destruirá los miembros que estén marcados para su eliminación cada vez que guarde el objeto padre. Establecer :autosave
en false
no es lo mismo que no establecer la opción :autosave
. Si la opción :autosave
no está presente, los nuevos objetos asociados se guardarán, pero los objetos asociados actualizados no se guardarán.
4.3.2.3 :class_name
Si el nombre del otro modelo no se puede derivar del nombre de la asociación, puede utilizar la opción :class_name
para proporcionar el nombre del modelo. Por ejemplo, si un autor tiene muchos libros, pero el nombre real del modelo que contiene los libros es Transaction
, lo configuraría de esta manera:
class Author < ApplicationRecord
has_many :books, class_name: "Transaction"
end
4.3.2.4 :counter_cache
Esta opción se puede utilizar para configurar un :counter_cache
personalizado con un nombre específico. Solo necesitas esta opción cuando has personalizado el nombre de tu :counter_cache
en la asociación belongs_to.
4.3.2.5 :dependent
Controla lo que sucede con los objetos asociados cuando su propietario es destruido:
:destroy
hace que todos los objetos asociados también sean destruidos.:delete_all
hace que todos los objetos asociados sean eliminados directamente de la base de datos (por lo que los callbacks no se ejecutarán).:destroy_async
: cuando el objeto es destruido, se encola un trabajoActiveRecord::DestroyAssociationAsyncJob
que llamará a destroy en sus objetos asociados. Active Job debe estar configurado para que esto funcione.:nullify
hace que la clave foránea se establezca enNULL
. La columna de tipo polimórfico también se establece en nulo en las asociaciones polimórficas. Los callbacks no se ejecutan.:restrict_with_exception
provoca que se genere una excepciónActiveRecord::DeleteRestrictionError
si hay algún registro asociado.:restrict_with_error
provoca que se agregue un error al propietario si hay algún objeto asociado.
Las opciones :destroy
y :delete_all
también afectan la semántica de los métodos collection.delete
y collection=
al hacer que destruyan los objetos asociados cuando se eliminan de la colección.
4.3.2.6 :foreign_key
Por convención, Rails asume que la columna utilizada para almacenar la clave foránea en el otro modelo es el nombre de este modelo con el sufijo _id
agregado. La opción :foreign_key
te permite establecer directamente el nombre de la clave foránea:
class Author < ApplicationRecord
has_many :books, foreign_key: "cust_id"
end
CONSEJO: En cualquier caso, Rails no creará columnas de clave foránea por ti. Debes definirlas explícitamente como parte de tus migraciones.
4.3.2.7 :inverse_of
La opción :inverse_of
especifica el nombre de la asociación belongs_to
que es la inversa de esta asociación. Consulta la sección de asociaciones bidireccionales para obtener más detalles.
class Author < ApplicationRecord
has_many :books, inverse_of: :author
end
class Book < ApplicationRecord
belongs_to :author, inverse_of: :books
end
4.3.2.8 :primary_key
Por convención, Rails asume que la columna utilizada para almacenar la clave primaria de la asociación es id
. Puedes anular esto y especificar explícitamente la clave primaria con la opción :primary_key
.
Supongamos que la tabla users
tiene id
como clave primaria pero también tiene una columna guid
. El requisito es que la tabla todos
debe contener el valor de la columna guid
como clave foránea y no el valor id
. Esto se puede lograr de la siguiente manera:
class User < ApplicationRecord
has_many :todos, primary_key: :guid
end
Ahora, si ejecutamos @todo = @user.todos.create
, el valor de user_id
en el registro @todo
será el valor guid
de @user
.
4.3.2.9 :source
La opción :source
especifica el nombre de la asociación de origen para una asociación has_many :through
. Solo necesitas usar esta opción si el nombre de la asociación de origen no se puede inferir automáticamente a partir del nombre de la asociación.
4.3.2.10 :source_type
La opción :source_type
especifica el tipo de asociación de origen para una asociación has_many :through
que procede a través de una asociación polimórfica.
class Author < ApplicationRecord
has_many :books
has_many :paperbacks, through: :books, source: :format, source_type: "Paperback"
end
class Book < ApplicationRecord
belongs_to :format, polymorphic: true
end
class Hardback < ApplicationRecord; end
class Paperback < ApplicationRecord; end
4.3.2.11 :through
La opción :through
especifica un modelo de unión a través del cual realizar la consulta. Las asociaciones has_many :through
proporcionan una forma de implementar relaciones de muchos a muchos, como se discutió anteriormente en esta guía.
4.3.2.12 :validate
Si estableces la opción :validate
en false
, los nuevos objetos asociados no se validarán cuando guardes este objeto. De forma predeterminada, esto es true
: los nuevos objetos asociados se validarán cuando se guarde este objeto.
4.3.3 Scopes para has_many
Puede haber momentos en los que desees personalizar la consulta utilizada por has_many
. Estas personalizaciones se pueden lograr mediante un bloque de scope. Por ejemplo:
class Author < ApplicationRecord
has_many :books, -> { where processed: true }
end
Puedes utilizar cualquiera de los métodos de consulta estándar active_record_querying.html dentro del bloque de scope. A continuación, se discuten los siguientes:
where
extending
group
includes
limit
offset
order
readonly
select
distinct
4.3.3.1 where
El método where
te permite especificar las condiciones que debe cumplir el objeto asociado.
class Author < ApplicationRecord
has_many :confirmed_books, -> { where "confirmed = 1" },
class_name: "Book"
end
También puedes establecer condiciones a través de un hash:
class Author < ApplicationRecord
has_many :confirmed_books, -> { where confirmed: true },
class_name: "Book"
end
Si usas la opción where
en estilo de hash, la creación de registros a través de esta asociación se limitará automáticamente utilizando el hash. En este caso, al utilizar @author.confirmed_books.create
o @author.confirmed_books.build
, se crearán libros donde la columna confirmed
tenga el valor true
.
4.3.3.2 extending
El método extending
especifica un módulo con nombre para extender el proxy de la asociación. Las extensiones de asociación se discuten en detalle más adelante en esta guía.
4.3.3.3 group
El método group
proporciona un nombre de atributo para agrupar el conjunto de resultados utilizando una cláusula GROUP BY
en la consulta SQL.
class Author < ApplicationRecord
has_many :chapters, -> { group 'books.id' },
through: :books
end
4.3.3.4 includes
Puedes utilizar el método includes
para especificar asociaciones de segundo orden que deben cargarse de forma ansiosa cuando se utiliza esta asociación. Por ejemplo, considera estos modelos:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
has_many :chapters
end
class Chapter < ApplicationRecord
belongs_to :book
end
Si frecuentemente recuperas capítulos directamente de autores (@author.books.chapters
), puedes hacer tu código un poco más eficiente incluyendo los capítulos en la asociación de autores a libros:
class Author < ApplicationRecord
has_many :books, -> { includes :chapters }
end
class Book < ApplicationRecord
belongs_to :author
has_many :chapters
end
class Chapter < ApplicationRecord
belongs_to :book
end
4.3.3.5 limit
El método limit
te permite restringir el número total de objetos que se obtendrán a través de una asociación.
class Author < ApplicationRecord
has_many :recent_books,
-> { order('published_at desc').limit(100) },
class_name: "Book"
end
4.3.3.6 offset
El método offset
te permite especificar el desplazamiento inicial para obtener objetos a través de una asociación. Por ejemplo, -> { offset(11) }
omitirá los primeros 11 registros.
4.3.3.7 order
El método order
dicta el orden en el que se recibirán los objetos asociados (en la sintaxis utilizada por una cláusula ORDER BY
en SQL).
class Author < ApplicationRecord
has_many :books, -> { order "date_confirmed DESC" }
end
4.3.3.8 readonly
Si utilizas el método readonly
, los objetos asociados serán de solo lectura al ser recuperados a través de la asociación.
4.3.3.9 select
El método select
te permite anular la cláusula SQL SELECT
que se utiliza para recuperar datos sobre los objetos asociados. Por defecto, Rails recupera todas las columnas.
ADVERTENCIA: Si especificas tu propio select
, asegúrate de incluir las columnas de clave primaria y clave externa del modelo asociado. Si no lo haces, Rails lanzará un error.
4.3.3.10 distinct
Utiliza el método distinct
para mantener la colección libre de duplicados. Esto es especialmente útil junto con la opción :through
.
class Person < ApplicationRecord
has_many :readings
has_many :articles, through: :readings
end
irb> person = Person.create(name: 'John')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
irb> person.articles.to_a
=> [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
irb> Reading.all.to_a
=> [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
En el caso anterior, hay dos lecturas y person.articles
muestra ambas, aunque estos registros apuntan al mismo artículo.
Ahora vamos a establecer distinct
:
class Person
has_many :readings
has_many :articles, -> { distinct }, through: :readings
end
irb> person = Person.create(name: 'Honda')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
irb> person.articles.to_a
=> [#<Article id: 7, name: "a1">]
irb> Reading.all.to_a
=> [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
En el caso anterior, todavía hay dos lecturas. Sin embargo, person.articles
muestra solo un artículo porque la colección carga solo registros únicos.
Si quieres asegurarte de que, al insertar, todos los registros en la asociación persistente sean distintos (para que puedas estar seguro de que al inspeccionar la asociación nunca encontrarás registros duplicados), debes agregar un índice único en la tabla misma. Por ejemplo, si tienes una tabla llamada readings
y quieres asegurarte de que los artículos solo se puedan agregar a una persona una vez, podrías agregar lo siguiente en una migración:
add_index :readings, [:person_id, :article_id], unique: true
Una vez que tenga este índice único, intentar agregar el artículo a una persona dos veces generará un error ActiveRecord::RecordNotUnique
:
irb> person = Person.create(name: 'Honda')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
ActiveRecord::RecordNotUnique
Tenga en cuenta que verificar la unicidad utilizando algo como include?
está sujeto a condiciones de carrera. No intente usar include?
para garantizar la distinción en una asociación. Por ejemplo, utilizando el ejemplo del artículo anterior, el siguiente código sería propenso a condiciones de carrera porque varios usuarios podrían intentar esto al mismo tiempo:
person.articles << article unless person.articles.include?(article)
4.3.4 ¿Cuándo se guardan los objetos?
Cuando asigna un objeto a una asociación has_many
, ese objeto se guarda automáticamente (para actualizar su clave externa). Si asigna varios objetos en una sola declaración, todos se guardan.
Si alguno de estos guardados falla debido a errores de validación, la declaración de asignación devuelve false
y la asignación en sí se cancela.
Si el objeto principal (el que declara la asociación has_many
) no está guardado (es decir, new_record?
devuelve true
), entonces los objetos secundarios no se guardan cuando se agregan. Todos los miembros no guardados de la asociación se guardarán automáticamente cuando se guarde el padre.
Si desea asignar un objeto a una asociación has_many
sin guardar el objeto, use el método collection.build
.
4.4 Referencia de la asociación has_and_belongs_to_many
La asociación has_and_belongs_to_many
crea una relación de muchos a muchos con otro modelo. En términos de base de datos, esto asocia dos clases a través de una tabla de unión intermedia que incluye claves externas que se refieren a cada una de las clases.
4.4.1 Métodos agregados por has_and_belongs_to_many
Cuando declara una asociación has_and_belongs_to_many
, la clase que la declara automáticamente obtiene varios métodos relacionados con la asociación:
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
En todos estos métodos, collection
se reemplaza con el símbolo pasado como primer argumento a has_and_belongs_to_many
, y collection_singular
se reemplaza con la versión singularizada de ese símbolo. Por ejemplo, dada la declaración:
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
Cada instancia del modelo Part
tendrá estos métodos:
assemblies
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies.destroy(object, ...)
assemblies=(objects)
assembly_ids
assembly_ids=(ids)
assemblies.clear
assemblies.empty?
assemblies.size
assemblies.find(...)
assemblies.where(...)
assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
assemblies.create!(attributes = {})
assemblies.reload
4.4.1.1 Métodos adicionales de columna
Si la tabla de unión para una asociación has_and_belongs_to_many
tiene columnas adicionales además de las dos claves externas, estas columnas se agregarán como atributos a los registros recuperados a través de esa asociación. Los registros devueltos con atributos adicionales siempre serán de solo lectura, porque Rails no puede guardar cambios en esos atributos.
ADVERTENCIA: El uso de atributos adicionales en la tabla de unión en una asociación has_and_belongs_to_many
está en desuso. Si necesita este tipo de comportamiento complejo en la tabla que une dos modelos en una relación de muchos a muchos, debe usar una asociación has_many :through
en lugar de has_and_belongs_to_many
.
4.4.1.2 collection
El método collection
devuelve una relación de todos los objetos asociados. Si no hay objetos asociados, devuelve una relación vacía.
@assemblies = @part.assemblies
4.4.1.3 collection<<(object, ...)
El método collection<<
agrega uno o más objetos a la colección creando registros en la tabla de unión.
@part.assemblies << @assembly1
NOTA: Este método también se conoce como collection.concat
y collection.push
.
4.4.1.4 collection.delete(object, ...)
El método collection.delete
elimina uno o más objetos de la colección eliminando registros en la tabla de unión. Esto no destruye los objetos.
@part.assemblies.delete(@assembly1)
4.4.1.5 collection.destroy(object, ...)
El método collection.destroy
elimina uno o más objetos de la colección eliminando registros en la tabla de unión. Esto no destruye los objetos.
@part.assemblies.destroy(@assembly1)
4.4.1.6 collection=(objects)
El método collection=
hace que la colección contenga solo los objetos suministrados, agregando y eliminando según corresponda. Los cambios se persisten en la base de datos.
4.4.1.7 collection_singular_ids
El método collection_singular_ids
devuelve una matriz de los ids de los objetos en la colección.
@assembly_ids = @part.assembly_ids
4.4.1.8 collection_singular_ids=(ids)
El método collection_singular_ids=
hace que la colección contenga solo los objetos identificados por los valores de clave primaria suministrados, agregando y eliminando según corresponda. Los cambios se persisten en la base de datos.
4.4.1.9 collection.clear
El método collection.clear
elimina todos los objetos de la colección al eliminar las filas de la tabla de unión. Esto no destruye los objetos asociados.
4.4.1.10 collection.empty?
El método collection.empty?
devuelve true
si la colección no contiene ningún objeto asociado.
<% if @part.assemblies.empty? %>
Esta parte no se utiliza en ningún ensamblaje
<% end %>
4.4.1.11 collection.size
El método collection.size
devuelve el número de objetos en la colección.
@assembly_count = @part.assemblies.size
4.4.1.12 collection.find(...)
El método collection.find
encuentra objetos dentro de la tabla de la colección.
@assembly = @part.assemblies.find(1)
4.4.1.13 collection.where(...)
El método collection.where
encuentra objetos dentro de la colección basados en las condiciones suministradas, pero los objetos se cargan de forma perezosa, lo que significa que la base de datos se consulta solo cuando se accede a los objetos.
@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
4.4.1.14 collection.exists?(...)
El método collection.exists?
verifica si existe un objeto que cumpla las condiciones suministradas en la tabla de la colección.
4.4.1.15 collection.build(attributes = {})
El método collection.build
devuelve un nuevo objeto del tipo asociado. Este objeto se instanciará a partir de los atributos pasados, y se creará el enlace a través de la tabla de unión, pero el objeto asociado aún no se guardará.
@assembly = @part.assemblies.build({ assembly_name: "Caja de transmisión" })
4.4.1.16 collection.create(attributes = {})
El método collection.create
devuelve un nuevo objeto del tipo asociado. Este objeto se instanciará a partir de los atributos pasados, se creará el enlace a través de la tabla de unión y, una vez que pase todas las validaciones especificadas en el modelo asociado, se guardará el objeto asociado.
@assembly = @part.assemblies.create({ assembly_name: "Caja de transmisión" })
4.4.1.17 collection.create!(attributes = {})
Hace lo mismo que collection.create
, pero genera una excepción ActiveRecord::RecordInvalid
si el registro no es válido.
4.4.1.18 collection.reload
El método collection.reload
devuelve una relación de todos los objetos asociados, forzando una lectura de la base de datos. Si no hay objetos asociados, devuelve una relación vacía.
@assemblies = @part.assemblies.reload
4.4.2 Opciones para has_and_belongs_to_many
Si bien Rails utiliza valores predeterminados inteligentes que funcionarán bien en la mayoría de las situaciones, puede haber momentos en los que desee personalizar el comportamiento de la referencia de la asociación has_and_belongs_to_many
. Estas personalizaciones se pueden realizar fácilmente pasando opciones al crear la asociación. Por ejemplo, esta asociación utiliza dos opciones:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { readonly },
autosave: true
end
La asociación has_and_belongs_to_many
admite estas opciones:
:association_foreign_key
:autosave
:class_name
:foreign_key
:join_table
:validate
4.4.2.1 :association_foreign_key
Por convención, Rails asume que la columna en la tabla de unión utilizada para almacenar la clave externa que apunta al otro modelo es el nombre de ese modelo con el sufijo _id
agregado. La opción :association_foreign_key
le permite establecer directamente el nombre de la clave externa:
CONSEJO: Las opciones :foreign_key
y :association_foreign_key
son útiles al configurar una autoasociación de muchos a muchos. Por ejemplo:
class User < ApplicationRecord
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
association_foreign_key: "other_user_id"
end
4.4.2.2 :autosave
Si establece la opción :autosave
en true
, Rails guardará los miembros de la asociación cargados y destruirá los miembros que estén marcados para su eliminación cada vez que guarde el objeto principal. Establecer :autosave
en false
no es lo mismo que no establecer la opción :autosave
. Si la opción :autosave
no está presente, los nuevos objetos asociados se guardarán, pero los objetos asociados actualizados no se guardarán.
4.4.2.3 :class_name
Si el nombre del otro modelo no se puede derivar del nombre de la asociación, puede utilizar la opción :class_name
para proporcionar el nombre del modelo. Por ejemplo, si una parte tiene muchos ensamblajes, pero el nombre real del modelo que contiene los ensamblajes es Gadget
, configuraría las cosas de esta manera:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, class_name: "Gadget"
end
4.4.2.4 :foreign_key
Por convención, Rails asume que la columna en la tabla de unión utilizada para almacenar la clave externa que apunta a este modelo es el nombre de este modelo con el sufijo _id
agregado. La opción :foreign_key
le permite establecer directamente el nombre de la clave externa:
class User < ApplicationRecord
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
association_foreign_key: "other_user_id"
end
4.4.2.5 :join_table
Si el nombre predeterminado de la tabla de unión, basado en el orden alfabético, no es el que desea, puede utilizar la opción :join_table
para anular el valor predeterminado.
4.4.2.6 :validate
Si estableces la opción :validate
en false
, entonces los nuevos objetos asociados no serán validados cada vez que guardes este objeto. Por defecto, esto es true
: los nuevos objetos asociados serán validados cuando se guarde este objeto.
4.4.3 Alcances para has_and_belongs_to_many
Puede haber momentos en los que desees personalizar la consulta utilizada por has_and_belongs_to_many
. Estas personalizaciones se pueden lograr mediante un bloque de alcance. Por ejemplo:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { where active: true }
end
Puedes utilizar cualquiera de los métodos de consulta estándar métodos de consulta de Active Record dentro del bloque de alcance. Los siguientes se discuten a continuación:
where
extending
group
includes
limit
offset
order
readonly
select
distinct
4.4.3.1 where
El método where
te permite especificar las condiciones que el objeto asociado debe cumplir.
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { where "factory = 'Seattle'" }
end
También puedes establecer condiciones a través de un hash:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { where factory: 'Seattle' }
end
Si utilizas un where
estilo hash, entonces la creación de registros a través de esta asociación se limitará automáticamente utilizando el hash. En este caso, al utilizar @parts.assemblies.create
o @parts.assemblies.build
se crearán ensamblajes donde la columna factory
tenga el valor "Seattle".
4.4.3.2 extending
El método extending
especifica un módulo con nombre para extender el proxy de asociación. Las extensiones de asociación se discuten en detalle más adelante en esta guía.
4.4.3.3 group
El método group
proporciona un nombre de atributo para agrupar el conjunto de resultados, utilizando una cláusula GROUP BY
en el SQL del buscador.
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { group "factory" }
end
4.4.3.4 includes
Puedes utilizar el método includes
para especificar asociaciones de segundo orden que se deben cargar de forma ansiosa cuando se utiliza esta asociación.
4.4.3.5 limit
El método limit
te permite restringir el número total de objetos que se obtendrán a través de una asociación.
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { order("created_at DESC").limit(50) }
end
4.4.3.6 offset
El método offset
te permite especificar el desplazamiento inicial para obtener objetos a través de una asociación. Por ejemplo, si estableces offset(11)
, se omitirán los primeros 11 registros.
4.4.3.7 order
El método order
dicta el orden en el que se recibirán los objetos asociados (en la sintaxis utilizada por una cláusula ORDER BY
de SQL).
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { order "assembly_name ASC" }
end
4.4.3.8 readonly
Si utilizas el método readonly
, entonces los objetos asociados serán de solo lectura cuando se recuperen a través de la asociación.
4.4.3.9 select
El método select
te permite anular la cláusula SQL SELECT
que se utiliza para recuperar datos sobre los objetos asociados. Por defecto, Rails recupera todas las columnas.
4.4.3.10 distinct
Utiliza el método distinct
para eliminar duplicados de la colección.
4.4.4 ¿Cuándo se Guardan los Objetos?
Cuando asignas un objeto a una asociación has_and_belongs_to_many
, ese objeto se guarda automáticamente (para actualizar la tabla de unión). Si asignas varios objetos en una sola declaración, todos se guardan.
Si alguna de estas guardas falla debido a errores de validación, entonces la declaración de asignación devuelve false
y la asignación en sí se cancela.
Si el objeto padre (el que declara la asociación has_and_belongs_to_many
) no está guardado (es decir, new_record?
devuelve true
), entonces los objetos hijos no se guardan cuando se agregan. Todos los miembros no guardados de la asociación se guardarán automáticamente cuando se guarde el padre.
Si quieres asignar un objeto a una asociación has_and_belongs_to_many
sin guardar el objeto, utiliza el método collection.build
.
4.5 Callbacks de Asociación
Los callbacks normales se conectan al ciclo de vida de los objetos de Active Record, lo que te permite trabajar con esos objetos en varios puntos. Por ejemplo, puedes utilizar un callback :before_save
para hacer que algo suceda justo antes de que se guarde un objeto.
Los callbacks de asociación son similares a los callbacks normales, pero se activan por eventos en el ciclo de vida de una colección. Hay cuatro callbacks de asociación disponibles:
before_add
after_add
before_remove
after_remove
Defines los callbacks de asociación agregando opciones a la declaración de la asociación. Por ejemplo:
class Author < ApplicationRecord
has_many :books, before_add: :check_credit_limit
def check_credit_limit(book)
# ...
end
end
Rails pasa el objeto que se está agregando o eliminando al callback. Puede apilar devoluciones de llamada en un solo evento pasándolas como una matriz:
class Author < ApplicationRecord
has_many :books,
before_add: [:check_credit_limit, :calculate_shipping_charges]
def check_credit_limit(book)
# ...
end
def calculate_shipping_charges(book)
# ...
end
end
Si una devolución de llamada before_add
lanza :abort
, el objeto no se agrega a la colección. De manera similar, si una devolución de llamada before_remove
lanza :abort
, el objeto no se elimina de la colección:
# el libro no se agregará si se alcanza el límite
def check_credit_limit(book)
throw(:abort) if limit_reached?
end
NOTA: Estas devoluciones de llamada solo se llaman cuando los objetos asociados se agregan o eliminan a través de la colección de asociación:
# Desencadena la devolución de llamada `before_add`
author.books << book
author.books = [book, book2]
# No desencadena la devolución de llamada `before_add`
book.update(author_id: 1)
4.6 Extensiones de asociación
No estás limitado a la funcionalidad que Rails construye automáticamente en los objetos proxy de asociación. También puedes extender estos objetos a través de módulos anónimos, agregando nuevos buscadores, creadores u otros métodos. Por ejemplo:
class Author < ApplicationRecord
has_many :books do
def find_by_book_prefix(book_number)
find_by(category_id: book_number[0..2])
end
end
end
Si tienes una extensión que debe ser compartida por muchas asociaciones, puedes usar un módulo de extensión con nombre. Por ejemplo:
module FindRecentExtension
def find_recent
where("created_at > ?", 5.days.ago)
end
end
class Author < ApplicationRecord
has_many :books, -> { extending FindRecentExtension }
end
class Supplier < ApplicationRecord
has_many :deliveries, -> { extending FindRecentExtension }
end
Las extensiones pueden hacer referencia a los internos del proxy de asociación utilizando estos tres atributos del accesor proxy_association
:
proxy_association.owner
devuelve el objeto del que forma parte la asociación.proxy_association.reflection
devuelve el objeto de reflexión que describe la asociación.proxy_association.target
devuelve el objeto asociado parabelongs_to
ohas_one
, o la colección de objetos asociados parahas_many
ohas_and_belongs_to_many
.
4.7 Asociación de ámbito utilizando el propietario de la asociación
El propietario de la asociación se puede pasar como un solo argumento al bloque de ámbito en situaciones en las que se necesita un control aún mayor sobre el ámbito de la asociación. Sin embargo, como advertencia, ya no será posible precargar la asociación.
class Supplier < ApplicationRecord
has_one :account, ->(supplier) { where active: supplier.active? }
end
5 Herencia de tabla única (STI)
A veces, es posible que desees compartir campos y comportamiento entre diferentes modelos. Digamos que tenemos los modelos Car, Motorcycle y Bicycle. Queremos compartir los campos color
y price
y algunos métodos para todos ellos, pero tener un comportamiento específico para cada uno y controladores separados también.
Primero, generemos el modelo base Vehicle:
$ bin/rails generate model vehicle type:string color:string price:decimal{10.2}
¿Notaste que estamos agregando un campo "type"? Dado que todos los modelos se guardarán en una sola tabla de base de datos, Rails guardará en esta columna el nombre del modelo que se está guardando. En nuestro ejemplo, esto puede ser "Car", "Motorcycle" o "Bicycle". STI no funcionará sin un campo "type" en la tabla.
A continuación, generaremos el modelo Car que hereda de Vehicle. Para esto, podemos usar la opción --parent=PARENT
, que generará un modelo que hereda del padre especificado y sin migración equivalente (ya que la tabla ya existe).
Por ejemplo, para generar el modelo Car:
$ bin/rails generate model car --parent=Vehicle
El modelo generado se verá así:
class Car < Vehicle
end
Esto significa que todo el comportamiento agregado a Vehicle también está disponible para Car, como asociaciones, métodos públicos, etc.
Crear un coche lo guardará en la tabla vehicles
con "Car" como campo type
:
Car.create(color: 'Red', price: 10000)
generará el siguiente SQL:
INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
Consultar registros de coches buscará solo vehículos que sean coches:
Car.all
ejecutará una consulta como esta:
SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')
6 Tipos delegados
Herencia de tabla única (STI)
funciona mejor cuando hay poca diferencia entre las subclases y sus atributos, pero incluye todos los atributos de todas las subclases que necesitas crear en una sola tabla.
La desventaja de este enfoque es que resulta en un aumento de tamaño de esa tabla. Ya que incluso incluirá atributos específicos de una subclase que no son utilizados por nada más.
En el siguiente ejemplo, hay dos modelos de Active Record que heredan de la misma clase "Entry" que incluye el atributo subject
.
```ruby
Esquema: entradas[ id, tipo, asunto, creado_en, actualizado_en]
class Entry < ApplicationRecord end
class Comment < Entry end
class Message < Entry end ```
Los tipos delegados resuelven este problema, a través de delegated_type
.
Para utilizar los tipos delegados, debemos modelar nuestros datos de una manera particular. Los requisitos son los siguientes:
- Hay una superclase que almacena los atributos compartidos entre todas las subclases en su tabla.
- Cada subclase debe heredar de la superclase y tendrá una tabla separada para cualquier atributo adicional específico de ella.
Esto elimina la necesidad de definir atributos en una sola tabla que se comparten involuntariamente entre todas las subclases.
Para aplicar esto a nuestro ejemplo anterior, necesitamos regenerar nuestros modelos.
Primero, generemos el modelo base Entry
que actuará como nuestra superclase:
$ bin/rails generate model entry entryable_type:string entryable_id:integer
Luego, generaremos los nuevos modelos Message
y Comment
para la delegación:
$ bin/rails generate model message subject:string body:string
$ bin/rails generate model comment content:string
Después de ejecutar los generadores, deberíamos obtener modelos que se vean así:
# Esquema: entradas[ id, entryable_type, entryable_id, creado_en, actualizado_en ]
class Entry < ApplicationRecord
end
# Esquema: mensajes[ id, asunto, cuerpo, creado_en, actualizado_en ]
class Message < ApplicationRecord
end
# Esquema: comentarios[ id, contenido, creado_en, actualizado_en ]
class Comment < ApplicationRecord
end
6.1 Declarar delegated_type
Primero, declaremos un delegated_type
en la superclase Entry
.
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
end
El parámetro entryable
especifica el campo a utilizar para la delegación e incluye los tipos Message
y Comment
como las clases delegadas.
La clase Entry
tiene los campos entryable_type
y entryable_id
. Este es el campo con los sufijos _type
y _id
agregados al nombre entryable
en la definición de delegated_type
.
entryable_type
almacena el nombre de la subclase del delegado y entryable_id
almacena el id del registro de la subclase del delegado.
A continuación, debemos definir un módulo para implementar esos tipos delegados, declarando el parámetro as: :entryable
en la asociación has_one
.
module Entryable
extend ActiveSupport::Concern
included do
has_one :entry, as: :entryable, touch: true
end
end
Y luego incluir el módulo creado en su subclase.
class Message < ApplicationRecord
include Entryable
end
class Comment < ApplicationRecord
include Entryable
end
Con esta definición completa, nuestro delegador Entry
ahora proporciona los siguientes métodos:
Método | Retorno |
---|---|
Entry#entryable_class |
Message o Comment |
Entry#entryable_name |
"message" o "comment" |
Entry.messages |
Entry.where(entryable_type: "Message") |
Entry#message? |
Devuelve true cuando entryable_type == "Message" |
Entry#message |
Devuelve el registro de mensaje cuando entryable_type == "Message" , de lo contrario nil |
Entry#message_id |
Devuelve entryable_id cuando entryable_type == "Message" , de lo contrario nil |
Entry.comments |
Entry.where(entryable_type: "Comment") |
Entry#comment? |
Devuelve true cuando entryable_type == "Comment" |
Entry#comment |
Devuelve el registro de comentario cuando entryable_type == "Comment" , de lo contrario nil |
Entry#comment_id |
Devuelve entryable_id cuando entryable_type == "Comment" , de lo contrario nil |
6.2 Creación de objetos
Al crear un nuevo objeto Entry
, podemos especificar la subclase entryable
al mismo tiempo.
Entry.create! entryable: Message.new(subject: "¡Hola!")
6.3 Agregar más delegación
Podemos expandir nuestro delegador Entry
y mejorar aún más definiendo delegates
y utilizando polimorfismo en las subclases.
Por ejemplo, para delegar el método title
de Entry
a sus subclases:
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[ Message Comment ]
delegates :title, to: :entryable
end
class Message < ApplicationRecord
include Entryable
def title
subject
end
end
class Comment < ApplicationRecord
include Entryable
def title
content.truncate(20)
end
end
Comentarios
Se te anima a ayudar a mejorar la calidad de esta guía.
Por favor, contribuye si encuentras algún error tipográfico o factual. Para empezar, puedes leer nuestra contribución a la documentación sección.
También puedes encontrar contenido incompleto o desactualizado. Por favor, añade cualquier documentación faltante para main. Asegúrate de revisar Edge Guides primero para verificar si los problemas ya están resueltos o no en la rama principal. Consulta las Directrices de las Guías de Ruby on Rails para el estilo y las convenciones.
Si por alguna razón encuentras algo que corregir pero no puedes solucionarlo tú mismo, por favor abre un problema.
Y por último, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.