Ga naar inhoud

Fluent

Fluent is een ORM framework voor Swift. Het maakt gebruik van het sterke type systeem van Swift om een eenvoudig te gebruiken interface voor je database te bieden. Het gebruik van Fluent draait om het maken van modeltypes die gegevensstructuren in je database representeren. Deze modellen worden vervolgens gebruikt om te creëren, lezen, updaten en verwijderen operaties uit te voeren in plaats van het schrijven van ruwe queries.

Configuratie

Wanneer u een project aanmaakt met vapor new, antwoord dan "yes" bij het includeren van Fluent en kies welke database driver u wilt gebruiken. Dit zal automatisch de dependencies toevoegen aan uw nieuwe project, evenals voorbeeld configuratie code.

Bestaand Project

Als u een bestaand project heeft waaraan u Fluent wilt toevoegen, dan moet u twee afhankelijkheden toevoegen aan uw package:

  • vapor/fluent@4.0.0
  • Een (of meer) Fluent driver(s) van uw keuze
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-<db>-driver.git", from: <version>),
.target(name: "App", dependencies: [
    .product(name: "Fluent", package: "fluent"),
    .product(name: "Fluent<db>Driver", package: "fluent-<db>-driver"),
    .product(name: "Vapor", package: "vapor"),
]),

Zodra de pakketten zijn toegevoegd als dependencies, kunt u uw databases configureren met app.databases in configure.swift.

import Fluent
import Fluent<db>Driver

app.databases.use(<db config>, as: <identifier>)

Elk van de Fluent drivers hieronder heeft meer specifieke instructies voor configuratie.

Drivers

Fluent heeft momenteel vier officieel ondersteunde stuurprogramma's. U kunt op GitHub zoeken naar de tag fluent-driver voor een volledige lijst van officiële en third-party Fluent database drivers.

PostgreSQL

PostgreSQL is een open-source SQL-database die voldoet aan de standaarden. Het is gemakkelijk te configureren op de meeste cloud hosting providers. Dit is de aanbevolen database driver van Fluent.

Om PostgreSQL te gebruiken, voeg de volgende dependencies toe aan uw pakket.

.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0")
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver")

Als de afhankelijkheden zijn toegevoegd, configureer dan de database referenties met Fluent met app.databases.use in configure.swift.

import Fluent
import FluentPostgresDriver

app.databases.use(.postgres(hostname: "localhost", username: "vapor", password: "vapor", database: "vapor"), as: .psql)

Je kunt ook de credentials uit een database connection string halen.

try app.databases.use(.postgres(url: "<connection string>"), as: .psql)

SQLite

SQLite is een open source, ingebedde SQL database. Zijn simplistische aard maakt het een geweldige kandidaat voor prototyping en testen.

Om SQLite te gebruiken, moet u de volgende afhankelijkheden aan uw pakket toevoegen.

.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0")
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver")

Als de afhankelijkheden zijn toegevoegd, configureer dan de database met Fluent met app.databases.use in configure.swift.

import Fluent
import FluentSQLiteDriver

app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)

U kunt SQLite ook configureren om de database vluchtig in het geheugen op te slaan.

app.databases.use(.sqlite(.memory), as: .sqlite)

Als u een in-memory database gebruikt, zorg er dan voor dat Fluent automatisch migreert door --auto-migrate te gebruiken of app.autoMigrate() uit te voeren na het toevoegen van migraties.

app.migrations.add(CreateTodo())
try app.autoMigrate().wait()
// of
try await app.autoMigrate()

Tip

De SQLite configuratie schakelt automatisch foreign key constraints in op alle gemaakte verbindingen, maar verandert niets aan de foreign key configuraties in de database zelf. Het direct verwijderen van records in een database kan een overtreding zijn van foreign key constraints en triggers.

MySQL

MySQL is een populaire open source SQL-database. Het is beschikbaar op veel cloud hosting providers. Dit stuurprogramma ondersteunt ook MariaDB.

