Ga naar inhoud

Relaties

Fluent's model API helpt u bij het maken en onderhouden van verwijzingen tussen uw modellen door middel van relaties. Drie typen relaties worden ondersteund:

Parent

De @Parent relatie slaat een verwijzing op naar de @ID eigenschap van een ander model.

final class Planet: Model {
    // Voorbeeld van een parent relatie.
    @Parent(key: "star_id")
    var star: Star
}

@Parent bevat een @Field genaamd id welke wordt gebruikt voor het instellen en bijwerken van de relatie.

// Stel parent relatie id in
earth.$star.id = sun.id

Bijvoorbeeld, de Planet initializer zou er zo uitzien:

init(name: String, starID: Star.IDValue) {
    self.name = name
    // ...
    self.$star.id = starID
}

De key parameter definieert de veldsleutel om te gebruiken voor het opslaan van de identifier van de parent. Ervan uitgaande dat Star een UUID identifier heeft, is deze @Parent relatie compatibel met de volgende veld definitie.

.field("star_id", .uuid, .required, .references("star", "id"))

Merk op dat de .references constraint optioneel is. Zie schema voor meer informatie.

Optionele Parent

De @OptionalParent relatie slaat een optionele verwijzing op naar de @ID eigenschap van een ander model. Het werkt hetzelfde als @Parent maar staat toe dat de relatie nil is.

final class Planet: Model {
    // Voorbeeld van een optionele parent relatie.
    @OptionalParent(key: "star_id")
    var star: Star?
}

De velddefinitie is gelijk aan die van @Parent, behalve dat de .required restrictie moet worden weggelaten.

.field("star_id", .uuid, .references("star", "id"))

Optionele Child

De @OptionalChild eigenschap creëert een één-op-één relatie tussen de twee modellen. Het slaat geen waarden op in het root model.

final class Planet: Model {
    // Voorbeeld van een optionele child relatie.
    @OptionalChild(for: \.$planet)
    var governor: Governor?
}

De for parameter accepteert een sleutelpad naar een @Parent of @OptionalParent relatie die verwijst naar het root model.

Een nieuw model kan aan deze relatie worden toegevoegd met de create methode.

// Voorbeeld van het toevoegen van een nieuw model aan een relatie.
let jane = Governor(name: "Jane Doe")
try await mars.$governor.create(jane, on: database)

Dit zal de parent id op het child model automatisch instellen.

Aangezien deze relatie geen waarden opslaat, is er geen databaseschema-item vereist voor het basismodel.

Het één-op-één-karakter van de relatie moet in het schema van het child-model worden afgedwongen met een .unique restrictie op de kolom die naar het parent-model verwijst.

try await database.schema(Governor.schema)
    .id()
    .field("name", .string, .required)
    .field("planet_id", .uuid, .required, .references("planets", "id"))
    // Voorbeeld van unieke beperking
    .unique(on: "planet_id")
    .create()

Waarschuwing

Het weglaten van de unieke beperking op het parent ID veld uit het schema van de klant kan leiden tot onvoorspelbare resultaten. Als er geen uniciteits beperking is, kan de child tabel meer dan een child rij bevatten voor een gegeven parent; in dit geval zal een @OptionalChild eigenschap nog steeds slechts toegang hebben tot één child per keer, met geen enkele manier om te controleren welk child geladen wordt. Als je meerdere child rijen voor een bepaalde parent moet opslaan, gebruik dan @Children.

Children

De @Children eigenschap creëert een één-op-veel relatie tussen twee modellen. Het slaat geen waarden op in het root model.

final class Star: Model {
    // Voorbeeld van een children relatie.
    @Children(for: \.$star)
    var planets: [Planet]
}

De for parameter accepteert een sleutelpad naar een @Parent of @OptionalParent relatie die verwijst naar het root model. In dit geval verwijzen we naar de @Parent relatie uit het vorige voorbeeld.

Nieuwe modellen kunnen aan deze relatie worden toegevoegd met de create methode.

// Voorbeeld van het toevoegen van een nieuw model aan een relatie.
let earth = Planet(name: "Earth")
try await sun.$planets.create(earth, on: database)

Dit zal de parent id op het child model automatisch instellen.

Aangezien deze relatie geen waarden opslaat, is er geen invoer in het databaseschema vereist.

Siblings

De @Siblings eigenschap creëert een veel-op-veel relatie tussen twee modellen. Het doet dit door middel van een tertiair model, een pivot genaamd.

Laten we eens kijken naar een voorbeeld van een veel-op-veel relatie tussen een Planet en een Tag.

enum PlanetTagStatus: String, Codable { case accepted, pending }

// Voorbeeld van een pivot model.
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
    }
}

Elk model dat tenminste twee @Parent relaties bevat, één voor elk model dat gerelateerd moet worden, kan gebruikt worden als pivot. Het model kan aanvullende eigenschappen bevatten, zoals zijn ID, en kan zelfs andere @Parent relaties bevatten.

Het toevoegen van een unieke constraint aan het pivot model kan helpen om overbodige entries te voorkomen. Zie schema voor meer informatie.

// Staat duplicaat relaties niet toe.
.unique(on: "planet_id", "tag_id")

Zodra de pivot is gemaakt, gebruik de @Siblings eigenschap om de relatie te maken.

final class Planet: Model {
    // Voorbeeld van een siblings relatie.
    @Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
    public var tags: [Tag]
}

De @Siblings eigenschap vereist drie parameters:

  • through: Het type van het pivot model.
  • from: Sleutelpad van de pivot naar de parent-relatie die verwijst naar het basismodel.
  • to: Sleutelpad van de pivot naar de parent relation die naar het gerelateerde model verwijst.

