Relaciones¶
La API de modelo de Fluent te ayuda a crear y mantener referencias entre tus modelos mediante relaciones. Estos son los tres tipos de relaciones soportados:
Parent¶
La relación @Parent
guarda una referencia a la propiedad @ID
de otro modelo.
final class Planet: Model {
// Ejemplo de relación parent.
@Parent(key: "star_id")
var star: Star
}
@Parent
contiene un @Field
llamado id
usado para establecer y actualizar la relación.
// Establece el id de la relación parent
earth.$star.id = sun.id
Por ejemplo, el inicializador de Planet
se vería de la siguiente manera:
init(name: String, starID: Star.IDValue) {
self.name = name
// ...
self.$star.id = starID
}
El parámetro key
define la clave del campo en el que se guarda el identificador del parent. Asumiendo que Star
tiene un identificador UUID
, esta relación @Parent
es compatible con la siguiente definición de campo.
.field("star_id", .uuid, .required, .references("star", "id"))
Cabe destacar que la restricción (constraint) .references
es opcional. Ver schema para más información.
Optional Parent¶
La relación @OptionalParent
guarda una referencia opcional a la propiedad @ID
de otro modelo. Funciona de manera similar a @Parent
pero permite que la relación sea nil
.
final class Planet: Model {
// Ejemplo de una relación parent opcional.
@OptionalParent(key: "star_id")
var star: Star?
}
La definición del campo es similar a la de @Parent
, excepto la constraint .required
, que debe ser omitida.
.field("star_id", .uuid, .references("star", "id"))
Optional Child¶
La propiedad @OptionalChild
crea una relación uno a uno entre dos modelos. No guarda ningún valor en el modelo raíz.
final class Planet: Model {
// Ejemplo de una relación optional child.
@OptionalChild(for: \.$planet)
var governor: Governor?
}
El parámetro for
acepta un key path hacia una relación @Parent
o @OptionalParent
referenciando el modelo raíz.
Puede añadirse un nuevo modelo a esta relación usando el método create
.
// Ejemplo de añadir un nuevo modelo a una relación.
let jane = Governor(name: "Jane Doe")
try await mars.$governor.create(jane, on: database)
Esto establecerá de manera automática el identificador (id) del parent en el modelo child.
Dado que esta relación no guarda valores, no se necesita especificar un esquema de base de datos para el modelo raíz.
La naturaleza "uno a uno" de la relación debe hacerse patente en el esquema del modelo child usando una constraint .unique
en la columna que haga referencia al modelo parent.
try await database.schema(Governor.schema)
.id()
.field("name", .string, .required)
.field("planet_id", .uuid, .required, .references("planets", "id"))
// Ejemplo de una restricción única
.unique(on: "planet_id")
.create()
Advertencia
Omitir la restricción de unicidad en el campo del identificador del parent en el esquema del cliente puede ocasionar resultados impredecibles.
Si no hay una restricción de unicidad, la tabla child puede llegar a contener más de una fila child para un solo parent; en este caso, una propiedad @OptionalChild
seguirá pudiendo acceder a un único child, sin manera de controlar cuál de ellos carga. Si necesitaras guardas varias filas child para un único parent, usa @Children
.
Children¶
La propiedad @Children
crea una relación uno a muchos entre dos modelos. No guarda ningún valor en el modelo raíz.
final class Star: Model {
// Ejemplo de una relación children.
@Children(for: \.$star)
var planets: [Planet]
}
El parámetro for
acepta un key path a una relación @Parent
o @OptionalParent
referenciando el modelo raíz. En este caso, estamos referenciando la relación @Parent
del anterior ejemplo.
Puede añadirse un nuevo modelo a esta relación usando el método create
.
// Ejemplo de añadir un nuevo modelo a una relación.
let earth = Planet(name: "Earth")
try await sun.$planets.create(earth, on: database)
Esto establecerá de manera automática el identificador (id) del parent en el modelo child.
Dado que esta relación no guarda valores, no se necesita especificar un esquema de base de datos.
Siblings¶
La propiedad @Siblings
crea una relación muchos a muchos entre dos modelos. Lo hace mediante un modelo terciario llamado pivote.
Echemos un vistazo a un ejemplo de relación muchos a muchos entre un Planet
y una Tag
.
enum PlanetTagStatus: String, Codable { case accepted, pending }
// Ejemplo de modelo pivote.
final class PlanetTag: Model {
static let schema = "planet+tag"
@ID(key: .id)
var id: UUID?
@Parent(key: "planet_id")
var planet: Planet
@Parent(key: "tag_id")
var tag: Tag
@OptionalField(key: "comments")
var comments: String?
@OptionalEnum(key: "status")
var status: PlanetTagStatus?
init() { }
init(id: UUID? = nil, planet: Planet, tag: Tag, comments: String?, status: PlanetTagStatus?) throws {
self.id = id
self.$planet.id = try planet.requireID()
self.$tag.id = try tag.requireID()
self.comments = comments
self.status = status
}
}
Cualquier modelo que incluya al menos dos relaciones @Parent
referenciando a sus respectivos modelos puede usarse como pivote. El modelo puede contener propiedades adicionales como su propio ID, e inclusive otras relaciones @Parent
.
Añadir una restricción de unicidad al modelo pivote puede ayudar a prevenir entradas redundantes. Ver schema para más información.
// No permite relaciones duplicadas.
.unique(on: "planet_id", "tag_id")
Una vez el pivote está creado, usa la propiedad @Siblings
para crear la relación.
final class Planet: Model {
// Ejemplo de relación sibling.
@Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
public var tags: [Tag]
}
La propiedad @Siblings
requiere de tres parámetros:
through
: El tipo del modelo pivote.from
: El key path del pivote a la relación parent referenciando el modelo raíz.to
: El key path del pivote a la relación parent referenciando el modelo conectado.
La propiedad @Siblings
inversa en el modelo conectado completa la relación.
final class Tag: Model {
// Ejemplo de una relación sibling.
@Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet)
public var planets: [Planet]
}
Añadir a Siblings¶
La propiedad @Siblings
tiene métodos para añadir o quitar modelos de la relación.
Usa el método attach()
para añadir un modelo o una colección (array) de modelos a la relación. Los modelos pivote son creados y guardados de manera automática según sea necesario. Un closure de callback puede especificarse para poblar propiedades adicionales de cada pivote creado:
let earth: Planet = ...
let inhabited: Tag = ...
// Añadir el modelo a la relación.
try await earth.$tags.attach(inhabited, on: database)
// Poblar los atributos del pivote al establecer la relación.
try await earth.$tags.attach(inhabited, on: database) { pivot in
pivot.comments = "This is a life-bearing planet."
pivot.status = .accepted
}
// Añadir varios modelos con atributos a la relación.
let volcanic: Tag = ..., oceanic: Tag = ...
try await earth.$tags.attach([volcanic, oceanic], on: database) { pivot in
pivot.comments = "This planet has a tag named \(pivot.$tag.name)."
pivot.status = .pending
}
Si estás añadiendo un solo modelo, puedes usar el parámetro method
para indicar si la relación debería ser revisada o no antes del guardado.
// Solo añade si la relación no es ya existente.
try await earth.$tags.attach(inhabited, method: .ifNotExists, on: database)
Usa el método detach
para eliminar un modelo de la relación. Esto borra el modelo pivote correspondiente.
// Elimina el modelo de la relación.
try await earth.$tags.detach(inhabited, on: database)
Puedes comprobar que un modelo esté conectado o no usando el método isAttached
.
// Comprueba que los modelos estén conectados.
earth.$tags.isAttached(to: inhabited)
Get¶
Usa el método get(on:)
para recuperar el valor de una relación.
// Recupera todos los planetas del sol.
sun.$planets.get(on: database).map { planets in
print(planets)
}
// O
let planets = try await sun.$planets.get(on: database)
print(planets)
Usa el parámetro reload
para indicar si la relación debería ser recuperada de nuevo o no de la base de datos si ya ha sido cargada.
try await sun.$planets.get(reload: true, on: database)
Query¶
Usa el método query(on:)
en una relación para crear un constructor de consulta para los modelos conectados.
// Recupera todos los planetas del sol cuyo nombre empiece con M.
try await sun.$planets.query(on: database).filter(\.$name =~ "M").all()
Ver query para más información.
Eager Loading¶
El constructor de consultas (query builder) de Fluent te permite precargar las relaciones de un modelo cuando es recuperado de la base de datos. Esto se conoce como "eager loading" y te permite acceder a las relaciones de manera sincrónica sin la necesidad de llamar previamente a load
o get
.
Para hacer un "eager load" de una relación, pasa un key path a la relación con el método with
en el constructor de consultas.
// Ejemplo de eager loading.
Planet.query(on: database).with(\.$star).all().map { planets in
for planet in planets {
// `star` es accesible de manera sincrónica aquí
// dado que ha sido precargada.
print(planet.star.name)
}
}
// O
let planets = try await Planet.query(on: database).with(\.$star).all()
for planet in planets {
// `star` es accesible de manera sincrónica aquí
// dado que ha sido precargada.
print(planet.star.name)
}
En el ejemplo anterior, se le ha pasado un key path a la relación @Parent
llamada star
con with
. Esto provoca que el constructor de consultas haga una consulta adicional después de cargar todos los planetas para recuperar todas las estrellas conectadas a éstos. Las estrellas son accesibles de manera sincrónica mediante la propiedad @Parent
.
Cada relación precargada (eager loaded) necesita una única consulta adicional, sin importar cuántos modelos se hayan devuelto. La precarga (eager loading) sólo es posible con los métodos de constructor de consultas all
y first
.
Nested Eager Load¶
El método de constructor de consultas with
te permite precargar relaciones en el modelo que está siendo consultado. Sin embargo, también puedes precargar relaciones en los modelos conectados.
let planets = try await Planet.query(on: database).with(\.$star) { star in
star.with(\.$galaxy)
}.all()
for planet in planets {
// `star.galaxy` es accesible de manera sincrónica aquí
// dado que ha sido precargada.
print(planet.star.galaxy.name)
}
El método with
acepta un closure opcional como segundo parámetro. Este closure acepta un constructor de precarga para la relación elegida. No hay límite de profundidad en el anidado de precargas.
Lazy Eager Loading¶
En caso de que ya hayas recuperado el modelo del parent y quieres cargar una de sus relaciones, puedes usar el método load(on:)
para hacerlo. Esto recuperará el modelo conectado de la base de datos y permitirá acceder a él como una propiedad local.
planet.$star.load(on: database).map {
print(planet.star.name)
}
// O
try await planet.$star.load(on: database)
print(planet.star.name)
Para comprobar si una relación se ha cargado, usa la propiedad value
.
if planet.$star.value != nil {
// La relación se ha cargado.
print(planet.star.name)
} else {
// La relación no se ha cargado.
// Intentar acceder a planet.star fallará.
}
Si ya tienes el modelo conectado en una variable, puedes establecer la relación manualmente usando la propiedad value
mencionada anteriormente.
planet.$star.value = star
Esto unirá el modelo conectado al modelo parent como si hubiera sido cargado como "eager loaded" o como "lazy loaded", sin necesitar una consulta extra a la base de datos.