Om MySQL te gebruiken, moet u de volgende afhankelijkheden aan uw pakket toevoegen.

.package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.0.0")
.product(name: "FluentMySQLDriver", package: "fluent-mysql-driver")

Als de afhankelijkheden zijn toegevoegd, configureer dan de database referenties met Fluent met app.databases.use in configure.swift.

import Fluent
import FluentMySQLDriver

app.databases.use(.mysql(hostname: "localhost", username: "vapor", password: "vapor", database: "vapor"), as: .mysql)

Je kunt ook de credentials uit een database connection string halen.

try app.databases.use(.mysql(url: "<connection string>"), as: .mysql)

Om een lokale verbinding te configureren zonder tussenkomst van een SSL certificaat, moet u de certificaatverificatie uitschakelen. U zou dit bijvoorbeeld moeten doen als u verbinding maakt met een MySQL 8 database in Docker.

var tls = TLSConfiguration.makeClientConfiguration()
tls.certificateVerification = .none

app.databases.use(.mysql(
    hostname: "localhost",
    username: "vapor",
    password: "vapor",
    database: "vapor",
    tlsConfiguration: tls
), as: .mysql)

Waarschuwing

Schakel certificaat verificatie niet uit in productie. U moet een certificaat verstrekken aan de TLSConfiguration om tegen te verifiëren.

MongoDB

MongoDB is een populaire schemaloze NoSQL database ontworpen voor programmeurs. Het stuurprogramma ondersteunt alle cloudhostingproviders en zelf gehoste installaties vanaf versie 3.4 en hoger.

Opmerking

Deze driver wordt aangedreven door een gemeenschap gemaakt en onderhouden MongoDB-client genaamd MongoKitten. MongoDB onderhoudt een officiële client, mongo-swift-driver, samen met een Vapor integratie, mongodb-vapor.

Om MongoDB te gebruiken, voeg de volgende afhankelijkheden toe aan je pakket.

.package(url: "https://github.com/vapor/fluent-mongo-driver.git", from: "1.0.0"),
.product(name: "FluentMongoDriver", package: "fluent-mongo-driver")

Als de afhankelijkheden zijn toegevoegd, configureer dan de database referenties met Fluent met app.databases.use in configure.swift.

Om verbinding te maken, geeft u een verbindingsstring door in het standaardformaat van MongoDB connection URI format.

import Fluent
import FluentMongoDriver

try app.databases.use(.mongo(connectionString: "<connection string>"), as: .mongo)

Modellen

Modellen vertegenwoordigen vaste gegevensstructuren in uw database, zoals tabellen of verzamelingen. Modellen hebben één of meer velden die codeerbare waarden opslaan. Alle modellen hebben ook een unieke identifier. Property wrappers worden gebruikt om identifiers en velden aan te duiden, alsook meer complexe mappings die later worden vermeld. Kijk eens naar het volgende model dat een sterrenstelsel voorstelt.

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

    // Unieke identificatiecode voor dit melkwegstelsel.
    @ID(key: .id)
    var id: UUID?

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

    // Creëert een nieuwe, lege Galaxy.
    init() { }

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

Om een nieuw model te maken, maak een nieuwe klasse die voldoet aan Model.

Tip

Het wordt aanbevolen om model klassen final te markeren om de performance te verbeteren en de conformance eisen te vereenvoudigen.

De eerste vereiste van het Model protocol is de statische string schema.

static let schema = "galaxies"

Deze eigenschap vertelt Fluent met welke tabel of collectie het model correspondeert. Dit kan een tabel zijn die al bestaat in de database of een tabel die u gaat aanmaken met een migratie. Het schema is meestal snake_case en meervoud.

Identifier

De volgende vereiste is een identifier veld genaamd id.

@ID(key: .id)
var id: UUID?

