Ga naar inhoud

Models

Modellen vertegenwoordigen gegevens die zijn opgeslagen in tabellen of verzamelingen in uw database. Modellen hebben één of meer velden die codeerbare waarden opslaan. Alle modellen hebben een unieke identifier. Property wrappers worden gebruikt om identifiers, velden, en relaties aan te duiden.

Hieronder staat een voorbeeld van een eenvoudig model met één veld. Merk op dat modellen niet het volledige databaseschema beschrijven, zoals constraints, indexen, en foreign keys. Schema's worden gedefinieerd in migraties. Modellen zijn gericht op het representeren van de gegevens die zijn opgeslagen in uw databaseschema's.

final class Planet: Model {
    // Naam van de tabel of verzameling.
    static let schema = "planets"

    // Unieke identificator voor deze planeet.
    @ID(key: .id)
    var id: UUID?

    // De naam van de Planeet.
    @Field(key: "name")
    var name: String

    // Maakt een nieuwe, lege Planeet aan.
    init() { }

    // Creëert een nieuwe planeet met alle eigenschappen ingesteld.
    init(id: UUID? = nil, name: String) {
        self.id = id
        self.name = name
    }
}

Schema

Alle modellen hebben een statische, get-only schema eigenschap nodig. Deze string verwijst naar de naam van de tabel of collectie die dit model vertegenwoordigt.

final class Planet: Model {
    // Naam van de tabel of verzameling.
    static let schema = "planets"
}

Bij het bevragen van dit model, zullen gegevens worden opgehaald uit en opgeslagen in het schema genaamd "planets".

Tip

De schema naam is typisch de klasse naam in meervoud en met kleine letters.

Identifier

Alle modellen moeten een id eigenschap hebben, gedefinieerd met de @ID eigenschap wrapper. Dit veld identificeert instanties van uw model op een unieke manier.

final class Planet: Model {
    // Unieke identificatiecode voor deze planeet.
    @ID(key: .id)
    var id: UUID?
}

Standaard moet de @ID eigenschap de speciale .id sleutel gebruiken die verwijst naar een sleutel voor de onderliggende database-driver. Voor SQL is dit "id" en voor NoSQL is dit "_id".

De @ID moet ook van het type UUID zijn. Dit is de enige identifier waarde die momenteel door alle database drivers wordt ondersteund. Fluent zal automatisch nieuwe UUID identifiers genereren wanneer modellen worden gemaakt.

@ID heeft een optionele waarde, omdat niet-opgeslagen modellen nog geen identifier kunnen hebben. Om de identifier te krijgen of een foutmelding te krijgen, gebruik requireID.

let id = try planet.requireID()

Exists

@ID heeft een exists eigenschap die weergeeft of het model bestaat in de database of niet. Wanneer u een model initialiseert, is de waarde false. Nadat je een model hebt opgeslagen of wanneer je een model ophaalt uit de database, is de waarde true. Deze eigenschap is muteerbaar.

if planet.$id.exists {
    // Dit model bestaat in de database.
}

Custom Identifier

Fluent ondersteunt aangepaste identifier sleutels en types door gebruik te maken van de @ID(custom:) overload.

final class Planet: Model {
    // Unieke identificatiecode voor deze planeet.
    @ID(custom: "foo")
    var id: Int?
}

Het bovenstaande voorbeeld gebruikt een @ID met aangepaste sleutel "foo" en identifier type Int. Dit is compatibel met SQL databases die auto-incrementing primary keys gebruiken, maar is niet compatibel met NoSQL.

Custom @IDs staan de gebruiker toe om te specificeren hoe de identifier moet worden gegenereerd met behulp van de generatedBy parameter.

@ID(custom: "foo", generatedBy: .user)

De generatedBy parameter ondersteunt deze gevallen:

Gegenereerd Door Beschrijving
.user De @ID eigenschap wordt verwacht ingesteld te zijn voordat een nieuw model wordt opgeslagen.
.random @ID waardetype moet voldoen aan RandomGeneratable.
.database Database wordt verwacht een waarde te genereren bij het opslaan.

If the generatedBy parameter is omitted, Fluent will attempt to infer an appropriate case based on the @ID value type. For example, Int will default to .database generation unless otherwise specified. Indien de generatedBy parameter is weggelaten, zal Fluent proberen een geschikt geval af te leiden op basis van het @ID waardetype. Bijvoorbeeld, Int zal standaard .database generatie gebruiken tenzij anders gespecificeerd.

