Building Cheer Model

Building the Model for the Cheer app with Fluent, Vapor, and MySQL. With code samples for to-one, to-many, and many-to-many relationships.

Building the initial Cheer model

The entity model for the Cheer app is relatively simple: Pages have an Author, and each Page may have multiple Tags.

Okay, so that was just an excuse to play with the new Apple Pencil.

Page

The initial implementation of Page.swift looks like this:

import Vapor
import FluentMySQL

final class Page: Codable {
    var id: Int?
    var created: Date
    var slug: String
    var title: String
    var introduction: String
    var body: String
    var authorID: Int
    
    init(id: Int? = nil, created: Date, slug: String, title: String, introduction: String, body: String, authorID: Int) {
        self.id = id
        self.created = created
        self.slug = slug
        self.title = title
        self.introduction = introduction
        self.body = body
        self.authorID = authorID
    }
}


extension Page: MySQLModel { }
extension Page: Migration { }
extension Page: Content { }

Page has a mandatory to-one relationship to Author, hence the authorID. There's no evidence, yet, of the to-many relationship to Tag.

Aside from the annoyance of creating the init function (couldn't this be synthesized? or created behind the scenes as with the CodeGen functionality for .xcmodeld files?) the creation of the model object is fairly straightforward.

We need to conform to three protocols: MySQLModel since we're persisting the data to a MySQL database; Migration in order to get the database tables initially established, and to assist in migrations as our schema changes; and Content so that we can return codable versions of the model data.

Author

The Author entity is really simple—to be honest, at this stage it is just an excuse to act as the destination for our to-one relationship. The class looks like this:

final class Author: Codable {
    var id: Int?
    var name: String
    
    init(id: Int? = nil, name: String) {
        self.id = id
        self.name = name
    }
}

Page to Author relationship

A Page has one, and only one, Author, but an Author can have multiple Pages. The relationship from Author to Page is therefore a one-to-many relationship or—in Fluent's terminology—a Parent-Children relationship.

In order to implement the to-one relationship beween Page and Author we use the authorID and the parent function:

extension Page {
    var toAuthor: Parent<Page, Author> {
        return parent(\.authorID)
    }
}

We've created an additional var called toAuthor that simplifies getting the destination object, essentially by requesting the page's parent object whose id is our authorId.

Then we have to model the Author one-to-many Page relationship, where we return the author's children.

extension Author {
    var toPages: Children<Author, Page> {
        return children(\.authorID)
    }
}

Although it is tempting to think—particularly from a WebObject's perspective—that toAuthor and toPages return the actual parent object, or the child objects, this isn't technically true: they're returning a relation struct which we will have to traverse to get the actual destination objects. For this reason we're using the to prefix to highlight that we're not returning the destination object itself but a relationship to it.

Tag

The Tag entity is, once again, fairly simple, and just an excuse to try out a many-to-many relationship. The class looks like this:

final class Tag: Codable {
    var id: Int?
    var name: String
    
    init(id: Int? = nil, name: String) {
        self.id = id
        self.name = name
    }
}

Page to Tag relationship

A Page can have multiple Tags—they're the little gray things in the sidebar of this site which will eventually do more than just look pretty. The relationship we want is many-to-many:

In order to implement the many-to-many relationship—which in Fluent-speak is a 'siblings relationship'—between Page and Tag we have to use a PivotTable.

Leaving aside the whining that EOF/WebObjects modeled many-to-many relationships entirely behind the scenes back in 1996, the implementation is relatively straight-forward although it does require some boilerplate code: the PivotTable entity, and a var on either side of the relationship for convenience. The hard part seems to be juggling the types on the left and right hand side of the relationship.

LHS type:

RHS type:

The PageTagPivot.swift class looks like this:

import Vapor
import FluentMySQL

final class PageTagPivot {
    
    typealias Left = Page
    typealias Right = Tag
    
    static var leftIDKey: LeftIDKey = \.pageId
    static var rightIDKey: RightIDKey = \.tagId
    
    var id: Int?
    var pageId: Int
    var tagId: Int
    
    init(_ page: Page, _ tag: Tag) throws {
        pageId = try page.requireID()
        tagId = try tag.requireID()
    }

}

extension PageTagPivot: MySQLModel { }
extension PageTagPivot: Migration { }
extension PageTagPivot: ModifiablePivot {}

Fluent refers to the objects on both sides of a many-to-many relationship as 'siblings', and helpfully provides a Siblings struct that connects the two sides (referred to as Base and Related) and the PivotTable (Through) which joins them both.

As we did earlier, we can then add a convenience var to Page:

extension Page {
    var toTags: Siblings<Page, Tag, PageTagPivot> {
        return siblings()
    }
}

and to Tag:

extension Tag {
    var toPages: Siblings<Tag, Page, PageTagPivot> {
        return siblings()
    }
}

Notice that these two look remarkably similar—the key difference is the order to the types. In each case we're thinking of which entity we're going from, the entity we're going to, and then pivot table through which we're going.

Summary

We've modeled three entities, added a to-one relationship, and a many-to-many relationship through the PivotTable, and created convenience vars to help traverse the relationships. When we get round to actually fetching entities we'll actually start using these relationships.

Resources

Vapor's website.

Discuss?

Haven't added a commenting system to this app, and probably won't. So if you have any feedback—positive or negative—either say it via email to david@cocoacheerleaders.com or on Twitter at @CocoaCheer.