Dit veld moet de @ID property wrapper gebruiken. Fluent raadt aan om UUID en de speciale .id veldsleutel te gebruiken omdat dit compatibel is met alle Fluent drivers.

Als je een aangepaste ID sleutel of type wilt gebruiken, gebruik dan de @ID(custom:) overload.

Fields

Nadat de identifier is toegevoegd, kun je zoveel velden toevoegen als je wilt om extra informatie op te slaan. In dit voorbeeld is het enige extra veld de naam van het sterrenstelsel.

@Field(key: "name")
var name: String

Voor eenvoudige velden wordt de @Field property wrapper gebruikt. Net als @ID, specificeert de key parameter de naam van het veld in de database. Dit is vooral handig voor gevallen waar de database veld naamgeving conventie anders kan zijn dan in Swift, bijvoorbeeld het gebruik van snake_case in plaats van camelCase.

Vervolgens hebben alle modellen een lege init nodig. Dit stelt Fluent in staat om nieuwe instanties van het model te maken.

init() { }

Tenslotte kun je een gemakkelijke init voor je model toevoegen die al zijn eigenschappen instelt.

init(id: UUID? = nil, name: String) {
    self.id = id
    self.name = name
}

Het gebruik van convenience inits is vooral nuttig als je nieuwe eigenschappen aan je model toevoegt, omdat je compile-time fouten kunt krijgen als de init methode verandert.

Migraties

Als uw database voorgedefinieerde schema's gebruikt, zoals SQL databases, hebt u een migratie nodig om de database voor te bereiden op uw model. Migraties zijn ook nuttig voor het seeden van databases met data. Om een migratie te maken, definieert u een nieuw type dat voldoet aan het Migration of AsyncMigration protocol. Kijk eens naar de volgende migratie voor het eerder gedefinieerde Galaxy model.

struct CreateGalaxy: AsyncMigration {
    // Bereidt de database voor op het opslaan van Galaxy modellen.
    func prepare(on database: Database) async throws {
        try await database.schema("galaxies")
            .id()
            .field("name", .string)
            .create()
    }

    // Zet optioneel de wijzigingen terug die in de prepare methode zijn gemaakt.
    func revert(on database: Database) async throws {
        try await database.schema("galaxies").delete()
    }
}

De prepare methode wordt gebruikt om de database voor te bereiden voor het opslaan van Galaxy modellen.

Schema

In deze methode wordt database.schema(_:) gebruikt om een nieuwe SchemaBuilder aan te maken. Een of meer velden worden dan aan de bouwer toegevoegd voordat create() wordt aangeroepen om het schema te maken.

Elk veld dat aan de bouwer wordt toegevoegd heeft een naam, type, en optionele beperkingen.

field(<name>, <type>, <optional constraints>)

Er is een gemakkelijke id() methode om @ID eigenschappen toe te voegen met Fluent's aanbevolen standaardwaarden.

Het terugdraaien van de migratie maakt alle wijzigingen ongedaan die zijn aangebracht in de prepare-methode. In dit geval betekent dat het verwijderen van het schema van de Galaxy.

Zodra de migratie is gedefinieerd, moet u Fluent hierover informeren door het toe te voegen aan app.migrations in configure.swift.

app.migrations.add(CreateGalaxy())

Migreren

Om migraties uit te voeren, roep swift run App migrate op vanaf de commandoregel of voeg migrate toe als argument aan Xcode's App schema.

$ swift run App migrate
Migrate Command: Prepare
The following migration(s) will be prepared:
+ CreateGalaxy on default
Would you like to continue?
y/n> y
Migration successful

Querying

Nu je met succes een model hebt gemaakt en je database hebt gemigreerd, ben je klaar om je eerste query te maken.

All

Kijk eens naar de volgende route die een array zal teruggeven van alle melkwegstelsels in de database.

app.get("galaxies") { req async throws in
    try await Galaxy.query(on: req.db).all()
}

Om een Galaxy direct terug te geven in een route afsluiting, voeg conformiteit toe aan Content.

