Fluent Part 3 - Sibling Relationships

Written by Tim on Thursday, October 5, 2017. Last edited on Friday, October 6, 2017.

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

Fluent

In the last tutorial we looked at adding a parent-child relationship between our reminders and users. In this tutorial we are going to look at sibling relationships and allow reminders to be categorised. By the end of this tutorial you will set up a sibling relationships between a new Category model and our existing Reminder model.

Sibling Relationships

First, let's recap on what sibling relationships are and how they work in Vapor. A sibling relationship is a many-to-many relationship; in our app this means that a reminder can have many categories and a category can contain many reminders. This means that in our models, you can't really link IDs to each other since there will probably be many links.

In Fluent, these relationships are tracked in Pivots. This will create a new table to contain all of the relationships and manage them for you. It is simple to set up and Fluent provides all of the conveniences you need to be able to manage them.

The Category

So like the previous tutorials we need to set up the category, which includes the model and the controller to be able to create our categories.

The Category Model

Like before, we need to do the usual SPM dance and create our new file:

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

Now that we have our file, we can write our model code. This will be almost identical to our user, with simply a name property and the usual protocol conformances:

import FluentProvider

final class Category: Model {

    static let entity = "categories"

    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 Category: 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 Category: 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 Category: ResponseRepresentable {}

One thing you will notice about this model is that we are defining the entity property on our model, which is what the table is called. By default, Fluent will lowercase your model name and add an s on the end. Obviously for something like 'Category' this isn't right! Whilst it probably won't cause any problems, it can cause confusion and may affect some people's OCD! So by setting this property we can ensure that the table is named correctly.

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

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

The Category Controller

The Category Controller will be an almost identical clone to the User Controller for now, so create the file:

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

Then our CategoriesController, which will be able to create a reminder and view them at /api/categories/:

import Vapor
import FluentProvider

struct CategoriesController {
    func addRoutes(to drop: Droplet) {
        let categoryGroup = drop.grouped("api", "categories")
        categoryGroup.get(handler: allCategories)
        categoryGroup.post("create", handler: createCategory)
        categoryGroup.get(Category.parameter, handler: getCategory)
    }

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

    func allCategories(_ req: Request) throws -> ResponseRepresentable {
        let categories = try Category.all()
        return try categories.makeJSON()
    }

    func getCategory(_ req: Request) throws -> ResponseRepresentable {
        let category = try req.parameters.next(Category.self)
        return category
    }
}

Finally register the categories route in Routes.swift:

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

You should now be able to create categories now! But now we want to be able to link them to our reminders.

Setting Up A Sibling Relationship

Because all the information for a many-to-many relationship is stored in a separate table, there is actually nothing to change in the models. In Fluent, this separate table is called a Pivot, so the first thing we need to do is add the pivot to our preparations:

private func setupPreparations() throws {
    preparations.append(User.self)
    preparations.append(Reminder.self)
    preparations.append(Category.self)
    preparations.append(Pivot<Reminder, Category>.self)
}

When this preparation is run, Fluent will create the pivot table which will hold the relationships between our Reminder model and Category model.

Now that we have done this, on our Reminder model we can add an extension to get all of the reminder's categories:

extension Reminder {
    var categories: Siblings<Reminder, Category, Pivot<Reminder, Category>> {
        return siblings()
    }
}

Whilst the generics may make things look complicated at first, it is actually really simply. With just this code, if we want to get an array of all the reminder's categories, all we simply call is reminder.categories.all(). We can also add a similar extension to our Category model:

extension Category {
    var reminders: Siblings<Category, Reminder, Pivot<Category, Reminder>> {
        return siblings()
    }
}

So now we can get the siblings of our different models, but how do we actually add one? Well once we have our models, we simply call add - it really is that simple! If we have a category object and want to add it to our reminder all we have to do is try reminder.categories.add(category). We can add it on to either model, but for readability and simplicity in your code, it is better to only do it in one place. So in our reminders controller, let's edit the createReminder function:

func createReminder(_ req: Request) throws -> ResponseRepresentable {
    guard let json = req.json else {
        throw Abort.badRequest
    }
    let reminder = try Reminder(json: json)
    try reminder.save()

    if let categories = json["categories"]?.array {
        for categoryJSON in categories {
            if let category = try Category.find(categoryJSON["id"]) {
                try reminder.categories.add(category)
            }
        }
    }

    return reminder
}

As before we will take the request's JSON and create a reminder from it. But this time, once we have created our reminder, we then see if the JSON contains any categories. If so, we will loop through the array, get the category from the database if it exists and then set up the relationships.

Viewing The Relationships

As in the previous tutorial, where we set up the route /api/reminders/<ID>/user/ to get the user of a reminder, we will set up a couple of routes to view the relationships. First, let's create the route to view the categories for a reminder. In our RemindersController we will register a new GET route at /api/reminders/<ID>/categories/ and create a handler for it:

func addRoutes(to drop: Droplet) {
    ...
    reminderGroup.get(Reminder.parameter, "categories", handler: getRemindersCategories)
}

func getRemindersCategories(_ req: Request) throws -> ResponseRepresentable {
    let reminder = try req.parameters.next(Reminder.self)
    return try reminder.categories.all().makeJSON()
}

You can see the extension we added earlier makes this really easy. We will also add a new route for getting all the reminders for a category at /api/categories/<ID>/reminders/ in our CategoriesController:

func addRoutes(to drop: Droplet) {
    ...
    categoryGroup.get(Category.parameter, "reminders", handler: getCategorysReminders)
}

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

To see all of this in action, let's create a user as before in the previous tutorial. Next create a couple of categories, by sending a request to http://localhost:8080/api/categories/create that looks something like:

Create a category

Now we can create our reminder by sending some JSON that looks something like:

{
  "description": "Migrate application to Swift 4",
  "title": "Swift 4",
  "user_id": 1,
  "categories": [
  {
    "id": 1
  },
  {
    "id": 2
  }
  ]
}

Note: if you are using Rested, you will need to click the 'Use Custom HTTP Body' checkbox and manually enter the JSON as it doesn't support JSON arrays in the parameters. You will also need to set the Content-Type header to be application/json as shown in the screenshot below.

If we sent this to our create reminder endpoint, we should get a 200 OK response:

Create a reminder with categories

Next if we visit /api/reminders/1/categories/ we should see our two categories:

View reminders categories

Finally if we visit /api/categories/1/reminders/ we should see our recently created reminder:

View category's reminder

Conclusion

By now you should have a good understanding of how controllers work in Vapor and how to set up both parent-child relationships and sibling relationships in Fluent. The next tutorial will wrap up our Fluent work and look at improving how we use it and how to integrate persistent storage.

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