Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

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:

  1. Create a new type for the migration to create the users table in the database.
  2. Implement prepare(on:) as required by Migration.
  3. Set up the schema for User with the name of the table as users.
  4. Create the ID column using the default properties.
  5. 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.
  6. Create the table.
  7. Implement revert(on:) as required by Migration. This deletes the table named users.

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:

  1. Define a new type UsersController that conforms to RouteCollection.
  2. Implement boot(routes:) as required by RouteCollection.
  3. Create a new route group for the path /api/users.
  4. Register createHandler(_:) to handle a POST request to /api/users.
  5. Define the route handler function.
  6. Decode the user from the request body.
  7. Save the decoded user. save(on:) returns EventLoopFuture<Void> so use map(_:) 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:

  1. Create a UsersController instance.
  2. 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:

  1. Define a new route handler, getAllHandler(_:), that returns EventLoopFuture<[User]>.
  2. Return all the users using a Fluent query.
  3. Define a new route handler, getHandler(_:), that returns EventLoopFuture<User>.
  4. 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:

  1. Register getAllHandler(_:) to process GET requests to /api/users/.
  2. Register getHandler(_:) to process GET requests to /api/users/<USER ID>. This uses a dynamic path component that matches the parameter you search for in getHandler(_:).

Build and run the application, then create a new request in RESTed. Configure the request as follows:

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:

  1. Add a new parameter to the initializer for the user’s ID of type User.IDValue. This is a typealias defined by Model, which resolves to UUID.
  2. 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 full User model to create an Acronym.

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:

  1. Decode the request body to CreateAcronymData instead of Acronym.
  2. 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:

  1. Stop the running Docker container postgres. This is the container currently running the database.
  2. Remove the Docker container postgres to delete any existing data.
  3. 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:

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:

  1. Define a new route handler, getUserHandler(_:), that returns EventLoopFuture<User>.
  2. Fetch the acronym specified in the request’s parameters and unwrap the returned future.
  3. 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 with acronym.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:

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:

  1. Define a new route handler, getAcronymsHandler(_:), that returns EventLoopFuture<[Acronym]>.
  2. Fetch the user specified in the request’s parameters and unwrap the returned future.
  3. 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:

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:

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.