final class Galaxy: Model, Content {
    ...
}

Galaxy.query wordt gebruikt om een nieuwe query builder voor het model te maken. req.db is een verwijzing naar de standaard database voor uw applicatie. Tenslotte, all() geeft alle modellen terug die in de database zijn opgeslagen.

Als je het project compileert en uitvoert en GET /galaxies opvraagt, zou je een lege array terug moeten zien. Laten we een route toevoegen voor het creëren van een nieuw sterrenstelsel.

Aanmaken

Volgens RESTful conventie, gebruik het POST /galaxies eindpunt om een nieuw sterrenstelsel te creëren. Aangezien modellen codeerbaar zijn, kun je een sterrenstelsel direct uit de request body decoderen.

app.post("galaxies") { req -> EventLoopFuture<Galaxy> in
    let galaxy = try req.content.decode(Galaxy.self)
    return galaxy.create(on: req.db)
        .map { galaxy }
}

Zie ook

Zie Content → Overview voor meer informatie over het decoderen van request bodies.

Als je eenmaal een instantie van het model hebt, kun je create(on:) oproepen om het model op te slaan in de database. Dit retourneert een EventLoopFuture<Void> die aangeeft dat het opslaan voltooid is. Zodra het opslaan is voltooid, retourneert u het nieuw gemaakte model met map.

Als je async/await gebruikt, kun je je code zo schrijven:

app.post("galaxies") { req async throws -> Galaxy in
    let galaxy = try req.content.decode(Galaxy.self)
    try await galaxy.create(on: req.db)
    return galaxy
}

In dit geval geeft de async versie niets terug, maar zal terugkomen zodra het opslaan voltooid is.

Bouw en draai het project en stuur het volgende verzoek.

POST /galaxies HTTP/1.1
content-length: 21
content-type: application/json

{
    "name": "Milky Way"
}

Je zou het gemaakte model terug moeten krijgen met een identifier als antwoord.

{
    "id": ...,
    "name": "Milky Way"
}

Als je nu GET /galaxies opnieuw opvraagt, zou je het nieuw aangemaakte sterrenstelsel terug moeten zien in de array.

Relaties

Wat zijn melkwegstelsels zonder sterren! Laten we eens een snelle blik werpen op Fluent's krachtige relationele mogelijkheden door een één-op-veel relatie toe te voegen tussen Galaxy en een nieuw Star model.

final class Star: Model, Content {
    // Naam van de tabel of verzameling.
    static let schema = "stars"

    // Unieke identificatiecode voor deze ster.
    @ID(key: .id)
    var id: UUID?

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

    // Verwijzing naar het sterrenstelsel waar deze ster in zit.
    @Parent(key: "galaxy_id")
    var galaxy: Galaxy

    // Creëert een nieuwe, lege Ster.
    init() { }

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

Parent

Het nieuwe Star model lijkt veel op Galaxy behalve dat er een nieuw veldtype is: @Parent.

@Parent(key: "galaxy_id")
var galaxy: Galaxy

De parent eigenschap is een veld dat de identifier van een ander model opslaat. Het model dat de verwijzing bevat wordt het "child" genoemd en het model waarnaar verwezen wordt wordt de "parent" genoemd. Dit type relatie is ook bekend als " één-op-veel". De key parameter aan de eigenschap specificeert de veldnaam die moet worden gebruikt om de sleutel van de ouder op te slaan in de database.

In de init-methode wordt de parent identifier ingesteld met $galaxy.

self.$galaxy.id = galaxyID

Door de naam van de bovenliggende eigenschap vooraf te laten gaan door $, krijgt u toegang tot de onderliggende property-wrapper. Dit is nodig om toegang te krijgen tot het interne @Field dat de eigenlijke identifier waarde opslaat.

Zie ook

Bekijk het Swift Evolution voorstel voor property wrappers voor meer informatie: [SE-0258] Property Wrappers](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md)