Initializer

Modellen moeten een lege initialisatiemethode hebben.

final class Planet: Model {
    // Maakt een nieuwe, lege planeet aan.
    init() { }
}

Fluent heeft deze methode intern nodig om modellen te initialiseren die door queries worden geretourneerd. Het wordt ook gebruikt voor reflectie.

Misschien wilt u een gemakkelijke initializer aan uw model toevoegen die alle eigenschappen accepteert.

final class Planet: Model {
    // Maakt een nieuwe planeet aan met alle eigenschappen ingesteld.
    init(id: UUID? = nil, name: String) {
        self.id = id
        self.name = name
    }
}

Het gebruik van gemaksinitializers maakt het gemakkelijker om in de toekomst nieuwe eigenschappen aan het model toe te voegen.

Field

Modellen kunnen nul of meer @Field eigenschappen hebben voor het opslaan van gegevens.

final class Planet: Model {
    // De naam van de planeet.
    @Field(key: "name")
    var name: String
}

Voor velden moet de databasesleutel expliciet worden gedefinieerd. Deze hoeft niet dezelfde te zijn als de naam van de eigenschap.

Tip

Fluent raadt aan snake_case te gebruiken voor databasesleutels en camelCase voor eigenschapsnamen.

Field waarden kunnen elk type zijn dat voldoet aan Codable. Het opslaan van geneste structuren en arrays in @Field wordt ondersteund, maar filterbewerkingen zijn beperkt. Zie @Group voor een alternatief.

Voor velden die een optionele waarde bevatten, gebruik @OptionalField.

@OptionalField(key: "tag")
var tag: String?

Waarschuwing

Een niet-optioneel veld dat een willSet property observer heeft die verwijst naar zijn huidige waarde of een didSet property observer die verwijst naar zijn oldValue zal resulteren in een fatale fout.

Relaties

Modellen kunnen nul of meer relatie-eigenschappen hebben die verwijzen naar andere modellen zoals @Parent, @Children, en @Siblings. Leer meer over relaties in de relations sectie.

Timestamp

@Timestamp is een speciaal type @Field dat een Foundation.Date opslaat. Tijdstempels worden automatisch door Fluent ingesteld op basis van de gekozen trigger.

final class Planet: Model {
    // Wanneer deze planeet werd aangemaakt.
    @Timestamp(key: "created_at", on: .create)
    var createdAt: Date?

    // Wanneer deze Planeet voor het laatst is bijgewerkt.
    @Timestamp(key: "updated_at", on: .update)
    var updatedAt: Date?
}

@Timestamp ondersteunt de volgende triggers.

Trigger Beschrijving
.create Wordt ingesteld wanneer een nieuw model in de database wordt opgeslagen.
.update Wordt ingesteld wanneer een bestaand model wordt opgeslagen in de database.
.delete Ingesteld wanneer een model uit de database wordt verwijderd. Zie soft delete.

De datumwaarde van @Timestamp is optioneel en moet op nil worden gezet bij het initialiseren van een nieuw model.

Timestamp Formaat

Standaard zal @Timestamp een efficiënte datetime codering gebruiken, gebaseerd op uw database driver. U kunt aanpassen hoe de tijdstempel wordt opgeslagen in de database met behulp van de format parameter.

// Slaat een ISO 8601 geformatteerd tijdstempel op dat weergeeft
// wanneer dit model voor het laatst werd bijgewerkt.
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?

Merk op dat de bijbehorende migratie voor dit .iso8601 voorbeeld opslag in .string formaat zou vereisen.

.field("updated_at", .string)

De beschikbare tijdstempelformaten worden hieronder opgesomd.

Formaat Beschrijving Type
.default Gebruikt een efficiënte datetime codering voor een specifieke database. Date
.iso8601 ISO 8601 string. Ondersteund withMilliseconds parameter. String
.unix Seconden sinds Unix epoch, inclusief fractie. Double

Je kunt de ruwe timestamp waarde direct benaderen met de timestamp eigenschap.

// Stel handmatig de tijdstempelwaarde in op deze
// ISO 8601-geformatteerde @Timestamp.
model.$updatedAt.timestamp = "2020-06-03T16:20:14+00:00"

Soft Delete

Het toevoegen van een @Timestamp die gebruik maakt van de .delete trigger aan je model zal soft-deletion mogelijk maken.