De inverse @Siblings eigenschap op het verwante model maakt de relatie compleet.

final class Tag: Model {
    // Voorbeeld van een siblings relatie.
    @Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet)
    public var planets: [Planet]
}

Siblings Attach

De @Siblings eigenschap heeft methoden voor het toevoegen en verwijderen van modellen uit de relatie.

Gebruik de attach() methode om een enkel model of een array van modellen toe te voegen aan de relatie. Pivot modellen worden indien nodig automatisch aangemaakt en opgeslagen. Er kan een callback closure worden gespecificeerd om aanvullende eigenschappen van elke gecreëerde pivot in te vullen:

let earth: Planet = ...
let inhabited: Tag = ...
// Voegt het model toe aan de relatie.
try await earth.$tags.attach(inhabited, on: database)
// Vul de pivot attributen in bij het maken van de relatie.
try await earth.$tags.attach(inhabited, on: database) { pivot in
    pivot.comments = "This is a life-bearing planet."
    pivot.status = .accepted
}
// Voeg meerdere modellen met attributen toe aan de relatie.
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
}

Bij het koppelen van een enkel model, kunt u de method parameter gebruiken om te kiezen of de relatie wel of niet gecontroleerd moet worden voor het opslaan.

// Koppelt alleen als de relatie nog niet bestaat.
try await earth.$tags.attach(inhabited, method: .ifNotExists, on: database)

Gebruik de detach methode om een model uit de relatie te verwijderen. Dit verwijdert het corresponderende pivot model.

// Verwijdert het model uit de relatie.
try await earth.$tags.detach(inhabited, on: database)

Je kunt controleren of een model gerelateerd is of niet met de isAttached methode.

// Controleert of de modellen verwant zijn.
earth.$tags.isAttached(to: inhabited)

Get

Gebruik de get(on:) methode om de waarde van een relatie op te halen.

// Haalt alle planeten van de zon op.
sun.$planets.get(on: database).map { planets in
    print(planets)
}

// Of

let planets = try await sun.$planets.get(on: database)
print(planets)

Gebruik de reload parameter om te kiezen of de relatie wel of niet opnieuw moet worden opgehaald uit de database als deze al eerder is geladen.

try await sun.$planets.get(reload: true, on: database)

Query

Gebruik de query(on:) methode op een relatie om een query builder te maken voor de gerelateerde modellen.

// Haal alle planeten van de zon op die een naam hebben die begint met M.
try await sun.$planets.query(on: database).filter(\.$name =~ "M").all()

Zie query voor meer informatie.

Eager Loading

Fluent's query bouwer maakt het mogelijk om de relaties van een model vooraf te laden wanneer het wordt opgehaald uit de database. Dit wordt eager loading genoemd en stelt u in staat om synchroon relaties te benaderen zonder dat u eerst load of get hoeft aan te roepen.

Om een relatie eager te laden, geef je een sleutelpad naar de relatie door aan de with methode op query builder.

// Voorbeeld van eager loading.
Planet.query(on: database).with(\.$star).all().map { planets in
    for planet in planets {
        // `star` is hier synchroon toegankelijk 
        // omdat het eager geladen is.
        print(planet.star.name)
    }
}

// Of

let planets = try await Planet.query(on: database).with(\.$star).all()
for planet in planets {
    // `star` is hier synchroon toegankelijk
    // omdat het eager geladen is.
    print(planet.star.name)
}

In het bovenstaande voorbeeld wordt een sleutelpad naar de @Parent relatie genaamd star doorgegeven aan with. Dit zorgt ervoor dat de query bouwer een extra query uitvoert nadat alle planeten zijn geladen om al hun gerelateerde sterren op te halen. De sterren zijn dan synchroon toegankelijk via de @Parent eigenschap.

Elke relatie die eager wordt geladen vereist slechts één extra query, ongeacht hoeveel modellen er worden geretourneerd. Eager loading is alleen mogelijk met de all en first methodes van query builder.

Nested Eager Load

Met de with methode van de query bouwer kunnen relaties eager geladen worden op het model waarop de query betrekking heeft. U kunt echter ook relaties op gerelateerde modellen eager laden.

let planets = try await Planet.query(on: database).with(\.$star) { star in
    star.with(\.$galaxy)
}.all()
for planet in planets {
    // `star` is hier synchroon toegankelijk
    // omdat het eager geladen is.
    print(planet.star.galaxy.name)
}

De with methode accepteert een optionele closure als tweede parameter. Deze closure accepteert een eager load builder voor de gekozen relatie. Er is geen limiet aan hoe diep eager loading genest kan worden.

Lazy Eager Loading

In het geval dat u het bovenliggende model al heeft opgehaald en u wilt een van zijn relaties laden, dan kunt u de load(on:) methode voor dat doel gebruiken. Hiermee wordt het gerelateerde model opgehaald uit de database en kan het worden benaderd als een lokale eigenschap.

planet.$star.load(on: database).map {
    print(planet.star.name)
}

// Of

try await planet.$star.load(on: database)
print(planet.star.name)

Om te controleren of een relatie al dan niet geladen is, gebruik je de value eigenschap.

if planet.$star.value != nil {
    // Relatie is geladen.
    print(planet.star.name)
} else {
    // De relatie is niet geladen.
    // Pogingen om toegang te krijgen tot planet.star zullen mislukken.
}

Als u het gerelateerde model al in een variabele heeft, kunt u de relatie handmatig instellen met behulp van de value eigenschap die hierboven is genoemd.

planet.$star.value = star

Dit zal het gerelateerde model aan de ouder koppelen alsof het eager loaded of lazy loaded was zonder een extra database query.