9.
Parent-Child Relationships
Written by Tim Condon
Chapter 5, “Fluent & Persisting Models”, introduced the concept of models. In this chapter, you’ll learn how to set up a parent-child relationship between two models. You’ll also learn the purpose of these relationships, how to model them in Vapor and how to use them with routes.
Note: This chapter requires that you have set up and configured PostgreSQL. Follow the steps in Chapter 6, “Configuring a Database”, to set up PostgreSQL in Docker and configure the Vapor application.
Parent-child relationships
Parent-child relationships describe a relationship where one model has “ownership” of one or more models. They are also known as one-to-one and one-to-many relationships.
For instance, if you model the relationship between people and pets, one person can have one or more pets. A pet can only ever have one owner. In the TIL application, users will create acronyms. Users (the parent) can have many acronyms, and an acronym (the child) can only be created by one user.
Creating a user
In Xcode, create a new file for the User
class called User.swift in Sources/App/Models. Next, create a migration file, CreateUser.swift, in Sources/App/Migrations. Finally, create a file called UsersController.swift in Sources/App/Controllers for the UsersController
.
User model
In Xcode, open User.swift and create a basic model for the user:
import Fluent
import Vapor
final class User: Model, Content {
static let schema = "users"
@ID
var id: UUID?
@Field(key: "name")
var name: String
@Field(key: "username")
var username: String
init() {}
init(id: UUID? = nil, name: String, username: String) {
self.name = name
self.username = username
}
}
The model contains two String
properties to hold the user’s name and username. It also contains an optional id
property that stores the ID of the model assigned by the database when it’s saved. You annotate each property with the relevant property wrapper.
Next, open CreateUser.swift and insert the following:
import Fluent
// 1
struct CreateUser: Migration {
// 2
func prepare(on database: Database) -> EventLoopFuture<Void> {
// 3
database.schema("users")
// 4
.id()
// 5
.field("name", .string, .required)
.field("username", .string, .required)
// 6
.create()
}
// 7
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("users").delete()
}
}
This is what your migration does:
- Create a new type for the migration to create the users table in the database.
- Implement
prepare(on:)
as required byMigration
. - Set up the schema for
User
with the name of the table asusers
. - Create the ID column using the default properties.
- Create the columns for the two other properties. These are both
String
and required. The name of the columns match the keys defined in the property wrapper for each property. - Create the table.
- Implement
revert(on:)
as required byMigration
. This deletes the table namedusers
.
Finally, open configure.swift to add CreateUser
to the migration list. Insert the following after app.migrations.add(CreateAcronym())
:
app.migrations.add(CreateUser())
This adds the new model to the migrations so Fluent prepares the table in the database at the next application start.
User controller
Open UsersController.swift and create a new controller that can create users:
import Vapor
// 1
struct UsersController: RouteCollection {
// 2
func boot(routes: RoutesBuilder) throws {
// 3
let usersRoute = routes.grouped("api", "users")
// 4
usersRoute.post(use: createHandler)
}
// 5
func createHandler(_ req: Request)
throws -> EventLoopFuture<User> {
// 6
let user = try req.content.decode(User.self)
// 7
return user.save(on: req.db).map { user }
}
}
This should look familiar by now; here’s what it does:
- Define a new type
UsersController
that conforms toRouteCollection
. - Implement
boot(routes:)
as required byRouteCollection
. - Create a new route group for the path /api/users.
- Register
createHandler(_:)
to handle a POST request to /api/users. - Define the route handler function.
- Decode the user from the request body.
- Save the decoded user.
save(on:)
returnsEventLoopFuture<Void>
so usemap(_:)
to wait for the save to complete and return the saved user.
Finally, open routes.swift and add the following to the end of routes(_:)
:
// 1
let usersController = UsersController()
// 2
try app.register(collection: usersController)
Here’s what this does:
- Create a
UsersController
instance. - Register the new controller instance with the router to hook up the routes.
Open UsersController.swift again and add the following to the end of UsersController
. These functions return a list of all users and a single user, respectively:
// 1
func getAllHandler(_ req: Request)
-> EventLoopFuture<[User]> {
// 2
User.query(on: req.db).all()
}
// 3
func getHandler(_ req: Request)
-> EventLoopFuture<User> {
// 4
User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
}
Here’s what this does:
- Define a new route handler,
getAllHandler(_:)
, that returnsEventLoopFuture<[User]>
. - Return all the users using a Fluent query.
- Define a new route handler,
getHandler(_:)
, that returnsEventLoopFuture<User>
. - Return the user specified by the request’s parameter named
userID
.
Register these two route handlers at the end of boot(routes:)
:
// 1
usersRoute.get(use: getAllHandler)
// 2
usersRoute.get(":userID", use: getHandler)
Here’s what this does:
- Register
getAllHandler(_:)
to process GET requests to /api/users/. - Register
getHandler(_:)
to process GET requests to /api/users/<USER ID>. This uses a dynamic path component that matches the parameter you search for ingetHandler(_:)
.
Build and run the application, then create a new request in RESTed. Configure the request as follows:
- URL: http://localhost:8080/api/users
- method: POST
- Parameter encoding: JSON-encoded
Add two parameters with names and values:
- name: your name
- username: a username of your choice
Send the request and you’ll see the saved user in the response:
Setting up the relationship
Modeling a parent-child relationship in Vapor matches how a database models the relationship, but in a “Swifty” way. Because a user owns each acronym, you add a user property to the acronym. The database represents this as a reference to the user in the acronyms table. This allows Fluent to search the database efficiently.
To get all the acronyms for a user, you retrieve all acronyms that contain that user reference. To get the user of an acronym, you use the user from that acronym. Fluent uses property wrappers to make all this possible.
Open Acronym.swift and add a new property after var long: String
:
@Parent(key: "userID")
var user: User
This adds a User
property of to the model. It uses the @Parent
property wrapper to create the link between the two models. Note this type is not optional, so an acronym must have a user. @Parent
is another special Fluent property wrapper. It tells Fluent that this property represents the parent of a parent-child relationship. Fluent uses this to query the database. @Parent
also allows you to create an Acronym
using only the ID of a User
, without needing a full User
object. This helps avoid additional database queries.
Replace the initializer with the following to reflect this:
// 1
init(
id: UUID? = nil,
short: String,
long: String,
userID: User.IDValue
) {
self.id = id
self.short = short
self.long = long
// 2
self.$user.id = userID
}
Here’s what you changed:
- Add a new parameter to the initializer for the user’s ID of type
User.IDValue
. This is a typealias defined byModel
, which resolves to UUID. - Set the ID of the projected value of the
user
property wrapper. As discussed above, this avoids you having to perform a lookup to get the fullUser
model to create anAcronym
.
Finally, open CreateAcronym.swift. Before .create()
add the following line:
.field("userID", .uuid, .required)
This adds the new column for user
using the key provided to the @Parent
property wrapper. The column type, uuid
, matches the ID column type from CreateUser
.
Domain Transfer Objects (DTOs)
You can send a request with a JSON payload to match the new Acronym
model. However, it looks like:
{
"short": "OMG",
"long": "Oh My God",
"user": {
"id": "2074AD1A-21DC-4238-B3ED-D076BBE5D135"
}
}
Because Acronym
has a user
property, the JSON must match this. The property wrapper allows you to only send an id
for user
, but it’s still complex to create. To solve this, you use a Domain Transfer Object or DTO. A DTO is a type that represents what a client should send or receive. Your route handler then accepts a DTO and converts it into something your code can use. At the bottom of AcronymsController.swift, add the following code:
struct CreateAcronymData: Content {
let short: String
let long: String
let userID: UUID
}
This DTO represents the JSON we expect from the client:
{
"short": "OMG",
"long": "Oh My God",
"userID": "2074AD1A-21DC-4238-B3ED-D076BBE5D135"
}
Next, replace the body of createHandler(_:)
with the following:
// 1
let data = try req.content.decode(CreateAcronymData.self)
// 2
let acronym = Acronym(
short: data.short,
long: data.long,
userID: data.userID)
return acronym.save(on: req.db).map { acronym }
Here’s what the updated code changes:
- Decode the request body to
CreateAcronymData
instead ofAcronym
. - Create an
Acronym
from the data received.
That’s all you need to do to set up the relationship! Before you run the application, you need to reset the database. Fluent has already run the CreateAcronym
migration but the table has a new column now. To add the new column to the table, you must delete the database so Fluent will run the migration again. Stop the application in Xcode and then in Terminal, enter:
# 1
docker stop postgres
# 2
docker rm postgres
# 3
docker run --name postgres -e POSTGRES_DB=vapor_database \
-e POSTGRES_USER=vapor_username \
-e POSTGRES_PASSWORD=vapor_password \
-p 5432:5432 -d postgres
Here’s what this does:
- Stop the running Docker container postgres. This is the container currently running the database.
- Remove the Docker container postgres to delete any existing data.
- Start a new Docker container running PostgreSQL. For more information, see Chapter 6, “Configuring a Database”.
Note: New migrations can also alter tables so you don’t lose production data when changing your models. Chapter 27, “Database/API Versioning & Migration” covers this.
Build and run the application in Xcode and the migrations run. Open RESTed and create a user following the steps from earlier in the chapter. Make sure you copy the returned ID.
Create a new request in RESTed and configure it as follows:
- URL: http://localhost:8080/api/acronyms
- method: POST
- Parameter encoding: JSON-encoded
Add three parameters with names and values:
- short: OMG
- long: Oh My God
- userID: the ID you copied earlier
Click Send Request. Your application creates the acronym with the user specified:
Finally, open AcronymsController.swift and replace updateHandler(_:)
with the following to account for the new property on Acronym
:
func updateHandler(_ req: Request) throws
-> EventLoopFuture<Acronym> {
let updateData =
try req.content.decode(CreateAcronymData.self)
return Acronym
.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
acronym.short = updateData.short
acronym.long = updateData.long
acronym.$user.id = updateData.userID
return acronym.save(on: req.db).map {
acronym
}
}
}
This updates the acronym’s properties with the new values provided in the request, including the new user ID.
Querying the relationship
Users and acronyms are now linked with a parent-child relationship. However, this isn’t very useful until you can query these relationships. Once again, Fluent makes that easy.
Getting the parent
Open AcronymsController.swift and add a new route handler after sortedHandler(_:)
:
// 1
func getUserHandler(_ req: Request)
-> EventLoopFuture<User> {
// 2
Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { acronym in
// 3
acronym.$user.get(on: req.db)
}
}
Here’s what this route handler does:
- Define a new route handler,
getUserHandler(_:)
, that returnsEventLoopFuture<User>
. - Fetch the acronym specified in the request’s parameters and unwrap the returned future.
- Use the property wrapper to get the acronym’s owner from the database. This performs a query on the
User
table to find the user with the ID saved in the database. If you try to access the property withacronym.user
, you’ll get an error because you haven’t retrieved the user from the database. Chapter 31, “Advanced Fluent”, discusses eager loading and working with properties.
Register the route handler at the end of boot(routes:)
:
acronymsRoutes.get(":acronymID", "user", use: getUserHandler)
This connects an HTTP GET request to /api/acronyms/<ACRONYM ID>/user to getUserHandler(_:)
.
Build and run the application, then create a new request in RESTed. Configure the request as follows:
- URL: http://localhost:8080/api/acronyms/\/user
- method: GET
Send the request and you’ll see the response returns the acronym’s user:
Getting the children
Getting the children of a model follows a similar pattern. Open User.swift and add a new property below var username: String
:
@Children(for: \.$user)
var acronyms: [Acronym]
This defines a new property — the user’s acronyms. You annotate the property with the @Children
property wrapper. @Children
tells Fluent that acronyms
represents the children in a parent-child relationship. This is like @ID
and @Field
, which you saw in Chapter 5, “Fluent & Persisting Models”.
Unlike @Parent
, @Children
doesn’t represent any column in the database. Fluent uses it to know what to link for the relationship. You pass the property wrapper a keypath to the parent property wrapper on the child model. In this case, you use \Acronym.$user
, or just \.$user
. Fluent uses this to query the database when retrieving all the children.
Fluent’s use of property wrappers also allows it to handle encoding and decoding of models. User
contains a property for all the acronyms. Normally Codable
would require you to provide all the acronyms to create a user from JSON. When creating an acronym, you would have to instantiate the array as well. @Children
allows you to have the best of both worlds — a property to represent all the children without having to specify it to create the model.
Open UsersController.swift and add a new route handler after getHandler(_:)
:
// 1
func getAcronymsHandler(_ req: Request)
-> EventLoopFuture<[Acronym]> {
// 2
User.find(req.parameters.get("userID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { user in
// 3
user.$acronyms.get(on: req.db)
}
}
Here’s what this route handler does:
- Define a new route handler,
getAcronymsHandler(_:)
, that returnsEventLoopFuture<[Acronym]>
. - Fetch the user specified in the request’s parameters and unwrap the returned future.
- Use the new property wrapper created above to get the acronyms using a Fluent query to return all the acronyms. Remember, this uses the property wrapper‘s projected value, not the wrapped value.
Register the route handler at the end of boot(routes:)
:
usersRoute.get(
":userID",
"acronyms",
use: getAcronymsHandler)
This connects an HTTP GET request to /api/users/<USER ID>/acronyms to getAcronymsHandler(_:)
.
Build and run the application, then create a new request in RESTed. Configure the request as follows:
- URL: http://localhost:8080/api/users/\/acronyms
- method: GET
Send the request and you’ll see the response returns the user’s acronyms:
Foreign key constraints
Foreign key constraints describe a link between two tables. They are frequently used for validation. Currently, there’s no link between the user table and the acronym table in the database. Fluent is the only thing that has knowledge of the link.
Using foreign key constraints has a number of benefits:
- It ensures you can’t create acronyms with users that don’t exist.
- You can’t delete users until you’ve deleted all their acronyms.
- You can’t delete the user table until you’ve deleted the acronym table.
Foreign key constraints are set up in the migration. Open CreateAcronym.swift, and replace .field("userID", .uuid, .required)
with the following:
.field("userID", .uuid, .required, .references("users", "id"))
This is the same as before but also adds a reference from the userID
column to the id
column in the Users table.
Finally, because you’re linking the acronym’s userID
property to the User
table, you must create the User
table first. In configure.swift, move the User
migration to before the Acronym
migration:
app.migrations.add(CreateUser())
app.migrations.add(CreateAcronym())
This ensures Fluent creates the tables in the correct order.
Stop the application in Xcode and follow the steps from earlier to delete the database.
Build and run the application, then create a new request in RESTed. Configure the request as follows:
- URL: http://localhost:8080/api/acronyms/
- method: POST
- Parameter encoding: JSON-encoded
Add three parameters with names and values:
- short: OMG
- long: Oh My God
- userID: E92B49F2-F239-41B4-B26D-85817F0363AB
This is a valid UUID string, but doesn’t refer to any user since the database is empty. Send the request; you’ll get an error saying there’s a foreign key constraint violation:
Create a user as you did earlier and copy the ID. Send the create acronym request again, this time using the valid ID. The application creates the acronym without any errors.
Where to go from here?
In this chapter, you learned how to implement parent-child relationships in Vapor using Fluent. This allows you to start creating complex relationships between models in the database. The next chapter covers the other type of relationship in databases: sibling relationships.