final class Planet: Model {
    // Wanneer deze planeet werd verwijderd.
    @Timestamp(key: "deleted_at", on: .delete)
    var deletedAt: Date?
}

Soft-deleted modellen bestaan nog steeds in de database na verwijdering, maar zullen niet worden geretourneerd in queries.

Tip

U kunt handmatig een tijdstempel bij verwijderen instellen op een datum in de toekomst. Dit kan worden gebruikt als een vervaldatum.

Om te forceren dat een soft-deletable model uit de database wordt verwijderd, gebruikt u de force parameter in delete.

// Verwijdert uit de database zelfs als het model 
// soft deletable is. 
model.delete(force: true, on: database)

Om een soft-deleted model te herstellen, gebruik de restore methode.

// Wist de tijdstempel bij verwijdering, 
// zodat dit model in query's kan worden geretourneerd. 
model.restore(on: database)

Om soft-deleted modellen in een query op te nemen, gebruikt u withDeleted.

// Vangt alle planeten op, inclusief soft deleted planeten.
Planet.query(on: database).withDeleted().all()

Enum

@Enum is een speciaal type van @Field voor het opslaan van string representeerbare types als native database enums. Native database enums bieden een extra laag van type veiligheid aan uw database en kunnen performanter zijn dan raw enums.

// String representable, Codable enum voor diersoorten.
enum Animal: String, Codable {
    case dog, cat
}

final class Pet: Model {
    // Slaat het type dier op als een native database enum.
    @Enum(key: "type")
    var type: Animal
}

Alleen typen die voldoen aan RawRepresentable waarbij RawValue String is, zijn compatibel met @Enum. String backed enums voldoen standaard aan deze eis.

Om een optionele enum op te slaan, gebruik @OptionalEnum.

De database moet voorbereid zijn om via een migratie met enums om te gaan. Zie enum voor meer informatie.

Raw Enums

Elk enum dat ondersteund wordt door een Codable type, zoals String of Int, kan worden opgeslagen in @Field. Het zal worden opgeslagen in de database als de ruwe waarde.

Group

Met @Group kunt u een geneste groep van velden als een enkele eigenschap in uw model opslaan. In tegenstelling tot Codable structs opgeslagen in een @Field, zijn de velden in een @Group opvraagbaar. Fluent bereikt dit door @Group op te slaan als een platte structuur in de database.

Om een @Group te gebruiken, definieer eerst de geneste structuur die je wilt opslaan met het Fields protocol. Dit is vergelijkbaar met Model behalve dat er geen identifier of schema naam nodig is. Je kunt hier veel eigenschappen opslaan die Model ondersteunt zoals @Field, @Enum, of zelfs een andere @Group.

// Een huisdier met naam en diersoort.
final class Pet: Fields {
    // De naam van het huisdier.
    @Field(key: "name")
    var name: String

    // Het soort huisdier. 
    @Field(key: "type")
    var type: String

    // Maakt een nieuw, leeg huisdier aan.
    init() { }
}

Nadat je de velddefinitie hebt gemaakt, kun je deze gebruiken als waarde van een @Group eigenschap.

final class User: Model {
    // Het geneste huisdier van de gebruiker.
    @Group(key: "pet")
    var pet: Pet
}

De velden van een @Group zijn toegankelijk via dot-syntax.

let user: User = ...
print(user.pet.name) // String

U kunt geneste velden als normaal opvragen door gebruik te maken van punt-syntax op de eigenschap-wrappers.

User.query(on: database).filter(\.$pet.$name == "Zizek").all()

In de database, wordt @Group opgeslagen als een platte structuur met sleutels verbonden door _. Hieronder is een voorbeeld van hoe User er in de database uit zou zien.

id name pet_name pet_type
1 Tanner Zizek Cat
2 Logan Runa Dog

Codable

Modellen voldoen standaard aan Codable. Dit betekent dat u uw modellen kunt gebruiken met Vapor's content API door conformiteit aan het Content protocol toe te voegen.

extension Planet: Content { }

app.get("planets") { req async throws in 
    // Geef een array van alle planeten.
    try await Planet.query(on: req.db).all()
}

Bij het serialiseren naar / van Codable, zullen model eigenschappen hun variabele namen gebruiken in plaats van sleutels. Relaties zullen serialiseren als geneste structuren en alle eager geladen data zal worden meegenomen.

