Diving into Vapor, Part 5: Parent-Child Relationships
In the last tutorial, we created a sibling relationship for the User
model to represent follower/following relationships between them. Now we will look at a new kind of database relationship call the 'Parent-Child' relationship. This is when a single Parent
model owns any number of Child
models, but a Child
model can only be connected to a single Parent
model.
Creating a Child
In the app we are creating, the ‘parent’ model will be User
and the 'child' model will be a new Post
model. Start by creating a Post.swift
file in your Models/
directory and building the basic structure of the Post
model:
Now we need to think of what a post needs. There are many things you could add, such as images, polls, and other widgets, but that gets out of the scope of this tutorial, so we will have only 2 fields:
contents
: This is the text that people can read.userID
: The ID of theUser
that owns the post.
We could have an array of tags, but you get better performance and maintenance if you have a pivot between a Tag
model and Post
model, so we'll leave that out for now.
Your Post
model should then look like this:
You might have differences with your implementation. You might make userID
mutable or have the initializer take in a User
model and extract the ID from it. That's okay.
Don’t forgot to add your Post
model to the migrations config:
At this point the relationship is ready. All we are going to do now is add helper property to the User
model so we can easily get its child Post
models:
CRUD and Parent-Child Relationships
Now that the Post
model is setup, we can create API endpoints for it. We can start out with the controller looking like this:
Notice that the posts
router group is not /posts
, but /users/{user}/posts
. This is because a Post
model cannot be independent of a User
parent, so we want to show that in the API.
Read
We’ll start with a basic GET
route that fetches all the posts belonging to a User
. We do this by getting the User
model from the request's parameters and calling .posts
on it. This property lets you create a QueryBuilder
that automatically filters the children for parent's ID. We create the QueryBuilder
and call .all()
on it:
Create
Creating a Post
model will be a bit more complex on our side because of the way we are structuring the API. We already have the User
ID in the request's parameters, so we don't want to require it in the request's body, which would happen if we decode Post
. Instead we are going to create a new struct called PostBody
that only has a content
property. We can use this struct to create a new Post
instance once we get a User
ID. I added a helper method to the PostBody
struct for this:
Now that we can decode the request body, we can create the route handler. To create the Post
model, we need to get the User
model ID from the request parameters. We could just get the User
model, but that requires an extra database query we don't want to have to make. Instead, we will get the raw parameter value, and convert it to a UUID. This won't verify that the User
ID passed in is valid, but we can fix that later by adding a foreign-key constraint to the Post
model's migration.
After we get the UUID, we can convert the PostBody
to a Post
and save it:
Update
Unlike Twitter, we’ll let people edit their posts 😄. First, we decoding the request’s body to a PostBody
instance; then we will get User
passed in to the request's parameters. The User
's posts can then by filtered by their ID, using the ID which was also passed into the request's parameters. We can then update the Post
's in the query using the decoded PostBody.content
property. Since we are filtering by ID, we can guarantee there will be 0 or 1 result, so we call .first()
and unwrap it, throwing an Abort(.notFound)
if we get nil
:
Delete
To delete a post, we will get the Post
ID and User
from the request parameters and filter the User
's posts with the ID. We can then call .delete()
on the query and transform it to an HTTPStatus.noContent
:
Finish up by registering your PostController
router in your routes.swift
file:
Foreign-Key Constraints
I said we would add a foreign-key constraint to the Post
model so the userID
passed in is always valid. Create a custom migration for the Post
model like we did for the User
model, but instead of adding UNIQUE
constraints to our properties, we will add a reference between the Post.userID
property and the User.id
property, like this:
Now the database will always check to make sure the userID
value is valid.
Great job! You have successfully created a parent-child relationship in Fluent! As always, check out the docs and say ‘hi’ on the Discord server! You can find the source for this project here.
If you want, you can add an additional GET
route the PostController
controller to get a single Post
from a User
instead of all of them. Have Fun!