Server-Side Swift Beta

Learn web development with Swift, using Vapor and Kitura.

Database Migrations with Vapor

In this Server-Side Swift tutorial, see how to perform various migrations on your Vapor application database, using PostgreSQL running on Docker.

5/5 1 Rating

Version

  • Swift 5, macOS 10.14, Xcode 11

Data is at the center of your application and at some point, how you represent that data is going to change. In this server-side Swift tutorial, you’ll learn how to migrate the data for your Vapor applications. You’ll use PostgreSQL running on Docker to store your data. You’ll learn how to view your data in a PostgreSQL client for macOS, how to add fields for your model, remove fields, how to add constraints, and how to seed your database with values.

Note: This tutorial assumes you have some experience with using Vapor to build web apps, as well as have a basic understanding of Fluent. See Getting Started with Server-side Swift with Vapor if you’re new to Vapor and Using Fluent and Persisting Models in Vapor if you’re new to Fluent. This tutorial uses PostgreSQL on Docker. If you need a refresher on Docker, see Docker on macOS: Getting Started.

Getting Started

Download the starter project at the top or bottom of this page by clicking on the Download Materials button. RoomFinder is a Vapor application to model conference rooms for your new startup. Unzip the downloaded file and then from a Terminal, cd into the folder that was unzipped and enter the following commands:

cd RoomFinder-Starter
open Package.swift

The first command takes you into the directory of the starter project for the RoomFinder app. The second command opens the project in Xcode, using Xcode 11’s new SwiftPM integrations.

Running PostgreSQL On Docker

This tutorial requires you to run PostgreSQL on Docker. Assuming you have Docker installed and running on macOS, in Terminal, enter the following command:

docker run --name postgres -e POSTGRES_DB=vapor \
-e POSTGRES_USER=vapor -e POSTGRES_PASSWORD=password \
-p 5432:5432 -d postgres

This command:

  • Runs a new container named postgres.
  • Pulls down the image if it currently does not exist on your machine.
  • Specifies the database name, username and password through environment variables.
  • Maps the host port 5432, which is the default port for Postgres, to the container port 5432.

The steps to connect to Postgres through your Vapor application are already implemented in the starter project in Sources/App/configure.swift.

Create a Room Model

You’ll start by creating a model class for the conference room data you want to migrate.

In Xcode, create a new file within the Sources/App/Models folder, and call the new file Room.swift:

Add Room.swift

Change the contents of the new file to the following:

import FluentPostgreSQL
import Vapor

final class Room: Codable {
  var id: UUID?
}

The model class contains a single optional UUID property to hold a room’s unique identifier if it has been set. Once the Room has been saved, it’s ID is set. All Fluent models must conform to Codable. The class is marked final to provide performance benefits by the compiler.

Next, you’ll add two extensions to Room.swift that make the Room class conform to convenience protocols. First,

extension Room: PostgreSQLUUIDModel {}

The Fluent packages provide Model convenience protocols for each database provider and tell Fluent which type the ID will be. There’s also PostgreSQLModel and PostgreSQLStringModel protocols for models with IDs of type Int or String.

Now add the second extension:

extension Room: Content {}

The Content protocol is added so that you can use this model in your router.

Introducing Postico

Postico is a great free PostgresQL client for macOS that you can use for this tutorial. There is a paid version as well that gives you more advanced features, but you’ll only need the free features. You can download it at https://eggerapps.at/postico/.

Once Postico is installed, you can connect to PostgreSQL on Docker just like you would if PostgreSQL was running on your local machine without Docker since you mapped your local port of 5432 to the container’s port of 5432. Enter the credentials you used earlier when creating your container, as in the screenshot below:

Postico

Remember your super secret password was password.

When you first connect Postico to your database, it should be empty:

Empty database in Postico

Create the Room Migration

In order to save your Room model data into the database, you must create a table for it. This is done through a migration. Migrations allow you to make repeatable changes to your database. By using migrations, you allow another team member to follow in your footsteps by running the same set of migrations. If you start altering the database schema outside of Fluent, other team members will be lost without those same set of manual migrations you did. It’s best practice to let the Fluent framework handle the migration for you.

In the Room model, add the following extension at the bottom:

extension Room: Migration {}

Now that your Room model conforms to Migration, you’re able to tell Fluent to create the table when the app starts. In configure.swift in the Sources/App folder, find the section labeled // Configure migrations. Add the following before services.register(migrations):

migrations.add(model: Room.self, database: .psql)

