Sibling Relationships in Vapor 2 (many-to-many Relationships)

286 Views

This tutorial will cover how to add a many-to-many relationship (aka Sibling relationship) between 2 of your models in Vapor 2.

To start, download the starter project from my Github

The starter project is very basic. It has a Post model, and a Tag model. We want to set up a situation where a Post object can have many tags associated to it, and each Tag object can have many posts associated to it as well--a classic many-to-many relationship.

First, in Config+Setup.swift, add the following line to your setupPreparations() method:

    preparations.append(Pivot<Post, Tag>)

Now, when the scheme is run, this will create a third table in your database, post_tag. This join table has 2 columns: post_id and tag_id. The single line of code above creates all of this for us.

Next, in Post.swift, but outside the main class definition, add the following extension and computed property:

    extension Post {
      var tags: Siblings<Post, Tag, Pivot<Post, Tag>> {
        return siblings()
      }
    }

The property tags above is what will enable us to be able to call post.tags.all() to retrieve all of a post's associated tags. The result is an array of tags, [Tag]

Likewise, in Tag.swift, but outside the main class definition, add the corresponding extension and property:

    extension Tag {
      var posts: Siblings<Tag, Post, Pivot<Tag, Post>> {
         return siblings()
      }
    }

This posts property is what will allow us to call tag.posts.all() when we want to fetch all posts associated with a specific tag object.

Actually, above is the bare minimum we need in order to get sibling relationships working in our app. At this point, the database table and columns are set, and all API methods for adding/removing siblings are available to us. So, given a specific post, how can we add a tag to it?

In PostController.swift, go to the function createPost, and change it to read the following:

    func createPost(request: Request) throws -> ResponseRepresentable {
        if let title = request.data[Post.titleKey]?.string {
            let post = Post(title: title)
            try post.save()        // 1
            if let tag = try Tag.all().first { // 2
                try post.tags.add(tag)      // 3
            }
            return post
        }
        return try JSON(node: ["error": "error saving post!"])
    }

First (1), we take the title parameter from the data, and we create a post and save it. You must save the post before attaching the tag to it.

2nd (2), we retrieve the first tag from the database. In practice, you can use any tag (not just the first), or you can even pass in the id of the tag via the params, and the say let tag = try Tag.find(id). I simply use the first tag here for simplicity.

Third (3), this is the line where we create the relationship. Internally, this is where fluent adds a new row to our post_tag table, with a post_id value of the given post, and a tag_id value of the tag we passed in.

To remove a tag, instead of adding one, we can call try post.tags.remove(tag).

That's all we need to get many-to-many relationships working in Vapor!

If you would like to check and see if you have it setup correctly, I wrote a method that you can add as a route to your PostController file:

    func viewFirstPostsTags(request: Request) throws -> ResponseRepresentable {
        let allPosts = try Post.all() // retrives all posts
        
        // loop through all posts, and print their tag names
        for post in allPosts {
            let tags = try post.tags.all()
            for postsTag in tags {
                print("post with title: \(post.title), has tag: \(postsTag.name)")
            }
        }
        
        let allTags = try Tag.all()
        // lopo through all tags, and print their post's titles
        for tag in allTags {
            let posts = try tag.posts.all()
            for post in posts {
                print("tag with name: \(tag.name), has post: \(post.title)")
            }
        }
        return try JSON(node: ["result": "result"])
    }

Add self.droplet.get("viewsiblings", handler: viewFirstPostsTags) to the addRoutes() function, and then when you go to http://localhost:8080/viewsiblings, it will loop through all posts and print out the tags belonging to them, and loop through all tags and print out the posts belonging to them.

Thanks for reading!



Enjoy this article? Consider supporting VaporForums by following us on Twitter! Get the latest Vapor articles, tutorials, and news.