5.
Fluent & Persisting Models
Written by Tim Condon
In Chapter 2, “Hello, Vapor!”, you learned the basics of creating a Vapor app, including how to create routes. This chapter explains how to use Fluent to save data in Vapor applications. You’ll need to have Docker installed and running. Visit https://www.docker.com/get-docker and follow the instructions to install it.
Fluent
Fluent is Vapor’s ORM or object relational mapping tool. It’s an abstraction layer between the Vapor application and the database, and it’s designed to make working with databases easier. Using an ORM such as Fluent has a number of benefits.
The biggest benefit is you don’t have to use the database directly! When you interact directly with a database, you write database queries as strings. These aren’t type-safe and can be painful to use from Swift.
Fluent benefits you by allowing you to use any of a number of database engines, even in the same app. Finally, you don’t need to know how to write queries since you can interact with your models in a “Swifty” way.
Models are the Swift representation of your data and are used throughout Fluent. Models are the objects, such as user profiles, you save and access in your database. Fluent returns and uses type-safe models when interacting with the database, giving you compile-time safety.
Acronyms
Over the next several chapters, you’ll build a complex “Today I Learned” application that can save different acronyms and their meanings. Start by creating a new project, using the Vapor Toolbox. In Terminal, enter the following command:
cd ~/vapor
This command takes you into a directory called vapor inside your home directory and assumes that you completed the steps in Chapter 2, “Hello, Vapor!”. Next, enter:
vapor new TILApp
When asked if you’d like to use Fluent enter y and then press Enter. Next enter 1 to choose PostgreSQL as the database, followed by Enter. When the toolbox asks if you want to use Leaf or other dependencies, enter n, followed by Enter. This creates a new Vapor project called TILApp using the template and configuring PostgreSQL as the database.
The TIL app uses PostgreSQL throughout the book. However, it should work without any modifications with any database supported by Fluent. You’ll learn how to configure different databases in Chapter 6, “Configuring a Database”.
The template provides example files for models, migrations and controllers. You’ll build your own so delete the examples. In Terminal, enter:
cd TILApp
rm -rf Sources/App/Models/*
rm -rf Sources/App/Migrations/*
rm -rf Sources/App/Controllers/*
If prompted to confirm the deletions, enter y. Now, open the project in Xcode:
open Package.swift
This creates an Xcode project from your Swift package, using Xcode’s support for Swift Package Manager. It takes a while to download all the dependencies for the first time. When it’s finished, you’ll see the dependencies in the sidebar and a TILApp scheme available:
First, open configure.swift and delete the following line:
app.migrations.add(CreateTodo())
Next, open routes.swift and delete the following line:
try app.register(collection: TodoController())
This removes the remaining references to the template’s example model migration and controller.
Create a new Swift file in Sources/App/Models called Acronym.swift. Inside the new file, insert the following:
import Vapor
import Fluent
// 1
final class Acronym: Model {
// 2
static let schema = "acronyms"
// 3
@ID
var id: UUID?
// 4
@Field(key: "short")
var short: String
@Field(key: "long")
var long: String
// 5
init() {}
// 6
init(id: UUID? = nil, short: String, long: String) {
self.id = id
self.short = short
self.long = long
}
}
Here’s what this code is doing:
- Define a class that conforms to
Model
. - Specify the
schema
as required byModel
. This is the name of the table in the database. - Define an optional
id
property that stores the ID of the model, if one has been set. This is annotated with Fluent’s@ID
property wrapper. This tells Fluent what to use to look up the model in the database. - Define two
String
properties to hold the acronym and its definition. These use the@Field
property wrapper to denote a generic database field. Thekey
parameter is the name of the column in the database. - Provide an empty initializer as required by
Model
. Fluent uses this to initialize models returned from database queries. - Provide an initializer to create the model as required.
If you’re coming from Fluent 3, this model looks very different. Fluent 4 leverages property wrappers to provide strong and complex database integration. @ID
marks a property as the ID for that table. Fluent uses this property wrapper to perform queries in the database when finding models. The property wrapper is also used for relationships, which you’ll learn about in the next chapters. By default in Fluent, the ID must be a UUID
and called id
.
@Field
marks the property of a model as a generic column in the database. Fluent uses the property wrapper for performing queries with filters. The use of property wrappers allows Fluent to update individual fields in a model, rather than the entire model. You can also select specified fields from the database instead of all fields for a model. Note that you should only use @Field
with non-optional properties. If you have an optional property in your model you should use @OptionalField
.
To save the model in the database, you must create a table for it. Fluent does this with a migration. Migrations allow you to make reliable, testable, reproducible changes to your database. They are commonly used to create a database schema, or table description, for your models. They are also used to seed data into your database or make changes to your models after they’ve been saved.
Fluent 3 could infer a lot of the table information for you. However this didn’t scale to large complex projects, especially when you need to add or remove columns or even rename them. In Xcode, create a new Swift file in Sources/App/Migrations called CreateAcronym.swift.
Insert the following into the new file:
import Fluent
// 1
struct CreateAcronym: Migration {
// 2
func prepare(on database: Database) -> EventLoopFuture<Void> {
// 3
database.schema("acronyms")
// 4
.id()
// 5
.field("short", .string, .required)
.field("long", .string, .required)
// 6
.create()
}
// 7
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("acronyms").delete()
}
}
Here’s what the migration is doing:
- Define a new type,
CreateAcronym
that conforms toMigration
. - Implement
prepare(on:)
as required byMigration
. You call this method when you run your migrations. - Define the table name for this model. This must match
schema
from the model. - Define the ID column in the database.
- Define columns for
short
andlong
. Set the column type tostring
and mark the columns as required. This matches the non-optionalString
properties in the model. The field names must match the key of the property wrapper, not the name of the property itself. - Create the table in the database.
- Implement
revert(on:)
as required byMigration
. You call this function when you revert your migrations. This deletes the table referenced withschema(_:)
.
All references to column names and table names are strings. This is deliberate as using properties causes issues if those property names change in the future. Chapter 35, “Production Concerns & Redis” describes one solution for improving this and making it type-safe.
Migrations only run once; once they have run in a database, they are never executed again. It’s important to remember this as Fluent won’t attempt to recreate a table if you change the migration.
Now that you have a migration for Acronym
you can tell Fluent to create the table. Open configure.swift and, after app.databases.use(_:as:)
, add the following:
// 1
app.migrations.add(CreateAcronym())
// 2
app.logger.logLevel = .debug
// 3
try app.autoMigrate().wait()
Here’s what your new code does:
- Add
CreateAcronym
to the list of migrations to run. - Set the log level for the application to debug. This provides more information and enables you to see your migrations.
- Automatically run migrations and wait for the result. Fluent allows you to choose when to run your migrations. This is helpful when you need to schedule them, for example. You can use
wait()
here since you’re not running on anEventLoop
.
To test with PostgreSQL, you’ll run the Postgres server in a Docker container. Open Terminal
and enter the following command:
docker run --name postgres -e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
Here’s what this does:
- Run a new container named postgres.
- Specify the database name, username and password through environment variables.
- Allow applications to connect to the Postgres server on its default port: 5432.
- Run the server in the background as a daemon.
- Use the Docker image named postgres for this container. If the image is not present on your machine, Docker automatically downloads it.
To check that your database is running, enter the following in Terminal to list all active containers:
docker ps
Now you’re ready to run the app! Set the active scheme to TILApp with My Mac as the destination. Build and run. Check the console and see that the migrations have run.
You should see something similar to the following:
Saving models
When your app’s user enters a new acronym, you need a way to save it.
In Vapor 4, Codable
makes this trivial. Vapor provides Content
, a wrapper around Codable
, which allows you to convert models and other data between various formats.
This is used extensively in Vapor, and you’ll see it throughout the book.
Open Acronym.swift and add the following to the end of the file to make Acronym
conform to Content
:
extension Acronym: Content {}
Since Acronym
already conforms to Codable
via Model
, you don’t have to add anything else. To create an acronym, the user’s browser sends a POST request containing a JSON payload that looks similar to the following:
{
"short": "OMG",
"long": "Oh My God"
}
You’ll need a route to handle this POST request and save the new acronym. Open routes.swift and add the following to the end of routes(_:)
:
// 1
app.post("api", "acronyms") { req -> EventLoopFuture<Acronym> in
// 2
let acronym = try req.content.decode(Acronym.self)
// 3
return acronym.save(on: req.db).map {
// 4
acronym
}
}
Here’s what this does:
- Register a new route at /api/acronyms that accepts a POST request and returns
EventLoopFuture<Acronym>
. It returns the acronym once it’s saved. - Decode the request’s JSON into an
Acronym
model usingCodable
. - Save the model using Fluent and the database from
Request
. -
save(on:)
returnsEventLoopFuture<Void>
so usemap
to return the acronym when the save completes.
Fluent and Vapor’s integrated use of Codable
makes this simple. Since Acronym
conforms to Content
, it’s easily converted between JSON and Model
.
This allows Vapor to return the model as JSON in the response without any effort on your part. Build and run the application to try it out.
A good tool to test this is RESTed, available as a free download from the Mac App Store. Other tools such as Paw and Postman are suitable as well.
In RESTed, configure the request as follows:
- URL: http://localhost:8080/api/acronyms
- method: POST
- Parameter encoding: JSON-encoded
Add two parameters with names and values:
- short: OMG
- long: Oh My God
Setting the parameter encoding to JSON-encoded ensures the data is sent as JSON.
It’s important to note this also sets the Content-Type
header to application/json
, which tells Vapor the request contains JSON. If you’re using a different client to send the request, you may need to set this manually.
Click Send Request and you’ll see the acronym provided in the response.
The id
field will have a value as it has now been saved in the database:
Where to go from here?
This chapter has introduced you to Fluent and how to create models in Vapor and save them in the database. The next chapters build on this application to create a full-featured TIL application.