Set the active scheme to Run with My Mac as the destination. Build and run. In the console, you should see that your migration was run. You should see something similar to the console output below:

Running Migrations

Now that your first migration has been run, in Postico you can click the refresh icon in the toolbar on the right. You should now see two tables, Fluent and Room:

Postico after first run

The Fluent table is used by the Fluent framework to keep track of the migrations. Double-click on it and you should see one entry like the following:

Fluent table

Breaking down the columns, the entry has a unique id, which is of type UUID, that is used as the primary key in the Fluent table. The name column is the name of the migration. This is the name of the migration you added in configure.swift. The batch column is the group of migrations that were run at a single time. If multiple migrations are performed at the same time, they will all have the same batch number. This way, when Fluent reverts a single migration, Fluent will roll back the migrations part of the same batch. The createdAt and updatedAt columns are the timestamps the migration was created at or updated at, respectively.

When a new migration is executed, a row will be entered in the Fluent table. If you run your app again, you will not see the Preparing migration ‘Room’ in the console. You should not edit this table manually: You’re only viewing it in Postico in order to see how Fluent works on your behalf.

Click on the vapor database icon in the top toolbar to go back to the tables view. Double-click on the Room table.

Room table

You can see the Room table is empty since you haven’t added any Rooms yet. You’ve only created the table to hold the Rooms. The table has a single id column that matches the ID you created in code.

Click on the bottom segmented control Structure item to see the table structure.

Room table structure

By looking at the table structure for the Room table, the id column’s type is uuid that matches the ID type you created in code on your Room model.

Update the Room Migration

At your startup, you quickly realize that engineers aren’t going to want to locate conference rooms by a UUID and would much rather have a name for the room. This sounds like it’s time to update your Room model!

In Xcode, create a new folder in the Sources/App directory called Migrations. Within this new folder, create a file called 0001_AddRoomName.swift. It’s best to have some sort of way to preserve the order your migrations need to be run in. Two options are to use the date of your migration or just to number them in order. Here you’ve chosen the latter.

In 0001_AddRoomName.swift, update the code to the following:

import FluentPostgreSQL
import Vapor

struct AddRoomName: Migration {

}

Notice the name of the struct does not include the 0001_ prefix. That’s just used to keep track of your migrations, but does not need to be part of the struct. Migrations are typically written as a struct when updating an existing model. Migrations require you to provide three things:

  • Fluent needs to know the type of database the migration can run on.
  • A function prepare(on:) used to change the database. In your case, you’ll add the property name to the Room model.
  • A function revert(on:) that is the reverse of prepare(on:). This is how Fluent knows how to undo a migration. In your case, removing the name property would undo the adding of the name property.

Add the following to AddRoomName:

// 1
typealias Database = PostgreSQLDatabase

static func prepare(on connection: PostgreSQLConnection) -> Future<Void> {
  // 2
  return Database.update(Room.self, on: connection) { builder in
    // 3
    builder.field(for: \.name)
  }
}

// 4
static func revert(on connection: PostgreSQLConnection) -> Future<Void> {
  return Database.update(Room.self, on: connection) { builder in
    builder.deleteField(for: \.name)
  }
}

In the code above, you:

  1. Tell Fluent that the database type is PostgreSQL.
  2. Tell Fluent which model to update. Notice you use update here. If you were creating a new model in a migration, you’d use create.
  3. Use the SchemaBuilder object to change your database schema. In this case, you are adding a field name for your Room model.
  4. In revert(on:), you do the opposite of what you did in prepare(on:) and delete the field name.

The name property does not exist on your Room model yet. In Room.swift, add the following to the Room class:

var name: String

init(name: String) {
  self.name = name
}

Now everything should compile, but in order for this migration to be run, you need to add it to the set of migrations to run in configure.swift. Below the previous migration you added and before services.register(migrations), add:

migrations.add(migration: AddRoomName.self, database: .psql)

Build and run. Refresh the view in Postico and you should see a new column name of type text was added for the Room table”

Updated Room table

Seeding Your Database

You have a set of conference rooms that are ready to be used, but they aren’t in the database yet. In this migration, you’ll seed the database with data. Create a new file in the Migrations directory called 0002_AddRooms.swift that you’ll use to add the rooms.

In 0002_AddRooms.swift, start by changing the contents to the following:

import FluentPostgreSQL
import Vapor

struct AddRooms: Migration {
  typealias Database = PostgreSQLDatabase
}

This creates the AddRooms migration using PostgreSQL as the database.

