Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · 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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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
  }
}
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()
  }
}
app.migrations.add(CreateUser())

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 }
  }
}
// 1
let usersController = UsersController()
// 2
try app.register(collection: usersController)
// 1
func getAllHandler(_ req: Request) 
  throws -> EventLoopFuture<[User]> {
  // 2
  User.query(on: req.db).all()
}

// 3
func getHandler(_ req: Request) 
  throws -> EventLoopFuture<User> {
  // 4
  User.find(req.parameters.get("userID"), on: req.db)
      .unwrap(or: Abort(.notFound))
}
// 1
usersRoute.get(use: getAllHandler)
// 2
usersRoute.get(":userID", use: getHandler)

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.

@Parent(key: "userID")
var user: User
// 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
}
.field("userID", .uuid, .required)

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"
  }
}
struct CreateAcronymData: Content {
  let short: String
  let long: String
  let userID: UUID
}
{
  "short": "OMG",
  "long": "Oh My God",
  "userID": "2074AD1A-21DC-4238-B3ED-D076BBE5D135"
}
// 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 }
# 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

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
      }
    }
}

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) 
  throws -> 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)
    }
}
acronymsRoutes.get(":acronymID", "user", use: getUserHandler)

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]
// 1
func getAcronymsHandler(_ req: Request) 
  throws -> 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)
    }
}
usersRoute.get(
  ":userID", 
  "acronyms", 
  use: getAcronymsHandler)

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.

.field("userID", .uuid, .required, .references("users", "id"))
app.migrations.add(CreateUser())
app.migrations.add(CreateAcronym())

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.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now