Maak vervolgens een migratie om de database voor te bereiden op de verwerking van Star.

struct CreateStar: AsyncMigration {
    // Bereidt de database voor op het opslaan van Ster modellen.
    func prepare(on database: Database) async throws {
        try await database.schema("stars")
            .id()
            .field("name", .string)
            .field("galaxy_id", .uuid, .references("galaxies", "id"))
            .create()
    }

    // Zet optioneel de wijzigingen terug die in de prepare methode zijn gemaakt.
    func revert(on database: Database) async throws {
        try await database.schema("stars").delete()
    }
}

Dit is grotendeels hetzelfde als de migratie van melkwegstelsels, behalve het extra veld om de identifier van het bovenliggende melkwegstelsel op te slaan.

field("galaxy_id", .uuid, .references("galaxies", "id"))

Dit veld specificeert een optionele beperking die de database vertelt dat de waarde van het veld verwijst naar het veld "id" in het "melkwegstelsels" schema. Dit is ook gekend als een vreemde sleutel en helpt de gegevensintegriteit te verzekeren.

Zodra de migratie is gemaakt, voeg het toe aan app.migrations na de CreateGalaxy migratie.

app.migrations.add(CreateGalaxy())
app.migrations.add(CreateStar())

Omdat migraties in volgorde worden uitgevoerd, en CreateStar verwijst naar het schema van de melkwegstelsels, is volgorde belangrijk. Tenslotte, voer de migraties uit om de database voor te bereiden.

Voeg een route toe voor het maken van nieuwe sterren.

app.post("stars") { req async throws -> Star in
    let star = try req.content.decode(Star.self)
    try await star.create(on: req.db)
    return star
}

Maak een nieuwe ster die verwijst naar de eerder gemaakte galaxy met het volgende HTTP-verzoek.

POST /stars HTTP/1.1
content-length: 36
content-type: application/json

{
    "name": "Sun",
    "galaxy": {
        "id": ...
    }
}

Je zou de nieuw aangemaakte ster terug moeten zien met een unieke identificatie.

{
    "id": ...,
    "name": "Sun",
    "galaxy": {
        "id": ...
    }
}

Children

Laten we nu eens kijken hoe je Fluent's eager-loading functie kunt gebruiken om automatisch de sterren van een sterrenstelsel te retourneren in de GET /galaxies route. Voeg de volgende eigenschap toe aan het Galaxy model.

// Alle sterren in dit heelal.
@Children(for: \.$galaxy)
var stars: [Star]

De @Children eigenschap wrapper is de inverse van @Parent. Het neemt een sleutel-pad naar het @Parent veld van het child als het for argument. De waarde is een array van children omdat er nul of meer child modellen kunnen bestaan. Er zijn geen wijzigingen nodig in de migratie van het sterrenstelsel omdat alle informatie die nodig is voor deze relatie is opgeslagen op Star.

Eager Load

Nu de relatie compleet is, kun je de with methode op de query bouwer gebruiken om automatisch de galaxy-ster relatie op te halen en te serialiseren.

app.get("galaxies") { req in
    try await Galaxy.query(on: req.db).with(\.$stars).all()
}

Een sleutel-pad naar de @Children relatie wordt doorgegeven aan with om Fluent te vertellen dat deze relatie automatisch wordt geladen in alle resulterende modellen. Bouw en draai het programma en stuur nog een verzoek naar GET /galaxies. U zou nu de sterren automatisch in het antwoord moeten zien.

[
    {
        "id": ...,
        "name": "Milky Way",
        "stars": [
            {
                "id": ...,
                "name": "Sun",
                "galaxy": {
                    "id": ...
                }
            }
        ]
    }
]

Volgende stappen

Gefeliciteerd met het maken van uw eerste modellen en migraties en het uitvoeren van basis create en read operaties. Voor meer diepgaande informatie over al deze functies, bekijk hun respectievelijke secties in de Fluent handleiding.