Data Transfer Object

De standaard Codable conformiteit van het model kan eenvoudig gebruik en prototyping eenvoudiger maken. Het is echter niet geschikt voor elk gebruik. Voor bepaalde situaties zult u een data transfer object (DTO) moeten gebruiken.

Tip

Een DTO is een afzonderlijk Codable type dat de datastructuur voorstelt die je wilt coderen of decoderen.

Ga uit van het volgende User model in de komende voorbeelden.

// Verkort gebruikersmodel ter referentie.
final class User: Model {
    @ID(key: .id)
    var id: UUID?

    @Field(key: "first_name")
    var firstName: String

    @Field(key: "last_name")
    var lastName: String
}

Een veel voorkomende toepassing van DTOs is het implementeren van PATCH verzoeken. Deze verzoeken bevatten alleen waarden voor velden die bijgewerkt moeten worden. Een poging om een Model direct uit zo'n verzoek te decoderen zou mislukken als een van de vereiste velden zou ontbreken. In het onderstaande voorbeeld zie je hoe een DTO wordt gebruikt om de request data te decoderen en een model te updaten.

// Structuur van PATCH /users/:id verzoek.
struct PatchUser: Decodable {
    var firstName: String?
    var lastName: String?
}

app.patch("users", ":id") { req async throws -> User in 
    // Decodeer de verzoekgegevens.
    let patch = try req.content.decode(PatchUser.self)
    // Haal de gewenste gebruiker uit de database.
    guard let user = try await User.find(req.parameters.get("id"), on: req.db) else {
        throw Abort(.notFound)
    }
    // Als er een voornaam is opgegeven, actualiseer die dan.
    if let firstName = patch.firstName {
        user.firstName = firstName
    }
    // Als er een nieuwe achternaam is opgegeven, actualiseer die dan.
    if let lastName = patch.lastName {
        user.lastName = lastName
    }
    // Sla de gebruiker op en stuur hem terug.
    try await user.save(on: req.db)
    return user
}

Een andere veel voorkomende use case voor DTOs is het aanpassen van het formaat van uw API antwoorden. Het onderstaande voorbeeld toont hoe een DTO kan worden gebruikt om een berekend veld aan een antwoord toe te voegen.

// Structuur van GET /users antwoord.
struct GetUser: Content {
    var id: UUID
    var name: String
}

app.get("users") { req async throws -> [GetUser] in 
    // Haal alle gebruikers op uit de database.
    let users = try await User.query(on: req.db).all()
    return try users.map { user in
        // Converteer elke gebruiker naar GET return type.
        try GetUser(
            id: user.requireID(),
            name: "\(user.firstName) \(user.lastName)"
        )
    }
}

Zelfs als de structuur van de DTO identiek is aan de Codable conformiteit van het model, kan het hebben als een apart type helpen om grote projecten netjes te houden. Als u ooit een wijziging moet aanbrengen in de eigenschappen van uw modellen, hoeft u zich geen zorgen te maken over het verbreken van de publieke API van uw app. U kunt ook overwegen om uw DTOs in een apart pakket te stoppen dat gedeeld kan worden met gebruikers van uw API.

Om deze redenen bevelen wij het gebruik van DTO's ten zeerste aan, waar mogelijk, vooral voor grote projecten.

Alias

Met het ModelAlias protocol kunt u een model uniek identificeren dat meerdere malen in een query wordt samengevoegd. Voor meer informatie, zie joins.

Save

Om een model op te slaan in de database, gebruik je de save(on:) methode.

planet.save(on: database)

Deze methode zal create of update intern aanroepen, afhankelijk van of het model al bestaat in de database.

Create

Je kunt de create methode aanroepen om een nieuw model in de database op te slaan.

let planet = Planet(name: "Earth")
planet.create(on: database)

create is ook beschikbaar op een array van modellen. Dit slaat alle modellen op in de database in een enkele batch / query.

// Voorbeeld van batchcreatie.
[earth, mars].create(on: database)

Waarschuwing

Modellen die gebruik maken van @ID(custom:) met de .database generator (meestal auto-incrementing Ints) zullen hun nieuw aangemaakte identifiers niet toegankelijk hebben na batch create. Voor situaties waarin je toegang tot de identifiers nodig hebt, roep create aan voor elk model.

Om een array van modellen afzonderlijk te maken, gebruik map + flatten.

