Fluent Part 2 - Parent-Child Relationships

Written by Tim on Thursday, September 28, 2017

This is the third tutorial of a series of tutorials on Vapor. For more, click here

Fluent

In the last tutorial we started to put together some parts of our Reminders application and we can now create and view reminders. In this tutorial we are going to add some more complexity to our application by introducing relationships between different models. By the end of this tutorial you will set up a parent-child relationship between a new User model and our existing Reminder model.

If you have been following along with the tutorial from the start, or are using an old template, you will need to upgrade your project for Swift 4. I have already written a short post on the basic steps, but you can also see this commit for the changes required to your Package.swift. Thankfully, at this stage, as we don't have much code, that is all that needs to be done!

Database Relationships

Fluent has the concept of three different types of relationships that can be set up between models:

  • Parent-Children (one-to-many)
  • Parent-Child (one-one)
  • Siblings (many-to-many)

Parent-Children and Parent-Child relationships are basically the same thing and they both describe a relationship between a parent model 'owning' a child model. So for example, in our Reminders application, the User-Reminder relationship will be a Parent-Children relationship - each reminder will have a user (the parent) who created that reminder and each user can create multiple reminders (the children). In the database, our user will have an ID and in the reminders table, each reminder will have a user's ID associated with it (a foreign key for those who know their databases).

Sibling relationships are slightly more complicated. There can be multiple relationships between different siblings; in a future tutorial on our reminders application, each reminder will have multiple categories and each category will contain multiple reminders. Because of the multiple nature of these relationships, we have to model these relationships in a separate table, called a Pivot table in Fluent. Each reminder will have an ID, each category will have an ID and then there will be a separate table with entries that link the two different models.

Thankfully, for all these types of relationships, you don't really have to worry too much about what goes on in the database - you just let Fluent take care of it all for you.

The User

The User Model

So let's go ahead and create our user! We need a new file again, so lets go ahead and create one and regenerate our Xcode project:

touch Sources/App/Models/User.swift
vapor xcode -y

Our user model will be fairly simple to start with, all we need is an ID, which Fluent will handle for us, and a name. We also want to be able to convert our user to and from JSON and return it as a response, like the Reminder, so we need to conform to the JSONConvertible and ResponseRepresentable protocols. So our User.swift file should look something like:

import FluentProvider

final class User: Model {
    let storage = Storage()

    let name: String

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

    init(row: Row) throws {
        name = try row.get("name")
    }

    func makeRow() throws -> Row {
        var row = Row()
        try row.set("name", name)
        return row
    }
}

extension User: Preparation {
    static func prepare(_ database: Database) throws {
        try database.create(self) { builder in
            builder.id()
            builder.string("name")
        }
    }

    static func revert(_ database: Database) throws {
        try database.delete(self)
    }
}

extension User: JSONConvertible {
    convenience init(json: JSON) throws {
        try self.init(name: json.get("name"))
    }

    func makeJSON() throws -> JSON {
        var json = JSON()
        try json.set("id", id)
        try json.set("name", name)
        return json
    }
}

extension User: ResponseRepresentable {}

Finally, don't forget to add the User to our list of preparations in Config+Setup.swift:

private func setupPreparations() throws {
    preparations.append(User.self)
    preparations.append(Reminder.self)
}

Note: the User preparation must come before the Reminder preparation so that when we set up the relationship later and try and set the User as the parent of a Reminder it knows what a User is.

The User Controller

Much like our Reminder Controller, we also want a controller to be able to create and view users, so it will look pretty similar to our Reminder Controller. So create the file and regenerate our Xcode project again:

touch Sources/App/Controllers/UsersController.swift
vapor xcode -y

Then in our UsersController.swift, set up the routes to be able to create a user, view a single user and view all the users at /api/users/:

import Vapor
import FluentProvider

struct UsersController {
    func addRoutes(to drop: Droplet) {
        let userGroup = drop.grouped("api", "users")
        userGroup.get(handler: allUsers)
        userGroup.post("create", handler: createUser)
        userGroup.get(User.parameter, handler: getUser)
    }

    func createUser(_ req: Request) throws -> ResponseRepresentable {
        guard let json = req.json else {
            throw Abort.badRequest
        }
        let user = try User(json: json)
        try user.save()
        return user
    }

    func allUsers(_ req: Request) throws -> ResponseRepresentable {
        let users = try User.all()
        return try users.makeJSON()
    }

    func getUser(_ req: Request) throws -> ResponseRepresentable {
        let user = try req.parameters.next(User.self)
        return user
    }
}

Finally register the controller in Routes.swift:

func setupRoutes() throws {
    let remindersController = RemindersController()
    remindersController.addRoutes(to: self)
    let usersControler = UsersController()
    usersControler.addRoutes(to: self)
}

You should now be able to create both users and reminders now! But we want to be able to link them up and see which user created which reminder.

Setting Up A Parent-Child Relationship

