Models¶
Models represent data stored in tables or collections in your database. Models have one or more fields that store codable values. All models have a unique identifier. Property wrappers are used to denote identifiers, fields, and relations.
Below is an example of a simple model with one field. Note that models do not describe the entire database schema, such as constraints, indexes, and foreign keys. Schemas are defined in migrations. Models are focused on representing the data stored in your database schemas.
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¶
All models require a static, get-only schema
property. This string references the name of the table or collection this model represents.
final class Planet: Model {
// Name of the table or collection.
static let schema = "planets"
}
When querying this model, data will be fetched from and stored to the schema named "planets"
.
Tip
The schema name is typically the class name pluralized and lowercased.
Identifier¶
All models must have an id
property defined using the @ID
property wrapper. This field uniquely identifies instances of your model.
final class Planet: Model {
// Unique identifier for this Planet.
@ID(key: .id)
var id: UUID?
}
By default, the @ID
property should use the special .id
key which resolves to an appropriate key for the underlying database driver. For SQL this is "id"
and for NoSQL it is "_id"
.
The @ID
should also be of type UUID
. This is the only identifier value currently supported by all database drivers. Fluent will automatically generate new UUID identifiers when models are created.
@ID
has an optional value since unsaved models may not have an identifier yet. To get the identifier or throw an error, use requireID
.
let id = try planet.requireID()
Exists¶
@ID
has an exists
property that represents whether the model exists in the database or not. When you initialize a model, the value is false
. After you save a model or when you fetch a model from the database, the value is true
. This property is mutable.
if planet.$id.exists {
// This model exists in database.
}
Custom Identifier¶
Fluent supports custom identifier keys and types using the @ID(custom:)
overload.
final class Planet: Model {
// Unique identifier for this Planet.
@ID(custom: "foo")
var id: Int?
}
The above example uses an @ID
with custom key "foo"
and identifier type Int
. This is compatible with SQL databases using auto-incrementing primary keys, but is not compatible with NoSQL.
Custom @ID
s allow the user to specify how the identifier should be generated using the generatedBy
parameter.
@ID(custom: "foo", generatedBy: .user)
The generatedBy
parameter supports these cases:
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. |
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.
Initializer¶
Models must have an empty initializer method.
final class Planet: Model {
// Creates a new, empty Planet.
init() { }
}
Fluent requires this method internally to initialize models returned by queries. It is also used for reflection.
You may want to add a convenience initializer to your model that accepts all properties.
final class Planet: Model {
// Creates a new Planet with all properties set.
init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
}
Using convenience initializers makes it easier to add new properties to the model in the future.
Field¶
Models can have zero or more @Field
properties for storing data.
final class Planet: Model {
// The Planet's name.
@Field(key: "name")
var name: String
}
Fields require the database key to be explicitly defined. This is not required to be the same as the property name.
Tip
Fluent recommends using snake_case
for database keys and camelCase
for property names.
Field values can be any type that conforms to Codable
. Storing nested structures and arrays in @Field
is supported, but filtering operations are limited. See @Group
for an alternative.
For fields that contain an optional value, use @OptionalField
.
@OptionalField(key: "tag")
var tag: String?
Warning
A non-optional field that has a willSet
property observer that references its current value or a didSet
property observer that references its oldValue
will result in a fatal error.
Relations¶
Models can have zero or more relation properties referencing other models like @Parent
, @Children
, and @Siblings
. Learn more about relations in the relations section.
Timestamp¶
@Timestamp
is a special type of @Field
that stores a Foundation.Date
. Timestamps are set automatically by Fluent according to the chosen 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
supports the following triggers.
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. |
@Timestamp
's date value is optional and should be set to nil
when initializing a new model.
Timestamp Format¶
By default, @Timestamp
will use an efficient datetime
encoding based on your database driver. You can customize how the timestamp is stored in the database using the format
parameter.
// Stores an ISO 8601 formatted timestamp representing
// when this model was last updated.
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?
Note that the associated migration for this .iso8601
example would require storage in .string
format.
.field("updated_at", .string)
Available timestamp formats are listed below.
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 |
You can access the raw timestamp value directly using the timestamp
property.
// Manually set the timestamp value on this ISO 8601
// formatted @Timestamp.
model.$updatedAt.timestamp = "2020-06-03T16:20:14+00:00"
Soft Delete¶
Adding a @Timestamp
that uses the .delete
trigger to your model will enable soft-deletion.
final class Planet: Model {
// When this Planet was deleted.
@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?
}
Soft-deleted models still exist in the database after deletion, but will not be returned in queries.
Tip
You can manually set an on delete timestamp to a date in the future. This can be used as an expiration date.
To force a soft-deletable model to be removed from the database, use the force
parameter in delete
.
// Deletes from the database even if the model
// is soft deletable.
model.delete(force: true, on: database)
To restore a soft-deleted model, use the restore
method.
// Clears the on delete timestamp allowing this
// model to be returned in queries.
model.restore(on: database)
To include soft-deleted models in a query, use withDeleted
.
// Fetches all planets including soft deleted.
Planet.query(on: database).withDeleted().all()
Enum¶
@Enum
is a special type of @Field
for storing string representable types as native database enums. Native database enums provide an added layer of type safety to your database and may be more performant than raw enums.
// 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
}
Only types conforming to RawRepresentable
where RawValue
is String
are compatible with @Enum
. String
backed enums meet this requirement by default.
To store an optional enum, use @OptionalEnum
.
The database must be prepared to handle enums via a migration. See enum for more information.
Raw Enums¶
Any enum backed by a Codable
type, like String
or Int
, can be stored in @Field
. It will be stored in the database as the raw value.
Group¶
@Group
allows you to store a nested group of fields as a single property on your model. Unlike Codable structs stored in a @Field
, the fields in a @Group
are queryable. Fluent achieves this by storing @Group
as a flat structure in the database.
To use a @Group
, first define the nested structure you would like to store using the Fields
protocol. This is very similar to Model
except no identifier or schema name is required. You can store many properties here that Model
supports like @Field
, @Enum
, or even another @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() { }
}
After you've created the fields definition, you can use it as the value of a @Group
property.
final class User: Model {
// The user's nested pet.
@Group(key: "pet")
var pet: Pet
}
A @Group
's fields are accessible via dot-syntax.
let user: User = ...
print(user.pet.name) // String
You can query nested fields like normal using dot-syntax on the property wrappers.
User.query(on: database).filter(\.$pet.$name == "Zizek").all()
In the database, @Group
is stored as a flat structure with keys joined by _
. Below is an example of how User
would look in the database.
id | name | pet_name | pet_type |
---|---|---|---|
1 | Tanner | Zizek | Cat |
2 | Logan | Runa | Dog |
Codable¶
Models conform to Codable
by default. This means you can use your models with Vapor's content API by adding conformance to the Content
protocol.
extension Planet: Content { }
app.get("planets") { req async throws in
// Return an array of all planets.
try await Planet.query(on: req.db).all()
}
When serializing to / from Codable
, model properties will use their variable names instead of keys. Relations will serialize as nested structures and any eager loaded data will be included.
Info
We recommend that for almost all cases you use a DTO instead of a model for your API responses and request bodies. See Data Transfer Object for more information.
Data Transfer Object¶
Model's default Codable
conformance can make simple usage and prototyping easier. However, it exposes the underlying database information to the API. This is usually not desirable from both a security standpoint - returning sensitive fields such as a user's password hash is a bad idea - and a usability point of view. It makes it difficult to change the database schema without breaking the API, accept or return data in a different format, or to add or remove fields from the API.
For most cases you shoud use a DTO, or data transfer object instead of a model (this is also known as a domain transfer object). A DTO is a separate Codable
type representing the data structure you would like to encode or decode. These decouple your API from your database schema and allow you to make changes to your models without breaking your app's public API, have different versions and make your API nicer to use for your clients.
Assume the following User
model in the upcoming examples.
// 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
}
One common use case for DTOs is in implementing PATCH
requests. These requests only include values for fields that should be updated. Attempting to decode a Model
directly from such a request would fail if any of the required fields were missing. In the example below, you can see a DTO being used to decode request data and update a model.
// 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
}
Another common use case for DTOs is customizing the format of your API responses. The example below shows how a DTO can be used to add a computed field to a response.
// 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)"
)
}
}
Another common use case is when dealing with relations, such as parent relations or children relations. See the Parent documentation for an example of how to use a DTO to make it easy to decode a model with a @Parent
relation.
Even if the DTO's structure is identical to model's Codable
conformance, having it as a separate type can help keep large projects tidy. If you ever need to make a change to your models properties, you don't have to worry about breaking your app's public API. You may also consider putting your DTOs in a separate package that can be shared with consumers of your API and adding Content
conformance in your Vapor app.
Alias¶
The ModelAlias
protocol lets you uniquely identify a model being joined multiple times in a query. For more information, see joins.
Save¶
To save a model to the database, use the save(on:)
method.
planet.save(on: database)
This method will call create
or update
internally depending on whether the model already exists in the database.
Create¶
You can call the create
method to save a new model to the database.
let planet = Planet(name: "Earth")
planet.create(on: database)
create
is also available on an array of models. This saves all of the models to the database in a single batch / query.
// Example of batch create.
[earth, mars].create(on: database)
Warning
Models using @ID(custom:)
with the .database
generator (usually autoincrementing Int
s) will not have their newly created identifiers accessible after batch create. For situations where you need to access the identifiers, call create
on each model.
To create an array of models separately, use map
+ flatten
.
[earth, mars].map { $0.create(on: database) }
.flatten(on: database.eventLoop)
If using async
/await
you can use:
await withThrowingTaskGroup(of: Void.self) { taskGroup in
[earth, mars].forEach { model in
taskGroup.addTask { try await model.create(on: database) }
}
}
Update¶
You can call the update
method to save a model that was fetched from the database.
guard let planet = try await Planet.find(..., on: database) else {
throw Abort(.notFound)
}
planet.name = "Earth"
try await planet.update(on: database)
To update an array of models, use map
+ flatten
.
[earth, mars].map { $0.update(on: database) }
.flatten(on: database.eventLoop)
// TOOD
Query¶
Models expose a static method query(on:)
that returns a query builder.
Planet.query(on: database).all()
Learn more about querying in the query section.
Find¶
Models have a static find(_:on:)
method for looking up a model instance by identifier.
Planet.find(req.parameters.get("id"), on: database)
This method returns nil
if no model with that identifier was found.
Lifecycle¶
Model middleware allow you to hook into your model's lifecycle events. The following lifecycle events are supported.
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). |
Model middleware are declared using the ModelMiddleware
or AsyncModelMiddleware
protocol. All lifecycle methods have a default implementation, so you only need to implement the methods you require. Each method accepts the model in question, a reference to the database, and the next action in the chain. The middleware can choose to return early, return a failed future, or call the next action to continue normally.
Using these methods you can perform actions both before and after the specific event completes. Performing actions after the event completes can be done by mapping the future returned from the next responder.
// 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")
}
}
}
or if using 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")
}
}
Once you have created your middleware, you can enable it using app.databases.middleware
.
// Example of configuring model middleware.
app.databases.middleware.use(PlanetMiddleware(), on: .psql)
Database Space¶
Fluent supports the setting of a space for a Model, which allows the partitioning of individual Fluent models between PostgreSQL schemas, MySQL databases, and multiple attached SQLite databases. MongoDB does not support spaces at the time of this writing. To place a model in a space other than the default, add a new static property to the model:
public static let schema = "planets"
public static let space: String? = "mirror_universe"
// ...
Fluent will use this when building all database queries.