Modele#
Modele reprezentują dane przechowywane w tabelach lub kolekcjach w twojej bazie danych. Modele mają jedno lub więcej pól, które przechowują wartości zgodne z Codable. Wszystkie modele mają unikalny identyfikator. Opakowania właściwości (property wrapper) są używane do oznaczania identyfikatorów, pól i relacji.
Poniżej znajduje się przykład prostego modelu z jednym polem. Zwróć uwagę, że modele nie opisują całego schematu bazy danych, takiego jak ograniczenia, indeksy i klucze obce. Schematy są definiowane w migracjach. Modele skupiają się na reprezentowaniu danych przechowywanych w schematach twojej bazy danych.
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#
Wszystkie modele wymagają statycznej właściwości schema, tylko do odczytu. Ten ciąg znaków odwołuje się do nazwy tabeli lub kolekcji, którą reprezentuje ten model.
final class Planet: Model {
// Name of the table or collection.
static let schema = "planets"
}
Podczas wykonywania zapytań do tego modelu, dane będą pobierane i zapisywane w schemacie o nazwie "planets".
Tip
Nazwa schematu to zazwyczaj nazwa klasy w liczbie mnogiej i zapisana małymi literami.
Identyfikator#
Wszystkie modele muszą mieć właściwość id zdefiniowaną za pomocą opakowania właściwości @ID. To pole jednoznacznie identyfikuje instancje twojego modelu.
final class Planet: Model {
// Unique identifier for this Planet.
@ID(key: .id)
var id: UUID?
}
Domyślnie właściwość @ID powinna używać specjalnego klucza .id, który przekłada się na odpowiedni klucz dla danego sterownika bazy danych. Dla SQL jest to "id", a dla NoSQL "_id".
@ID powinno być również typu UUID. Jest to obecnie jedyny typ wartości identyfikatora obsługiwany przez wszystkie sterowniki bazy danych. Fluent automatycznie wygeneruje nowe identyfikatory UUID podczas tworzenia modeli.
@ID ma opcjonalną wartość, ponieważ niezapisane modele mogą jeszcze nie mieć identyfikatora. Aby pobrać identyfikator lub rzucić błąd, użyj requireID.
let id = try planet.requireID()
Exists#
@ID posiada właściwość exists, która reprezentuje, czy model istnieje w bazie danych, czy nie. Podczas inicjalizacji modelu wartość ta wynosi false. Po zapisaniu modelu lub po pobraniu modelu z bazy danych wartość ta wynosi true. Ta właściwość jest mutowalna.
if planet.$id.exists {
// This model exists in database.
}
Niestandardowy identyfikator#
Fluent wspiera niestandardowe klucze i typy identyfikatorów za pomocą przeciążenia @ID(custom:).
final class Planet: Model {
// Unique identifier for this Planet.
@ID(custom: "foo")
var id: Int?
}
Powyższy przykład używa @ID z niestandardowym kluczem "foo" i typem identyfikatora Int. Jest to kompatybilne z bazami danych SQL wykorzystującymi automatycznie zwiększane klucze główne, ale nie jest kompatybilne z NoSQL.
Niestandardowe @ID pozwalają użytkownikowi określić, w jaki sposób identyfikator powinien być generowany, za pomocą parametru generatedBy.
@ID(custom: "foo", generatedBy: .user)
Parametr generatedBy wspiera następujące przypadki:
| Generated By | Description |
|---|---|
.user |
@ID property is expected to be set before saving a new model. |
.random |
@ID value type must conform to RandomGeneratable. |
.database |
Database is expected to generate a value upon save. |
Jeśli parametr generatedBy zostanie pominięty, Fluent spróbuje wywnioskować odpowiedni przypadek na podstawie typu wartości @ID. Na przykład Int domyślnie użyje generacji .database, chyba że zostanie określone inaczej.
Inicjalizator#
Modele muszą mieć pustą metodę inicjalizującą.
final class Planet: Model {
// Creates a new, empty Planet.
init() { }
}
Fluent wymaga tej metody wewnętrznie do inicjalizowania modeli zwracanych przez zapytania. Jest ona również używana do refleksji.
Możesz chcieć dodać do swojego modelu wygodny inicjalizator, który przyjmuje wszystkie właściwości.
final class Planet: Model {
// Creates a new Planet with all properties set.
init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
}
Używanie wygodnych inicjalizatorów ułatwia dodawanie nowych właściwości do modelu w przyszłości.
Field#
Modele mogą mieć zero lub więcej właściwości @Field do przechowywania danych.
final class Planet: Model {
// The Planet's name.
@Field(key: "name")
var name: String
}
Pola wymagają jawnego zdefiniowania klucza bazy danych. Nie musi on być taki sam jak nazwa właściwości.
Tip
Fluent zaleca używanie snake_case dla kluczy bazy danych i camelCase dla nazw właściwości.
Wartości pól mogą być dowolnym typem zgodnym z Codable. Przechowywanie zagnieżdżonych struktur i tablic w @Field jest wspierane, ale operacje filtrowania są ograniczone. Zobacz @Group jako alternatywę.
Dla pól zawierających wartość opcjonalną użyj @OptionalField.
@OptionalField(key: "tag")
var tag: String?
Warning
Nieopcjonalne pole, które posiada obserwator właściwości willSet odwołujący się do jego bieżącej wartości lub obserwator didSet odwołujący się do jego oldValue, spowoduje błąd krytyczny (fatal error).
Relacje#
Modele mogą mieć zero lub więcej właściwości relacji odwołujących się do innych modeli, takich jak @Parent, @Children i @Siblings. Dowiedz się więcej o relacjach w sekcji relacje.
Timestamp#
@Timestamp to specjalny rodzaj @Field, który przechowuje Foundation.Date. Znaczniki czasu są ustawiane automatycznie przez Fluent zgodnie z wybranym wyzwalaczem (trigger).
final class Planet: Model {
// When this Planet was created.
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
// When this Planet was last updated.
@Timestamp(key: "updated_at", on: .update)
var updatedAt: Date?
}
@Timestamp wspiera następujące wyzwalacze.
| Trigger | Description |
|---|---|
.create |
Set when a new model instance is saved to the database. |
.update |
Set when an existing model instance is saved to the database. |
.delete |
Set when a model is deleted from the database. See soft delete. |
Wartość daty @Timestamp jest opcjonalna i powinna być ustawiona na nil podczas inicjalizacji nowego modelu.
Format znacznika czasu#
Domyślnie @Timestamp użyje wydajnego kodowania datetime opartego na twoim sterowniku bazy danych. Możesz dostosować sposób przechowywania znacznika czasu w bazie danych za pomocą parametru format.
// Stores an ISO 8601 formatted timestamp representing
// when this model was last updated.
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?
Zwróć uwagę, że powiązana migracja dla tego przykładu .iso8601 wymagałaby przechowywania w formacie .string.
.field("updated_at", .string)
Poniżej wymieniono dostępne formaty znaczników czasu.
| Format | Description | Type |
|---|---|---|
.default |
Uses efficient datetime encoding for specific database. |
Date |
.iso8601 |
ISO 8601 string. Supports withMilliseconds parameter. |
String |
.unix |
Seconds since Unix epoch including fraction. | Double |
Możesz uzyskać bezpośredni dostęp do surowej wartości znacznika czasu za pomocą właściwości timestamp.
// Manually set the timestamp value on this ISO 8601
// formatted @Timestamp.
model.$updatedAt.timestamp = "2020-06-03T16:20:14+00:00"
Miękkie usuwanie (Soft Delete)#
Dodanie do modelu @Timestamp używającego wyzwalacza .delete włączy miękkie usuwanie (soft-deletion).
final class Planet: Model {
// When this Planet was deleted.
@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?
}
Modele usunięte w sposób miękki nadal istnieją w bazie danych po usunięciu, ale nie będą zwracane w zapytaniach.
Tip
Możesz ręcznie ustawić znacznik czasu usunięcia na datę w przyszłości. Może to być używane jako data wygaśnięcia.
Aby wymusić usunięcie modelu obsługującego miękkie usuwanie z bazy danych, użyj parametru force w delete.
// Deletes from the database even if the model
// is soft deletable.
model.delete(force: true, on: database)
Aby przywrócić model usunięty w sposób miękki, użyj metody restore.
// Clears the on delete timestamp allowing this
// model to be returned in queries.
model.restore(on: database)
Aby uwzględnić modele usunięte w sposób miękki w zapytaniu, użyj withDeleted.
// Fetches all planets including soft deleted.
Planet.query(on: database).withDeleted().all()
Enum#
@Enum to specjalny rodzaj @Field służący do przechowywania typów reprezentowalnych jako ciąg znaków jako natywnych enumeracji bazy danych. Natywne enumeracje bazy danych zapewniają dodatkową warstwę bezpieczeństwa typów w twojej bazie danych i mogą być bardziej wydajne niż surowe enumeracje.
// String representable, Codable enum for animal types.
enum Animal: String, Codable {
case dog, cat
}
final class Pet: Model {
// Stores type of animal as a native database enum.
@Enum(key: "type")
var type: Animal
}
Tylko typy zgodne z RawRepresentable, gdzie RawValue jest String, są kompatybilne z @Enum. Enumeracje oparte na String domyślnie spełniają ten wymóg.
Aby przechować opcjonalną enumerację, użyj @OptionalEnum.
Baza danych musi być przygotowana do obsługi enumeracji za pomocą migracji. Zobacz enum, aby uzyskać więcej informacji.
Surowe enumeracje#
Każda enumeracja oparta na typie zgodnym z Codable, takim jak String lub Int, może być przechowywana w @Field. Będzie ona przechowywana w bazie danych jako surowa wartość.
Group#
@Group pozwala przechować zagnieżdżoną grupę pól jako pojedynczą właściwość w twoim modelu. W przeciwieństwie do struktur zgodnych z Codable przechowywanych w @Field, pola w @Group można przeszukiwać (query). Fluent osiąga to, przechowując @Group jako płaską strukturę w bazie danych.
Aby użyć @Group, najpierw zdefiniuj zagnieżdżoną strukturę, którą chcesz przechować, korzystając z protokołu Fields. Jest to bardzo podobne do Model, z tym że nie jest wymagany żaden identyfikator ani nazwa schematu. Możesz przechowywać tutaj wiele właściwości, które obsługuje Model, takich jak @Field, @Enum, a nawet inny @Group.
// A pet with name and animal type.
final class Pet: Fields {
// The pet's name.
@Field(key: "name")
var name: String
// The type of pet.
@Field(key: "type")
var type: String
// Creates a new, empty Pet.
init() { }
}
Po utworzeniu definicji pól, możesz użyć jej jako wartości właściwości @Group.
final class User: Model {
// The user's nested pet.
@Group(key: "pet")
var pet: Pet
}
Pola @Group są dostępne za pomocą składni kropkowej.
let user: User = ...
print(user.pet.name) // String
Możesz wykonywać zapytania na zagnieżdżonych polach tak jak zwykle, używając składni kropkowej na opakowaniach właściwości.
User.query(on: database).filter(\.$pet.$name == "Zizek").all()
W bazie danych @Group jest przechowywane jako płaska struktura z kluczami połączonymi znakiem _. Poniżej znajduje się przykład, jak User wyglądałby w bazie danych.
| id | name | pet_name | pet_type |
|---|---|---|---|
| 1 | Tanner | Zizek | Cat |
| 2 | Logan | Runa | Dog |
Codable#
Modele domyślnie są zgodne z Codable. Oznacza to, że możesz używać swoich modeli z API Content Vapora, dodając zgodność z protokołem Content.
extension Planet: Content { }
app.get("planets") { req async throws in
// Return an array of all planets.
try await Planet.query(on: req.db).all()
}
Podczas serializacji do / z Codable, właściwości modelu będą używać swoich nazw zmiennych zamiast kluczy. Relacje będą serializowane jako zagnieżdżone struktury, a wszelkie chętnie (eager) załadowane dane zostaną uwzględnione.
Info
Zalecamy, aby w niemal wszystkich przypadkach używać DTO zamiast modelu dla odpowiedzi API i ciał żądań. Zobacz Data Transfer Object, aby uzyskać więcej informacji.
Data Transfer Object#
Domyślna zgodność modelu z Codable może ułatwić proste zastosowania i prototypowanie. Jednakże, naraża ona informacje z bazy danych na dostęp przez API. Jest to zazwyczaj niepożądane zarówno z punktu widzenia bezpieczeństwa - zwracanie wrażliwych pól, takich jak hash hasła użytkownika, to zły pomysł - jak i z punktu widzenia użyteczności. Utrudnia to zmianę schematu bazy danych bez naruszania API, przyjmowanie lub zwracanie danych w innym formacie, czy dodawanie lub usuwanie pól z API.
W większości przypadków powinieneś używać DTO, czyli obiektu transferu danych (data transfer object), zamiast modelu (znanego również jako domain transfer object). DTO to osobny typ zgodny z Codable, reprezentujący strukturę danych, którą chcesz zakodować lub zdekodować. Oddzielają one twoje API od schematu bazy danych i pozwalają na wprowadzanie zmian w modelach bez naruszania publicznego API twojej aplikacji, posiadanie różnych wersji oraz sprawiają, że twoje API jest wygodniejsze w użyciu dla twoich klientów.
Załóżmy poniższy model User w kolejnych przykładach.
// 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
}
Jednym z częstych zastosowań DTO jest implementacja żądań PATCH. Te żądania zawierają wartości tylko dla pól, które powinny zostać zaktualizowane. Próba zdekodowania Model bezpośrednio z takiego żądania zakończyłaby się niepowodzeniem, gdyby brakowało któregoś z wymaganych pól. Poniższy przykład pokazuje użycie DTO do zdekodowania danych żądania i zaktualizowania modelu.
// Structure of PATCH /users/:id request.
struct PatchUser: Decodable {
var firstName: String?
var lastName: String?
}
app.patch("users", ":id") { req async throws -> User in
// Decode the request data.
let patch = try req.content.decode(PatchUser.self)
// Fetch the desired user from the database.
guard let user = try await User.find(req.parameters.get("id"), on: req.db) else {
throw Abort(.notFound)
}
// If first name was supplied, update it.
if let firstName = patch.firstName {
user.firstName = firstName
}
// If new last name was supplied, update it.
if let lastName = patch.lastName {
user.lastName = lastName
}
// Save the user and return it.
try await user.save(on: req.db)
return user
}
Innym częstym zastosowaniem DTO jest dostosowywanie formatu odpowiedzi twojego API. Poniższy przykład pokazuje, jak DTO może zostać użyte do dodania obliczanego pola do odpowiedzi.
// Structure of GET /users response.
struct GetUser: Content {
var id: UUID
var name: String
}
app.get("users") { req async throws -> [GetUser] in
// Fetch all users from the database.
let users = try await User.query(on: req.db).all()
return try users.map { user in
// Convert each user to GET return type.
try GetUser(
id: user.requireID(),
name: "\(user.firstName) \(user.lastName)"
)
}
}
Innym częstym zastosowaniem jest obsługa relacji, takich jak relacje typu parent lub children. Zobacz dokumentację Parent, aby zobaczyć przykład użycia DTO w celu ułatwienia dekodowania modelu z relacją @Parent.
Nawet jeśli struktura DTO jest identyczna ze zgodnością modelu z Codable, posiadanie jej jako osobnego typu może pomóc utrzymać porządek w dużych projektach. Jeśli kiedykolwiek będziesz musiał wprowadzić zmianę we właściwościach swoich modeli, nie musisz martwić się o naruszenie publicznego API twojej aplikacji. Możesz również rozważyć umieszczenie swoich DTO w osobnym pakiecie, który może być współdzielony z konsumentami twojego API, dodając zgodność z Content w twojej aplikacji Vapor.
Alias#
Protokół ModelAlias pozwala na jednoznaczną identyfikację modelu, który jest łączony (joined) wielokrotnie w zapytaniu. Aby uzyskać więcej informacji, zobacz joins.
Save#
Aby zapisać model do bazy danych, użyj metody save(on:).
planet.save(on: database)
Ta metoda wewnętrznie wywoła create lub update, w zależności od tego, czy model już istnieje w bazie danych.
Create#
Możesz wywołać metodę create, aby zapisać nowy model do bazy danych.
let planet = Planet(name: "Earth")
planet.create(on: database)
create jest również dostępne dla tablicy modeli. Zapisuje to wszystkie modele do bazy danych w pojedynczej partii / zapytaniu (batch / query).
// Example of batch create.
[earth, mars].create(on: database)
Warning
Modele używające @ID(custom:) z generatorem .database (zazwyczaj automatycznie zwiększane Int) nie będą miały dostępu do swoich nowo utworzonych identyfikatorów po zbiorczym utworzeniu (batch create). W sytuacjach, gdy potrzebujesz uzyskać dostęp do identyfikatorów, wywołaj create na każdym modelu osobno.
Aby utworzyć tablicę modeli osobno, użyj map + flatten.
[earth, mars].map { $0.create(on: database) }
.flatten(on: database.eventLoop)
Jeśli używasz async/await, możesz użyć:
await withThrowingTaskGroup(of: Void.self) { taskGroup in
[earth, mars].forEach { model in
taskGroup.addTask { try await model.create(on: database) }
}
}
Update#
Możesz wywołać metodę update, aby zapisać model, który został pobrany z bazy danych.
guard let planet = try await Planet.find(..., on: database) else {
throw Abort(.notFound)
}
planet.name = "Earth"
try await planet.update(on: database)
Aby zaktualizować tablicę modeli, użyj map + flatten.
[earth, mars].map { $0.update(on: database) }
.flatten(on: database.eventLoop)
// TOOD
Query#
Modele udostępniają statyczną metodę query(on:), która zwraca query builder.
Planet.query(on: database).all()
Dowiedz się więcej o wykonywaniu zapytań w sekcji query.
Find#
Modele mają statyczną metodę find(_:on:) do wyszukiwania instancji modelu na podstawie identyfikatora.
Planet.find(req.parameters.get("id"), on: database)
Ta metoda zwraca nil, jeśli nie znaleziono modelu z tym identyfikatorem.
Lifecycle#
Middleware modeli pozwala na podłączenie się do zdarzeń cyklu życia twojego modelu. Wspierane są następujące zdarzenia cyklu życia.
| Method | Description |
|---|---|
create |
Runs before a model is created. |
update |
Runs before a model is updated. |
delete(force:) |
Runs before a model is deleted. |
softDelete |
Runs before a model is soft deleted. |
restore |
Runs before a model is restored (opposite of soft delete). |
Middleware modeli są deklarowane za pomocą protokołu ModelMiddleware lub AsyncModelMiddleware. Wszystkie metody cyklu życia mają domyślną implementację, więc musisz zaimplementować tylko te metody, których potrzebujesz. Każda metoda przyjmuje dany model, referencję do bazy danych oraz następną akcję w łańcuchu. Middleware może zdecydować się na wcześniejszy zwrot, zwrócenie nieudanego future, lub wywołanie następnej akcji, aby kontynuować normalnie.
Za pomocą tych metod możesz wykonywać akcje zarówno przed, jak i po zakończeniu określonego zdarzenia. Wykonywanie akcji po zakończeniu zdarzenia można zrealizować, mapując future zwrócony przez następnego respondenta.
// Example middleware that capitalizes names.
struct PlanetMiddleware: ModelMiddleware {
func create(model: Planet, on db: Database, next: AnyModelResponder) -> EventLoopFuture<Void> {
// The model can be altered here before it is created.
model.name = model.name.capitalized()
return next.create(model, on: db).map {
// Once the planet has been created, the code
// here will be executed.
print ("Planet \(model.name) was created")
}
}
}
lub jeśli używasz async/await:
struct PlanetMiddleware: AsyncModelMiddleware {
func create(model: Planet, on db: Database, next: AnyAsyncModelResponder) async throws {
// The model can be altered here before it is created.
model.name = model.name.capitalized()
try await next.create(model, on: db)
// Once the planet has been created, the code
// here will be executed.
print ("Planet \(model.name) was created")
}
}
Po utworzeniu middleware, możesz go włączyć za pomocą app.databases.middleware.
// Example of configuring model middleware.
app.databases.middleware.use(PlanetMiddleware(), on: .psql)
Database Space#
Fluent wspiera ustawianie przestrzeni (space) dla modelu, co pozwala na podzielenie poszczególnych modeli Fluent między schematami PostgreSQL, bazami danych MySQL oraz wieloma podłączonymi bazami danych SQLite. MongoDB nie wspiera przestrzeni w chwili pisania tego tekstu. Aby umieścić model w przestrzeni innej niż domyślna, dodaj do modelu nową właściwość statyczną:
public static let schema = "planets"
public static let space: String? = "mirror_universe"
// ...
Fluent użyje tego podczas budowania wszystkich zapytań do bazy danych.