[earth, mars].map { $0.create(on: database) }
    .flatten(on: database.eventLoop)

Als u async/await gebruikt kunt u als volgt te werk gaan:

await withThrowingTaskGroup(of: Void.self) { taskGroup in
    [earth, mars].forEach { model in
        taskGroup.addTask { try await model.create(on: database) }
    }
}

Update

Je kunt de update methode aanroepen om een model op te slaan dat is opgehaald uit de database.

guard let planet = try await Planet.find(..., on: database) else {
    throw Abort(.notFound)
}
planet.name = "Earth"
try await planet.update(on: database)

Om een array van modellen bij te werken, gebruik map + flatten.

[earth, mars].map { $0.update(on: database) }
    .flatten(on: database.eventLoop)

// TODO

Query

Modellen stellen een statische methode query(on:) beschikbaar die een query builder retourneert.

Planet.query(on: database).all()

Leer meer over query's in de query sectie.

Find

Modellen hebben een statische find(_:on:) methode om een model instantie op identifier op te zoeken.

Planet.find(req.parameters.get("id"), on: database)

Deze methode retourneert nil als er geen model met die identifier is gevonden.

Lifecycle

Met model middleware kunt u inhaken op de lifecycle-events van uw model. De volgende lifecycle-events worden ondersteund.

Methode Beschrijving
create Wordt uitgevoerd voordat een model wordt gemaakt.
update Wordt uitgevoerd voordat een model wordt bijgewerkt.
delete(force:) Wordt uitgevoerd voordat een model wordt verwijderd.
softDelete Wordt uitgevoerd voordat een model soft deleted wordt.
restore Wordt uitgevoerd voordat een model wordt hersteld (tegenovergestelde van soft delete).

Model middleware worden gedeclareerd met het ModelMiddleware of AsyncModelMiddleware protocol. Alle lifecycle methodes hebben een standaard implementatie, zodat u alleen de methodes hoeft te implementeren die u nodig heeft. Elke methode accepteert het model in kwestie, een verwijzing naar de database, en de volgende actie in de keten. De middleware kan kiezen om vroegtijdig terug te keren, een mislukte future terug te sturen, of de volgende actie aan te roepen om normaal verder te gaan.

Met behulp van deze methoden kun je acties uitvoeren zowel voor als na het voltooien van de specifieke gebeurtenis. Het uitvoeren van acties nadat de gebeurtenis is voltooid kan worden gedaan door de toekomst in kaart te brengen die door de volgende responder wordt geretourneerd.

// Voorbeeld middleware die namen met hoofdletters schrijft.
struct PlanetMiddleware: ModelMiddleware {
    func create(model: Planet, on db: Database, next: AnyModelResponder) -> EventLoopFuture<Void> {
        // Het model kan hier worden gewijzigd voordat het wordt gemaakt.
        model.name = model.name.capitalized()
        return next.create(model, on: db).map {
            // Zodra de planeet is gecreëerd, zal de code 
            // hier worden uitgevoerd.
            print ("Planet \(model.name) was created")
        }
    }
}

of als je async/await gebruikt:

struct PlanetMiddleware: AsyncModelMiddleware {
    func create(model: Planet, on db: Database, next: AnyAsyncModelResponder) async throws {
        // Het model kan hier worden gewijzigd voordat het wordt gemaakt.
        model.name = model.name.capitalized()
        try await next.create(model, on: db)
        // Zodra de planeet is gecreëerd, zal de code 
        // hier worden uitgevoerd.
        print ("Planet \(model.name) was created")
    }
}

Nadat u uw middleware heeft aangemaakt, kunt u deze inschakelen met app.databases.middleware.

// Voorbeeld van het configureren van model middleware.
app.databases.middleware.use(PlanetMiddleware(), on: .psql)

Database Space

Fluent ondersteunt het instellen van een ruimte voor een model, waardoor individuele Fluent-modellen kunnen worden gepartitioneerd tussen PostgreSQL-schema's, MySQL-databases, en meerdere gekoppelde SQLite-databases. MongoDB ondersteunt op het moment van schrijven nog geen ruimtes. Om een model in een andere ruimte dan de standaardruimte te plaatsen, voegt u een nieuwe statische eigenschap aan het model toe:

public static let schema = "planets"
public static let space: String? = "mirror_universe"

// ...

Fluent zal dit gebruiken bij het bouwen van alle database queries.