Home iOS & Swift Books Server-Side Swift with Vapor

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:

  1. Define a class that conforms to Model.
  2. Specify the schema as required by Model. This is the name of the table in the database.
  3. 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.
  4. Define two String properties to hold the acronym and its definition. These use the @Field property wrapper to denote a generic database field. The key parameter is the name of the column in the database.
  5. Provide an empty initializer as required by Model. Fluent uses this to initialize models returned from database queries.
  6. 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:

  1. Define a new type, CreateAcronym that conforms to Migration.
  2. Implement prepare(on:) as required by Migration. You call this method when you run your migrations.
  3. Define the table name for this model. This must match schema from the model.
  4. Define the ID column in the database.
  5. Define columns for short and long. Set the column type to string and mark the columns as required. This matches the non-optional String properties in the model. The field names must match the key of the property wrapper, not the name of the property itself.
  6. Create the table in the database.
  7. Implement revert(on:) as required by Migration. You call this function when you revert your migrations. This deletes the table referenced with schema(_:).

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:

  1. Add CreateAcronym to the list of migrations to run.
  2. Set the log level for the application to debug. This provides more information and enables you to see your migrations.
  3. 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 an EventLoop.

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:

  1. Register a new route at /api/acronyms that accepts a POST request and returns EventLoopFuture<Acronym>. It returns the acronym once it’s saved.
  2. Decode the request’s JSON into an Acronym model using Codable.
  3. Save the model using Fluent and the database from Request.
  4. save(on:) returns EventLoopFuture<Void> so use map 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:

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC