Diving into Vapor, Part 4: Deeper into Fluent

Caleb Kleveter
The Swift Web Developer
6 min readSep 6, 2018

--

In the last tutorial, we looked at the basics of Fluent queries (and routing, but that doesn’t matter at this time). This time, we will dig a little deeper with queries and model relations.

Make sure you run vapor update -y or swift package update before starting.

Model Relations

We are going to start with a sibling, or many-to-many, relationship. The app we are building will be a simple social media app. One important part of any social media platform is being able to follow people. To represent a follower/following connection, we would have a model that has two User IDs, one for the follower and one for the followed. Fluent has a protocol to help us build this kind of structure called Pivot. There are database type specific versions of this protocol, so use PostgreSQLPivot or MySQLPivot based on which database you are using.

We are going to call the pivot model UserConnection. That model looks like this:

Everything but lines 12–17 are required by the PostgreSQLPivot protocol. We add the rightID and leftID properties so we can satisfy the rightIDKey and leftIDKeyproperties. The initializer takes in two User models and extracts the IDs to assign them to the rightID and leftID properties. If either ID is missing, an error is thrown.

Conform the UserConnection pivot to the Migration protocol:

Then add it to the migration config in the global configure(_:_:_:) function:

Next, we will add a couple of helper properties to the User model so we can easily get and add followers/following users.

First is the following property. This property will get all the users that a single user follows. I am going to put this property in a User extension below the UserConnection model.

The property will look something like this:

Usually when you use the .siblings method, you can just call self.siblings() and all the generics stuff is figured out for you. Because we are using the same type for both the left and right properties of the pivot, we need to specify how they are related and which model is the base model.

The other computed property we want is for getting a user’s followers. This will be the same as the previous property, but we will switch the key-paths around:

We will also add two methods to the User model. One for following and another for un-following another user.

The first method, for following a user, looks like this:

We take in a user to follow and a DatabaseConnectable object to save the new pivot with. We wrap the body of the method in a Future.flatMap so the method doesn't need to throw. We create a UserConnection pivot with self as the base user and the user passed in as the foreign user. We save the pivot, then return the current user and followed user in a tuple.

To unfollow a user, we use the Siblings.detach method with the .following computed property. The signature of the method is almost the same:

You should now have the following extension in your UserConnection.swift file:

Query it Over Again

We will now put our pivot to use by adding some more routes to the UserController. The first two routes will be simple, getting the user's followers and the user's he/she is following. We have already discussed in previous tutorials what you need to make these routes, so give it a shot before looking at my code below!

We are going to add two more route to the UserController. One for following a user and another for un-following a user.

To follow a user, we are going to use a POST route (because we are creating a new pivot) with the path users/{user}/follow.

The handler just takes in a request. We get the user that will follow from the request’s parameters and the user to follow from the request’s body, with the follow key. After we find the user to follow, we create a new UserConnection pivot with the User.follow(user:on:) method. We then convert the tuple returned by that method to a dictionary and return it.

To un-follow a user, we are going to make a DELETE route with the path users/{user}/un-follow. The handler will be very similar to the one for following another user:

As with the previous handler, we get the current user and the user being followed, but then we call User.unfollow(user:on:). We then return the HTTP status 204 (No Content).

We will add one more route to the UserController to search for users by their name. This will get a GET route at users/search. The name to search by will be passed in using a query string. The handler looks like this:

First we get the name value from the request's query strings. Then we have to find the User models that match it. We do this using the =~ operator. This operator requires that the beginning of the string matches the value passed in, so hello would match hello world. You need to import the FluentSQL module to use this operator, because it is SQL specific.

We put the filters in an or group because if a user has a username of Jonny but their first name is Steve, you would never be able to find that user in the search.

We aren’t using raw queries for this project (or, at least not yet), but if you are wondering how they work, here is an example:

Migrating the ID

We changed the ID of the model to the username in the last tutorial, but that is actually a bad idea. An ID should never change for a model, but some users will want to change their username from time to time. We are going to modify the User model to have a UUID as its ID, but keep the usernameproperty unique.

First, add a var id: UUID? property back to the User model and make the usernameproperty non-optional. Then replace the User: Model extension to be either PostgreSQLUUIDModel or MySQLUUIDModel, depending on the database you are using:

This will break our UserConnection implementation. You will need to change the ID property types to UUID also:

Now we are going to create a custom migration for the User model. A migration is basically instructions that Fluent uses to generate the model's table in the database. There is a default implementation for this, which is why we didn't need to do this ourselves before.

The Database.create method creates a table in the database for the model passed in (self), using the SchemaCreator passed in as a blue print for the table. The addProperties(to:)method uses the init(from:) decoder initializer to get a reflection of the model and create columns for the model's properties. That is what the migration does by default.

We added two more instructions to the migration to mark both the username and email columns as UNIQUE. This means the no two users can share the same username or email.

Now that we have changed the properties of the User model, we will need to regenerate the table in the database. Like last time when we made a change, run vapor build && vapor run revert --all in the command-line.

Good Job! You have come to the end of this tutorial. You should now have the knowledge to create pivots between models, more complex queries with the Fluent ORM, and how migrations work under the hood.

The source-code for this project can be found here.

Remember that the docs are your best friend and there is always someone willing to help on the Discord server! Have fun!

--

--

Caleb Kleveter
The Swift Web Developer

iOS and Vapor Fanatic. Has lots of fun open sourcing on GitHub.