Modelli¶
I modelli rappresentano i dati memorizzati in tabelle o collezioni nel tuo database. I modelli hanno uno o più campi che memorizzano valori codificabili. Tutti i modelli hanno un identificatore univoco. I property wrapper vengono usati per denotare identificatori, campi e relazioni.
Di seguito è riportato un esempio di un modello semplice con un solo campo. Nota che i modelli non descrivono l'intero schema del database, come vincoli, indici e chiavi esterne. Gli schemi sono definiti nelle migrazioni. I modelli sono focalizzati sulla rappresentazione dei dati memorizzati negli schemi del tuo database.
final class Planet: Model {
// Name of the table or collection.
static let schema = "planets"
// Unique identifier for this Planet.
@ID(key: .id)
var id: UUID?
// The Planet's name.
@Field(key: "name")
var name: String
// Creates a new, empty Planet.
init() { }
// Creates a new Planet with all properties set.
init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
}
Schema¶
Tutti i modelli richiedono una proprietà schema statica e di sola lettura. Questa stringa fa riferimento al nome della tabella o collezione che questo modello rappresenta.
final class Planet: Model {
// Name of the table or collection.
static let schema = "planets"
}
Quando si interroga questo modello, i dati verranno recuperati e memorizzati nello schema denominato "planets".
Suggerimento
Il nome dello schema è tipicamente il nome della classe al plurale e in minuscolo.
Identificatore¶
Tutti i modelli devono avere una proprietà id definita usando il property wrapper @ID. Questo campo identifica in modo univoco le istanze del tuo modello.
final class Planet: Model {
// Unique identifier for this Planet.
@ID(key: .id)
var id: UUID?
}
Per impostazione predefinita, la proprietà @ID dovrebbe usare la chiave speciale .id che si risolve in una chiave appropriata per il driver di database sottostante. Per SQL è "id" e per NoSQL è "_id".
@ID dovrebbe essere anche di tipo UUID. Questo è l'unico valore identificatore attualmente supportato da tutti i driver di database. Fluent genererà automaticamente nuovi identificatori UUID quando i modelli vengono creati.
@ID ha un valore opzionale poiché i modelli non salvati potrebbero non avere ancora un identificatore. Per ottenere l'identificatore o lanciare un errore, usa requireID.
let id = try planet.requireID()
Exists¶
@ID ha una proprietà exists che rappresenta se il modello esiste nel database o meno. Quando inizializzi un modello, il valore è false. Dopo aver salvato un modello o quando recuperi un modello dal database, il valore è true. Questa proprietà è mutabile.
if planet.$id.exists {
// Questo modello esiste nel database.
}
Identificatore Personalizzato¶
Fluent supporta chiavi e tipi di identificatore personalizzati usando l'overload @ID(custom:).
final class Planet: Model {
// Identificatore univoco per questo Planet.
@ID(custom: "foo")
var id: Int?
}
L'esempio sopra usa un @ID con la chiave personalizzata "foo" e il tipo di identificatore Int. Questo è compatibile con i database SQL che usano chiavi primarie con incremento automatico, ma non è compatibile con NoSQL.
Gli @ID personalizzati consentono all'utente di specificare come deve essere generato l'identificatore usando il parametro generatedBy.
@ID(custom: "foo", generatedBy: .user)
Il parametro generatedBy supporta questi casi:
| Generato Da | Descrizione |
|---|---|
.user |
La proprietà @ID deve essere impostata prima di salvare un nuovo modello. |
.random |
Il tipo di valore @ID deve conformarsi a RandomGeneratable. |
.database |
Il database è previsto che generi un valore al momento del salvataggio. |
Se il parametro generatedBy viene omesso, Fluent tenterà di dedurre un caso appropriato in base al tipo di valore @ID. Per esempio, Int avrà come default la generazione .database a meno che non sia specificato diversamente.
Inizializzatore¶
I modelli devono avere un metodo inizializzatore vuoto.
final class Planet: Model {
// Creates a new, empty Planet.
init() { }
}
Fluent richiede questo metodo internamente per inizializzare i modelli restituiti dalle query. Viene anche usato per la riflessione.
Potresti voler aggiungere un inizializzatore di convenienza al tuo modello che accetta tutte le proprietà.
final class Planet: Model {
// Crea un nuovo Planet con tutte le proprietà impostate.
init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
}
L'uso di inizializzatori di convenienza rende più facile aggiungere nuove proprietà al modello in futuro.
Campo¶
I modelli possono avere zero o più proprietà @Field per memorizzare dati.
final class Planet: Model {
// Il nome del Planet.
@Field(key: "name")
var name: String
}
I campi richiedono che la chiave del database sia definita esplicitamente. Non è necessario che sia uguale al nome della proprietà.
Suggerimento
Fluent raccomanda di usare snake_case per le chiavi del database e camelCase per i nomi delle proprietà.
I valori dei campi possono essere di qualsiasi tipo che si conformi a Codable. Memorizzare strutture annidate e array in @Field è supportato, ma le operazioni di filtraggio sono limitate. Vedi @Group per un'alternativa.
Per i campi che contengono un valore opzionale, usa @OptionalField.
@OptionalField(key: "tag")
var tag: String?
Attenzione
Un campo non opzionale che ha un osservatore di proprietà willSet che fa riferimento al suo valore corrente o un osservatore di proprietà didSet che fa riferimento al suo oldValue provocherà un errore fatale.
Relazioni¶
I modelli possono avere zero o più proprietà di relazione che fanno riferimento ad altri modelli come @Parent, @Children e @Siblings. Per saperne di più sulle relazioni, consulta la sezione relazioni.
Timestamp¶
@Timestamp è un tipo speciale di @Field che memorizza una Foundation.Date. I timestamp vengono impostati automaticamente da Fluent in base al trigger scelto.
final class Planet: Model {
// Quando questo Planet è stato creato.
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
// Quando questo Planet è stato aggiornato l'ultima volta.
@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?
}
@Timestamp supporta i seguenti trigger.
| Trigger | Descrizione |
|---|---|
.create |
Impostato quando una nuova istanza del modello viene salvata nel database. |
.update |
Impostato quando un'istanza del modello esistente viene salvata nel database. |
.delete |
Impostato quando un modello viene eliminato dal database. Vedi eliminazione temporanea. |
Il valore data di @Timestamp è opzionale e dovrebbe essere impostato a nil quando si inizializza un nuovo modello.
Formato Timestamp¶
Per impostazione predefinita, @Timestamp utilizzerà una codifica datetime efficiente basata sul tuo driver di database. Puoi personalizzare come il timestamp viene memorizzato nel database usando il parametro format.
// Memorizza un timestamp formattato ISO 8601 rappresentante
// quando questo modello è stato aggiornato l'ultima volta.
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?
Nota che la migrazione associata per questo esempio .iso8601 richiederebbe la memorizzazione in formato .string.
.field("updated_at", .string)
I formati di timestamp disponibili sono elencati di seguito.
| Formato | Descrizione | Tipo |
|---|---|---|
.default |
Usa la codifica datetime efficiente per il database specifico. |
Date |
.iso8601 |
Stringa ISO 8601. Supporta il parametro withMilliseconds. |
String |
.unix |
Secondi dall'epoca Unix inclusa la frazione. | Double |
Puoi accedere al valore grezzo del timestamp direttamente usando la proprietà timestamp.
// Imposta manualmente il valore del timestamp su questo @Timestamp
// formattato ISO 8601.
model.$updatedAt.timestamp = "2020-06-03T16:20:14+00:00"
Eliminazione Temporanea¶
Aggiungere un @Timestamp che usa il trigger .delete al tuo modello abiliterà l'eliminazione temporanea.
final class Planet: Model {
// Quando questo Planet è stato eliminato.
@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?
}
I modelli eliminati temporaneamente esistono ancora nel database dopo l'eliminazione, ma non verranno restituiti nelle query.
Suggerimento
Puoi impostare manualmente un timestamp di eliminazione a una data nel futuro. Questo può essere usato come data di scadenza.
Per forzare la rimozione dal database di un modello eliminabile temporaneamente, usa il parametro force in delete.
// Elimina dal database anche se il modello è eliminabile temporaneamente.
model.delete(force: true, on: database)
Per ripristinare un modello eliminato temporaneamente, usa il metodo restore.
// Cancella il timestamp di eliminazione permettendo a questo
// modello di essere restituito nelle query.
model.restore(on: database)
Per includere i modelli eliminati temporaneamente in una query, usa withDeleted.
// Recupera tutti i pianeti inclusi quelli eliminati temporaneamente.
Planet.query(on: database).withDeleted().all()
Enum¶
@Enum è un tipo speciale di @Field per memorizzare tipi rappresentabili come stringhe come enum nativi del database. Gli enum nativi del database forniscono un ulteriore livello di sicurezza dei tipi al tuo database e possono essere più performanti degli enum grezzi.
// Enum rappresentabile come stringa e conforme a Codable per i tipi di animali.
enum Animal: String, Codable {
case dog, cat
}
final class Pet: Model {
// Memorizza il tipo di animale come enum nativo del database.
@Enum(key: "type")
var type: Animal
}
Solo i tipi che si conformano a RawRepresentable dove RawValue è String sono compatibili con @Enum. Gli enum basati su String soddisfano questo requisito per impostazione predefinita.
Per memorizzare un enum opzionale, usa @OptionalEnum.
Il database deve essere preparato per gestire gli enum tramite una migrazione. Vedi enum per ulteriori informazioni.
Enum Grezzi¶
Qualsiasi enum basato su un tipo Codable, come String o Int, può essere memorizzato in @Field. Verrà memorizzato nel database come valore grezzo.
Group¶
@Group ti consente di memorizzare un gruppo annidato di campi come una singola proprietà sul tuo modello. A differenza delle struct Codable memorizzate in un @Field, i campi in un @Group sono interrogabili. Fluent ottiene questo memorizzando @Group come struttura piatta nel database.
Per usare un @Group, prima definisci la struttura annidata che vorresti memorizzare usando il protocollo Fields. Questo è molto simile a Model tranne che non è richiesto nessun identificatore o nome schema. Puoi memorizzare qui molte proprietà che Model supporta come @Field, @Enum o anche un altro @Group.
// Un animale con nome e tipo.
final class Pet: Fields {
// Il nome del Pet.
@Field(key: "name")
var name: String
// Il tipo di Pet.
@Field(key: "type")
var type: String
// Crea un nuovo Pet vuoto.
init() { }
}
Dopo aver creato la definizione dei campi, puoi usarla come valore di una proprietà @Group.
final class User: Model {
// Il pet annidato dell'utente.
@Group(key: "pet")
var pet: Pet
}
I campi di un @Group sono accessibili tramite la sintassi a punti.
let user: User = ...
print(user.pet.name) // String
Puoi interrogare i campi annidati normalmente usando la sintassi a punti sui property wrapper.
User.query(on: database).filter(\.$pet.$name == "Zizek").all()
Nel database, @Group è memorizzato come una struttura piatta con le chiavi unite da _. Di seguito è riportato un esempio di come User apparirebbe nel database.
| id | name | pet_name | pet_type |
|---|---|---|---|
| 1 | Tanner | Zizek | Cat |
| 2 | Logan | Runa | Dog |
Codable¶
I modelli si conformano a Codable per impostazione predefinita. Questo significa che puoi usare i tuoi modelli con l'API dei contenuti di Vapor aggiungendo la conformità al protocollo Content.
extension Planet: Content { }
app.get("planets") { req async throws in
// Recupera tutti i pianeti dal database e li restituisce come JSON.
try await Planet.query(on: req.db).all()
}
Quando si serializza da/verso Codable, le proprietà del modello useranno i loro nomi di variabile invece delle chiavi. Le relazioni verranno serializzate come strutture annidate e qualsiasi dato caricato in modo eager verrà incluso.
Informazione
Raccomandiamo che nella quasi totalità dei casi tu utilizzi un DTO invece di un modello per le risposte dell'API e i corpi delle richieste. Vedi Data Transfer Object per ulteriori informazioni.
Data Transfer Object¶
La conformità Codable predefinita del modello può rendere più facile l'utilizzo semplice e la prototipazione. Tuttavia, espone le informazioni del database sottostante all'API. Questo di solito non è desiderabile sia da un punto di vista della sicurezza - restituire campi sensibili come l'hash della password di un utente è una cattiva idea - sia da un punto di vista dell'usabilità. Rende difficile cambiare lo schema del database senza rompere l'API, accettare o restituire dati in un formato diverso, o aggiungere o rimuovere campi dall'API.
Per la maggior parte dei casi dovresti usare un DTO, o data transfer object, invece di un modello (noto anche come domain transfer object). Un DTO è un tipo Codable separato che rappresenta la struttura dati che vorresti codificare o decodificare. Questi disaccoppiano la tua API dallo schema del database e ti consentono di apportare modifiche ai tuoi modelli senza rompere l'API pubblica della tua app, avere versioni diverse e rendere la tua API più gradevole da usare per i tuoi client.
Assume il seguente modello User negli esempi seguenti.
// Abridged user model for reference.
final class User: Model {
@ID(key: .id)
var id: UUID?
@Field(key: "first_name")
var firstName: String
@Field(key: "last_name")
var lastName: String
}
Un caso d'uso comune per i DTO è nell'implementazione delle richieste PATCH. Queste richieste includono solo i valori per i campi che devono essere aggiornati. Tentare di decodificare un Model direttamente da tale richiesta fallirebbe se mancasse uno qualsiasi dei campi richiesti. Nell'esempio seguente, puoi vedere un DTO utilizzato per decodificare i dati della richiesta e aggiornare un modello.
// Struttura della richiesta PATCH /users/:id.
struct PatchUser: Decodable {
var firstName: String?
var lastName: String?
}
app.patch("users", ":id") { req async throws -> User in
// Decodifica i dati della richiesta.
let patch = try req.content.decode(PatchUser.self)
// Recupera l'utente desiderato dal database.
guard let user = try await User.find(req.parameters.get("id"), on: req.db) else {
throw Abort(.notFound)
}
// Se è stato fornito un nuovo nome, aggiornalo.
if let firstName = patch.firstName {
user.firstName = firstName
}
// Se è stato fornito un nuovo cognome, aggiornalo.
if let lastName = patch.lastName {
user.lastName = lastName
}
// Salva l'utente e restituiscilo.
try await user.save(on: req.db)
return user
}
Un altro caso d'uso comune per i DTO è la personalizzazione del formato delle risposte dell'API. L'esempio seguente mostra come un DTO può essere utilizzato per aggiungere un campo calcolato a una risposta.
// Struttura della risposta GET /users.
struct GetUser: Content {
var id: UUID
var name: String
}
app.get("users") { req async throws -> [GetUser] in
// Recupera tutti gli utenti dal database.
let users = try await User.query(on: req.db).all()
return try users.map { user in
// Converte ogni utente nel tipo di ritorno GET.
try GetUser(
id: user.requireID(),
name: "\(user.firstName) \(user.lastName)"
)
}
}
Un altro caso d'uso comune è quando si lavora con le relazioni, come le relazioni parent o children. Vedi la documentazione di Parent per un esempio di come usare un DTO per rendere più facile decodificare un modello con una relazione @Parent.
Anche se la struttura del DTO è identica alla conformità Codable del modello, averlo come tipo separato può aiutare a mantenere in ordine i grandi progetti. Se hai mai bisogno di apportare una modifica alle proprietà dei tuoi modelli, non devi preoccuparti di rompere l'API pubblica della tua app. Potresti anche considerare di mettere i tuoi DTO in un pacchetto separato che può essere condiviso con i consumatori della tua API e aggiungere la conformità Content nella tua app Vapor.
Alias¶
Il protocollo ModelAlias ti consente di identificare in modo univoco un modello che viene unito più volte in una query. Per ulteriori informazioni, vedi join.
Salvataggio¶
Per salvare un modello nel database, usa il metodo save(on:).
planet.save(on: database)
Questo metodo chiamerà create o update internamente a seconda che il modello esista già nel database.
Creazione¶
Puoi chiamare il metodo create per salvare un nuovo modello nel database.
let planet = Planet(name: "Earth")
planet.create(on: database)
create è disponibile anche su un array di modelli. Questo salva tutti i modelli nel database in un singolo batch/query.
// Esempio di creazione in batch.
[earth, mars].create(on: database)
Attenzione
I modelli che usano @ID(custom:) con il generatore .database (di solito Int con incremento automatico) non avranno i loro identificatori appena creati accessibili dopo la creazione in batch. Per le situazioni in cui hai bisogno di accedere agli identificatori, chiama create su ogni modello.
Per creare un array di modelli separatamente, usa map + flatten.
[earth, mars].map { $0.create(on: database) }
.flatten(on: database.eventLoop)
Se si usa async/await puoi usare:
await withThrowingTaskGroup(of: Void.self) { taskGroup in
[earth, mars].forEach { model in
taskGroup.addTask { try await model.create(on: database) }
}
}
Aggiornamento¶
Puoi chiamare il metodo update per salvare un modello che è stato recuperato dal database.
guard let planet = try await Planet.find(..., on: database) else {
throw Abort(.notFound)
}
planet.name = "Earth"
try await planet.update(on: database)
Per aggiornare un array di modelli, usa map + flatten.
[earth, mars].map { $0.update(on: database) }
.flatten(on: database.eventLoop)
// TODO
Query¶
I modelli espongono un metodo statico query(on:) che restituisce un query builder.
Planet.query(on: database).all()
Per saperne di più sulle query, consulta la sezione query.
Find¶
I modelli hanno un metodo statico find(_:on:) per cercare un'istanza del modello tramite identificatore.
Planet.find(req.parameters.get("id"), on: database)
Questo metodo restituisce nil se non viene trovato nessun modello con quell'identificatore.
Ciclo di Vita¶
Il middleware del modello ti consente di agganciarti agli eventi del ciclo di vita del tuo modello. Sono supportati i seguenti eventi del ciclo di vita.
| Metodo | Descrizione |
|---|---|
create |
Eseguito prima che un modello venga creato. |
update |
Eseguito prima che un modello venga aggiornato. |
delete(force:) |
Eseguito prima che un modello venga eliminato. |
softDelete |
Eseguito prima che un modello venga eliminato temporaneamente. |
restore |
Eseguito prima che un modello venga ripristinato (opposto dell'eliminazione temporanea). |
Il middleware del modello viene dichiarato usando il protocollo ModelMiddleware o AsyncModelMiddleware. Tutti i metodi del ciclo di vita hanno un'implementazione predefinita, quindi devi implementare solo i metodi che richiedi. Ogni metodo accetta il modello in questione, un riferimento al database e l'azione successiva nella catena. Il middleware può scegliere di tornare presto, restituire un futuro fallito, o chiamare l'azione successiva per continuare normalmente.
Usando questi metodi puoi eseguire azioni sia prima che dopo il completamento dell'evento specifico. L'esecuzione di azioni dopo il completamento dell'evento può essere fatta mappando il futuro restituito dal responder successivo.
// Esempio di middleware che capitalizza i nomi.
struct PlanetMiddleware: ModelMiddleware {
func create(model: Planet, on db: Database, next: AnyModelResponder) -> EventLoopFuture<Void> {
// Il modello può essere modificato qui prima che venga creato.
model.name = model.name.capitalized()
return next.create(model, on: db).map {
// Una volta che il pianeta è stato creato, il codice
// qui verrà eseguito.
print ("Planet \(model.name) was created")
}
}
}
oppure se si usa async/await:
struct PlanetMiddleware: AsyncModelMiddleware {
func create(model: Planet, on db: Database, next: AnyAsyncModelResponder) async throws {
// Il modello può essere modificato qui prima che venga creato.
model.name = model.name.capitalized()
try await next.create(model, on: db)
// Una volta che il pianeta è stato creato, il codice
// qui verrà eseguito.
print ("Planet \(model.name) was created")
}
}
Una volta creato il tuo middleware, puoi abilitarlo usando app.databases.middleware.
// Esempio di configurazione del middleware del modello.
app.databases.middleware.use(PlanetMiddleware(), on: .psql)
Spazio del Database¶
Fluent supporta l'impostazione di uno spazio per un modello, che consente la partizione dei singoli modelli Fluent tra schemi PostgreSQL, database MySQL e più database SQLite collegati. MongoDB non supporta gli spazi al momento della stesura. Per collocare un modello in uno spazio diverso da quello predefinito, aggiungi una nuova proprietà statica al modello:
public static let schema = "planets"
public static let space: String? = "mirror_universe"
// ...
Fluent lo userà quando costruisce tutte le query del database.