Vapor 3 Series IV — Relationship
In our previous article, we enabled testing and wrote unit tests for each endpoint. Besides, we managed to run these tests on Linux environment with Docker as well. Testing allows us to develop and evolve our application quickly, because the test suite lets us verify everything still works as we change our codebase. In this article, we are going to explore two different kind of relationships among our models — — parent-child and sibling relationships. A parent-child relationship describes an ownership of one or more models, and it is known as one-to-one and one-to-many relationships. On the other hand, a sibling relationship describes links between two models, and it is known as many-to-many relationship.
Please notice that this article will base on the previous implementation.
Preparation
Before diving into relationships, we have to create two new model types as well as their controllers. As usual, use Terminal to create the necessary files and regenerate our Xcode project file with the following lines.
After generating our Xcode project file, open Pet.swift
and add the following code into the file.
These lines are just some boilerplate code to create our Pet
model, and it has a name
property of type String
and an age
property of type Int
. If you are still not familiar with these lines, please refer our very first article --- CRUD with Controllers.
Next, open PetsController.swift
and write the following lines.
These lines create the endpoints of our Pet
model, and these endpoints are protected by Bearer authentication, which we have already discussed in the second article of this series. In addition, the implementation of each endpoint is left as an exercise.
Then, open Category.swift
and add the following lines.
Again, we just create our Category
model here, and it has one name
property of type String
.
Next, open CategoriesController.swift
and write the following lines.
Similarly, these lines create the endpoints of our Category
model with Bearer authentication, and the implementation of each endpoint is an exercise as well.
After finishing our models and controllers, open configure.swift
and append the following two lines under migrations.add(model: Token.self, database: .sqlite)
.
These two line add our Pet
and Category
models to the list of migrations, so our application executes the migration at the next launch.
Last but not least, open router.swift
and append the following lines into routes(_ router:)
function.
At this point, we finish creating our models and controllers, so we are able to try these new endpoints with Postman.
Parent-Child Relationship
As we mentioned before, a parent-child relationship is an ownership between two models, and we are going to build this ownership relationship between our User
and Pet
models. More specifically, one user can have one or more pets, but a pet can only have one owner (user). First of all, open Pet.swift
and add a new property after var age: Int
.
This line adds a property of type User.ID
, and this property is not optional so a pet must have a user. Furthermore, please replace the initializer with the following lines to reflect the new property.
Next, open PetsController.swift
and rewrite updateHandler
method with the following code for the new property.
Now, our User
and Pet
models are linked with parent-child relationship, but it will be even more useful if we are able to query the relationships from each side. Open Pet.swift
and add an extension at the bottom of the file to get pet's user.
This computed property returns Fluent
's generic Parent
type, and it uses parent(_:)
function to retrieve the parent which is pet's user. Then swift to PetsController.swift
and add a new route handler under deleteHandler
.
This handler uses the computed property just created by us to get pet’s user and returns Future<User.Public>
. Moreover, we have to register the new route handler at the end of boot(router:)
method.
As a result, it connects an HTTP GET request to api/pets/:pet_id/user
to the new route handler. On the other hand, open User.swift
and append a new computed property after toPublic()
method.
This computed property returns Fluent
's generic Children
type, and it uses children(_:)
function to retrieve user's pets. Again, switch to UsersController.swift
and write a new route handler after loginHandler
method.
This handler uses the new computed property to get user’s pets and returns Future<[Pet]>
. Similarly, we need to register the new route handler at the end of boot(router:)
method.
Therefore, it connects a HTTP GET request to api/users/:user_id/pets
to the new route handler, and we can test these two new endpoints with Postman.
Although we just finish establishing the parent-child relationship between our User
and Pet
models with Fluent
, there is still no link between User
table and Pet
table in the database. We should set up foreign key constraints to ensure that a pet cannot be created with a non-existing user and a user cannot be deleted until all his/her pets have been deleted. Since foreign key constraints can be set up within the migration, open Pet.swift
and replace extension Pet: Migration {}
with the following lines.
Here, we add all fields to the database with addProperties(to:)
function, and add a reference between userID
property on our Pet
model and id
property on our User
model, which sets up the foreign key constraint between two tables. Since we are linking userID
property of our Pet
model to User
table, we must create User
table first. Go back to configure.swift
and double check that User
migration is above Pet
migration as the following.
This will make sure that Fluent
creates the tables in the correct order.
Sibling Relationship
Unlike a parent-child relationship, a sibling relationship doesn’t have limits between two models, for example, if there is a sibling relationship between our Pet
and Category
models, a pet can belong one or more categories and a category can contain one or more pets. However, we cannot just add a reference to our Category
directly in our Pet
model, because it's too inefficient to query. Instead, we need a separate model to hold on to the sibling relationship and it's called a pivot. Let's create our new pivot model with Terminal and regenerate our Xcode project file.
Then, open PetCategoryPivot.swift
and add the following code.
Here we take the advantage of ModifiablePivot
protocol to obtain some syntactic sugar for adding and removing the relationships. Besides, we add two properties to link the identifiers of our Pet
and Category
models respectively, and also define Left
and Right
types to tell Fluent
what the two types in the relationship are. After creating our new pivot model, open configure.swift
and append our new pivot model to the migration list after migrations.add(model: Category.self, database: .sqlite)
.
In order to actually create a relationship between our Pet
and Category
models, we have to use the new pivot. Let's switch to Pet.swift
and append a new computed property under var user: Parent<Pet, User>
.
This property returns the sibling of a Pet
that are of type Category
, and it uses Fluent
's siblings()
function to retrieve all the categories. After appending the computed property, open PetsController.swift
and add the following new route handler after getUserHandler
method.
This route handler uses attach(_:on:)
function to create the sibling relationship between our Pet
and Category
models. However, we have to register this route handler at the end of boot(router:)
method before trying it.
This line connects a HTTP POST request to api/pets/:pet_id/categories/:category_id
to the new route handler.
At this point, our Pet
and Category
models are linked with a sibling relationship, but it will be more useful if we can view these relationships. Within PetsController.swift
, add another new route handler under addCategoriesHandler
.
This route handler uses query(on:)
function to get the categories, and then we need to register this route handler at the bottom of boot(router:)
method as well.
This line connects a HTTP GET request to api/pets/:pet_id/categories
to the new route handler. On the other hand, open Category.swift
and add an extension at the end of the file.
This extension contains one computed property, and this property returns the sibling of a Category
that are of type Pet
. Furthermore, it also uses Fluent
's siblings()
function to retrieve all the pets. After creating the extension, switch to CategoriesController.swift
and append a new route handler at the end of the file.
This route handler uses query(on:)
function to get the pets, and then we also have to register this route handler at the bottom of boot(router:)
method.
This line connects a HTTP GET request to api/categories/:category_id/pets
to the new route handler.
Last but not least, we should take care of removing the sibling relationship, and fortunately removing a sibling relationship is very similar to adding the relationship. Switch back to PetsController.swift
and add one more route handler after getCategoriesHandler
.
This route handler uses detach(_:on:)
to remove the relationship, and returns a 204 No Content
HTTP status. Again, we have to register the route handler at the end of boot(router:)
method.
This line connects a HTTP DELETE request to api/pets/:pet_id/categories/:category_id
to the new route handler, and we can test these four new endpoints with Postman.
As creating parent-child relationships previously, it’s also a good practice to use foreign key constraints with sibling relationships. Currently, PetCategoryPivot
does not check the identifiers for the pets and categories, so we are able to delete pets and categories that are still linked by the pivot. Let's fix this by opening PetCategoryPivot.swift
and replacing extension PetCategoryPivot: Migration {}
with the following lines.
Inside this extension, we add all fields to the database with addProperties(to:)
function, and add a reference between petID
property on PetCategoryPivot
and id
property on Pet
, just the same with what we did for parent-child relationships. The only new thing here is .cascade
, which means the relationship will be automatically removed when we delete the pet from the database. Besides, we do the same thing to categoryID
property on PetCategoryPivot
and id
property on Category
.
Conclusion
Here is the entire project.
Let’s recap our journey of relationships. First of all, we create two new Pet
and Category
models and their controllers. Then, we link our User
and Pet
with parent-child relationships by adding a reference into our Pet
model directly, and use foreign key constraints to link User
table and Pet
table in the database. After implementing parent-child relationships, we create a new PetCategoryPivot
model and use it to link our Pet
and Category
models with sibling relationships. Similarly, we also take the advantage of foreign key constraints to link Pet
table and Category
table in the database. However, there are still something missing here. Please recall our previous article and it's always a good practice to write unit tests for the new relationships. The unit tests are left as exercises since they are not highly related to this topic, but I have included them with the project as well.