You start to think about what you want to name your conference rooms and decide on using Apple frameworks as their names. In AddRooms, add prepare(on:):

static func prepare(on connection: PostgreSQLConnection) -> Future<Void> {
  let room1 = Room(name: "Foundation")
  let room2 = Room(name: "UIKit")
  let room3 = Room(name: "SwiftUI")
  _ = room1.save(on: connection).transform(to: ())
  _ = room2.save(on: connection).transform(to: ())
  return room3.save(on: connection).transform(to: ())
}

This creates three Room models. You call save(on:) for each of the models in order to save the Room to the database. Because you don’t care about the result of the Future that’s returned, you can call transform(to: ()) on it.

Then add the following code that can be used to reverse the changes:

static func revert(on connection: PostgreSQLConnection) -> Future<Void> {
  let futures = ["Foundation", "UIKit", "SwiftUI"].map { name in
    return Room.query(on: connection).filter(\Room.name == name)
      .delete()
  }
  return futures.flatten(on: connection)
}

In revert(on:), you use Fluent’s query and filter methods to find the Rooms with the names you created in prepare(on:). Once you find them, you call delete() to remove those Rooms. Since you are waiting here for multiple futures to complete, you use flatten(on:).

In configure.swift, add this new migration after migrations.add(migration: AddRoomName.self, database: .psql) and before services.register(migrations):

migrations.add(migration: AddRooms.self, database: .psql)

Build and run. If you view the content of your Room table, you should see something similar to:

Seeded Room data

There should be three rows for the three Rooms you added in your migration.

Reverting Your Migration

So far, you’ve been adding fields and data to your Room table by calling prepare(on:) in your migrations. But how does revert(on:) work?

To revert the migrations, you need to add CommandConfig. At the bottom of the function in configure.swift, below services.register(migrations), add the following:

// 1
var commandConfig = CommandConfig.default()
// 2
commandConfig.useFluentCommands()
// 3
services.register(commandConfig)
  1. CommandConfig allows you to register commands to your container.
  2. This adds Fluent commands to the CommandConfig. Currently these are the migration commands revert and migrate.
  3. Registers the commandConfig service.

In order to run the revert command to revert your last migration, you need to add the revert option as a command-line argument. Edit your Run Scheme from the scheme chooser, or press Command-Option-R. This should look like:

Adding Revert

Then run your app. Since you are passing the revert argument on launch, in the console, Fluent asks if you want to revert the last back of migrations. Enter y and press Enter.

Now if you look at your Room table, you’ll see it’s empty:

Reverted Room table

And if you check your Fluent table, you see that it removed the last migration:

Reverted Fluent table

This is how you can revert a single migration. If you want to revert all of your migrations, you can pass revert --all.

Since you actually want the room data, remove the revert command-line argument and run the app again. You should once again now have three rooms in your Room table (you may need to hit the refresh button in Postico to see them).

Unique Room Migration

As your startup begins to grow and you get more conference rooms, as much as iOS developers are excited about SwiftUI, you don’t want someone to create another conference room with the same name. It’s time for another migration! This migration will add a unique key to the name property of the table.

Create a new file called 0003_UniqueRoomNames.swift in the Migrations directory and update the code to the following:

import FluentPostgreSQL
import Vapor

struct UniqueRoomNames: Migration {
  typealias Database = PostgreSQLDatabase

}

This sets up the struct for your unique room migration. Next add the prepare(on:) method:

static func prepare(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
  return Database.update(Room.self, on: conn) { builder in
    builder.unique(on: \.name)
  }
}

In this migration, you use Database.update(_:on:) like before to update your Room model. To make a property unique, you use the unique(on:) function, passing in the KeyPath of the unique field. In this case, it is the name KeyPath.

The reverse of this migration is to delete the unique key. Add the following code after prepare(on:):

static func revert(on conn: PostgreSQLConnection) -> EventLoopFuture<Void> {
  return Database.update(Room.self, on: conn) { builder in
    builder.deleteUnique(from: \.name)
  }
}

In order to run this migration, add it to the configure.swift file above services.register(migrations):

migrations.add(migration: UniqueRoomNames.self, database: .psql)

Before running the app again, there are already routes created for you to create a room and get all of the current rooms. In Sources/App/Controllers/RoomController.swift, uncomment the following code:

//  func rooms(_ req: Request) throws -> Future<[Room]> {
//    return Room.query(on: req).all()
//  }
//  
//  func create(_ req: Request) throws -> Future<Room> {
//    return try req.content.decode(Room.self).flatMap { room in
//      return room.save(on: req)
//    }
//  }

