Diving into Vapor, Part 4: Deeper into Fluent
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 leftIDKey
properties. 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 username
property unique.
First, add a var id: UUID?
property back to the User
model and make the username
property 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!