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 @ID
s 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 Int
s) 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.