Because the User is a parent of our Reminder, we don't actually need to touch the User model, all the changes will happen inside the Reminder model. The first thing we need to do is add a new field to our preparation, which will set up our foreign key constraint and link the two together. With Fluent this is easy, and there is a handy parent function that we can use:

extension Reminder: Preparation {
    static func prepare(_ database: Database) throws {
        try database.create(self) { builder in
            builder.id()
            builder.string("title")
            builder.string("description")
            builder.parent(User.self)
        }
    }
}

Next we actually need to add a property to our model that will store the ID of the parent user. This is an Identifier, which is the id of the User. So we need to add that field to all of the functions, including all of the initialisers, and the representable functions. Once this is all done, your new Reminder class should look something like this:

import FluentProvider

final class Reminder: Model {
    let storage = Storage()

    let title: String
    let description: String
    let userId: Identifier?

    init(title: String, description: String, user: User) {
        self.title = title
        self.description = description
        self.userId = user.id
    }

    init(row: Row) throws {
        title = try row.get("title")
        description = try row.get("description")
        userId = try row.get(User.foreignIdKey)
    }

    func makeRow() throws -> Row {
        var row = Row()
        try row.set("title", title)
        try row.set("description", description)
        try row.set(User.foreignIdKey, userId)
        return row
    }
}

extension Reminder: Preparation {
    static func prepare(_ database: Database) throws {
        try database.create(self) { builder in
            builder.id()
            builder.string("title")
            builder.string("description")
            builder.parent(User.self)
        }
    }

    static func revert(_ database: Database) throws {
        try database.delete(self)
    }
}

extension Reminder: JSONConvertible {
    convenience init(json: JSON) throws {
        let userId: Identifier = try json.get("user_id")
        guard let user = try User.find(userId) else {
            throw Abort.badRequest
        }
        try self.init(title: json.get("title"), description: json.get("description"), user: user)
    }

    func makeJSON() throws -> JSON {
        var json = JSON()
        try json.set("id", id)
        try json.set("title", title)
        try json.set("description", description)
        try json.set("user_id", userId)
        return json
    }
}

extension Reminder: ResponseRepresentable {}

If you run your application, you should be able to create a user using the API:

Create a user

You can then use the returned user's ID to create a reminder with the additional user_id field in the request:

Create a reminder with a user

The nice thing about this is that the user must exist when you try and create a reminder from JSON otherwise you get a 400 Bad Request.

Accessing Relationships

We can now create users and create reminders for those users, but getting more information out that just the user's ID from a reminder would be useful. We may want to set up a new route for the reminder to be able to get its User. We also might want to get the parent of any Reminder elsewhere in the app and we can do better than a manual query on the database with the User ID!

Getting The Parent

Thankfully Fluent provides some useful properties we can implement to make this easy! We will implement a Parent property in our extension:

extension Reminder {
    var user: Parent<Reminder, User> {
        return parent(id: userId)
    }
}

Now whenever we have a Reminder we can simply call try reminder.user.get() and this will return us the User for that reminder. (Note: that it will be optional in case the relationship hasn't been set up properly, though this should never happen in normal circumstances, but you should be aware of it as you will need to unwrap the result.) We can now create a new route for the Reminder which will return its user, for example /api/reminders/1/user/ in our RemindersController:

func getReminderUser(_ req: Request) throws -> ResponseRepresentable {
    let reminder = try req.parameters.next(Reminder.self)
    guard let user = try reminder.user.get() else {
        throw Abort.notFound
    }
    return user
}

We can then register our route inside the addRoutes function in the controller:

reminderGroup.get(Reminder.parameter, "user", handler: getReminderUser)

Now once we have created a user and a reminder, we can visit the new endpoint to get information about the reminder's user:

Get Reminders User

Getting The Children

As well are seeing the reminder's user, we may also want to see all of the reminders a user has created. This is useful for creating a user's profile page and listing all their created reminders. We can do this with another simple extension:

extension User {
    var reminders: Children<User, Reminder> {
        return children()
    }
}

This extension simply calls Fluent's inbuilt children function. We can leverage this with a new route handler for the user, so that we can call /api/users/1/reminders/:

func getUsersReminders(_ req: Request) throws -> ResponseRepresentable {
    let user = try req.parameters.next(User.self)
    return try user.reminders.all().makeJSON()
}

This route handler uses our reminders property we created in the extension to return a JSON array. We can now register the route in our UsersController.addRoutes() function:

userGroup.get(User.parameter, "reminders", handler: getUsersReminders)

Once this has been done and you have built your project, created a user and then created a number of reminders, you should be able to visit http://localhost:8080/api/users/1/reminders/ and see them all:

Get a user's reminders

Conclusion

We can now start creating parent-child relationships between different models using Fluent and next time we will look at creating Sibling relationships between models. With both of these we can create complex apps and utilise the power of Fluent to write clean, simple code.

All of the code for these tutorials can be found on Github, with this tutorial under the tutorial-3/fluent-2-parent-child tags.