In Sources/App/routes.swift, uncomment the following code:

//  let roomController = RoomController()
//
//  router.get("rooms", use: roomController.rooms)
//  router.post("rooms", use: roomController.create)

Build and run.

Note: This tutorial uses the RESTed app, available as a free download from the Mac App Store. If you like, you may use another REST client to test your APIs.

In the RESTed app, set up a request as follows:

  • URL: http://localhost:8080/rooms
  • Method: GET

Your request should look similar to the following:

RESTed GET rooms

Send the request in RESTed and you should see a response body with the rooms already in the database.

Now try to create a room that already exists. Set up a request as follows:

  • URL: http://localhost:8080/rooms
  • Method: POST
  • Add a single parameter called name. Use a name that already exists like SwiftUI.
  • Select JSON-encoded as the request type. This ensures that the data is sent as JSON and that the Content-Type header is set to application/json.

After sending your request, you should get an error like so:

RESTed POST room

The error shows that your migration to add the unique key to the room name property was successful.

Migrations with Default Values

Engineers at your startup are collaborating more and more, but they currently don’t know how people can fit in a room. Time to add that through a migration!

First, update your Room model by adding a size property:

var size: Int

The compiler will complain that size is not initialized. One option is to make this size property optional, but you would have to unwrap the optional every time you wanted to use it. Instead, you can modify the default initializer to take in the size property. Replace the initializer with the following code:

init(name: String, size: Int) {
  self.name = name
  self.size = size
}

This is OK for creating new rooms from this point on, but you previously created rooms with the old Room(name:) initializer when you seeded your database. You don’t want to change the previous migration. Migrating data is sensitive and can be error-prone. A better solution is to create a convenience initializer. Add the following below your previous initializer:

convenience init(name: String) {
  self.init(name: name, size: 0)
}

Now if you build, your code will compile, and you didn’t modify a prior migration.

It’s time to add the struct for your migration. Create a new file for the AddSizeToRoom migration in the Migrations directory called 0004_AddSizeToRoom.swift. Update the code in the file to the new migration:

import FluentPostgreSQL
import Vapor

struct AddSizeToRoom: Migration {
  typealias Database = PostgreSQLDatabase

  static func prepare(on connection: PostgreSQLConnection) -> Future<Void> {
    return Database.update(Room.self, on: connection) { builder in
      let defaultValueConstraint = PostgreSQLColumnConstraint.default(.literal(2))
      builder.field(for: \.size, type: .int, defaultValueConstraint)
    }
  }

  static func revert(on connection: PostgreSQLConnection) -> Future<Void> {
    return Database.update(Room.self, on: connection) { builder in
      builder.deleteField(for: \.size)
    }
  }
}

This migration creates a default value for your size column with the value 2. You could have chosen 0 here like the default value of your struct, but you know that the rooms you have can fit at least two people. If you query your data in the future and you see a Room that has a size of 0, you know someone forgot to set the size of the room before creating it.

Add the migration to configure.swift above services.register(migrations):

migrations.add(migration: AddSizeToRoom.self, database: .psql)

Build and run. In Postico, you should now see all five migrations in your Fluent table:

Migrations in Fluent table

Your Room table should now have a new column called size with a value of 2 in each row:

Final Room table

That finishes all the migrations for your conference room app!

As cleanup, you can now stop the Postgres Docker container you started earlier using the following commands in Terminal:

docker stop postgres
docker rm postgres

If you wish, you can also delete the Postgres Docker image that was downloaded:

docker image rm postgres

Where to Go From Here?

You can download the completed project for this tutorial using the Download Materials button at the top or bottom of this page.

This tutorial provides an overview of different migrations you may come across as you build your Vapor application and your data model changes. If you enjoyed this tutorial, why not check out our full-length book on Vapor development: Server-Side Swift with Vapor?

If you’re a beginner to web development, but have worked with Swift for some time, you’ll find it’s easy to create robust, fully-featured web apps and web APIs with Vapor 3.

Whether you’re looking to create a backend for your iOS app, or want to create fully-featured web apps, Vapor is the perfect platform for you.

You can find more Vapor tutorials as well as tutorials using the Kitura framework on our Server-Side Swift page.

Questions or comments on this tutorial? Leave them in the comments below!

Average Rating

5/5

Add a rating for this content

1 rating

Contributors

Comments