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

10. Sibling Relationships
Written by Tim Condon

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In Chapter 9, “Parent Child Relationships”, you learned how to use Fluent to build parent-child relationships between models. This chapter shows you how to implement the other type of relationship: sibling relationships. You’ll learn how to model them in Vapor and how to use them in 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.

Sibling relationships

Sibling relationships describe a relationship that links two models to each other. They are also known as many-to-many relationships. Unlike parent-child relationships, there are no constraints between models in a sibling relationship.

For instance, if you model the relationship between pets and toys, a pet can have one or more toys and a toy can be used by one or more pets. In the TIL application, you’ll be able to categorize acronyms. An acronym can be part of one or more categories and a category can contain one or more acronyms.

Creating a category

To implement categories, you’ll need to create a model, a migration, a controller and a pivot. Begin by creating the model.

Category model

In Xcode, create a new file Category.swift in Sources/App/Models. Open the file and insert a basic model for a category:

import Fluent
import Vapor

final class Category: Model, Content {
  static let schema = "categories"
  
  @ID
  var id: UUID?
  
  @Field(key: "name")
  var name: String
  
  init() {}
  
  init(id: UUID? = nil, name: String) {
    self.id = id
    self.name = name
  }
}
import Fluent

struct CreateCategory: Migration {
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    database.schema("categories")
      .id()
      .field("name", .string, .required)
      .create()
  }
  
  func revert(on database: Database) -> EventLoopFuture<Void> {
    database.schema("categories").delete()
  }
}
app.migrations.add(CreateCategory())

Category controller

Now it’s time to create the controller. In Sources/App/Controllers, create a new file called CategoriesController.swift. Open the file and add code for a new controller to create and retrieve categories:

import Vapor

// 1
struct CategoriesController: RouteCollection {
  // 2
  func boot(routes: RoutesBuilder) throws {
    // 3
    let categoriesRoute = routes.grouped("api", "categories")
    // 4
    categoriesRoute.post(use: createHandler)
    categoriesRoute.get(use: getAllHandler)
    categoriesRoute.get(":categoryID", use: getHandler)
  }
  
  // 5
  func createHandler(_ req: Request) 
    throws -> EventLoopFuture<Category> {
    // 6
    let category = try req.content.decode(Category.self)
    return category.save(on: req.db).map { category }
  }
  
  // 7
  func getAllHandler(_ req: Request) 
    throws -> EventLoopFuture<[Category]> {
    // 8
    Category.query(on: req.db).all()
  }
  
  // 9
  func getHandler(_ req: Request) 
    throws -> EventLoopFuture<Category> {
    // 10
    Category.find(req.parameters.get("categoryID"), on: req.db)
      .unwrap(or: Abort(.notFound))
  }
}
let categoriesController = CategoriesController()
try app.register(collection: categoriesController)

Creating a pivot

In Chapter 9, “Parent Child Relationships”, you added a reference to the user in the acronym to create the relationship between an acronym and a user. However, you can’t model a sibling relationship like this as it would be too inefficient to query. If you had an array of acronyms inside a category, to search for all categories of an acronym you’d have to inspect every category. If you had an array of categories inside an acronym, to search for all acronyms in a category you’d have to inspect every acronym. You need a separate model to hold on to this relationship. In Fluent, this is a pivot.

import Fluent
import Foundation

// 1
final class AcronymCategoryPivot: Model {
  static let schema = "acronym-category-pivot"
  
  // 1
  @ID
  var id: UUID?
  
  // 3
  @Parent(key: "acronymID")
  var acronym: Acronym
  
  // 3
  @Parent(key: "categoryID")
  var category: Category
  
  // 4
  init() {}
  
  // 5
  init(
    id: UUID? = nil, 
    acronym: Acronym,
    category: Category
  ) throws {
    self.id = id
    self.$acronym.id = try acronym.requireID()
    self.$category.id = try category.requireID()
  }
}
import Fluent

// 1
struct CreateAcronymCategoryPivot: Migration {
  // 2
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    // 3
    database.schema("acronym-category-pivot")
      // 4
      .id()
      // 5
      .field("acronymID", .uuid, .required,
        .references("acronyms", "id", onDelete: .cascade))
      .field("categoryID", .uuid, .required,
        .references("categories", "id", onDelete: .cascade))
      // 6
      .create()
  }
  
  // 7
  func revert(on database: Database) -> EventLoopFuture<Void> {
    database.schema("acronym-category-pivot").delete()
  }
}
app.migrations.add(CreateAcronymCategoryPivot())
@Siblings(
  through: AcronymCategoryPivot.self,
  from: \.$acronym,
  to: \.$category)
var categories: [Category]
// 1
func addCategoriesHandler(_ req: Request) 
  throws -> EventLoopFuture<HTTPStatus> {
  // 2
  let acronymQuery = 
    Acronym.find(req.parameters.get("acronymID"), on: req.db)
      .unwrap(or: Abort(.notFound))
  let categoryQuery = 
    Category.find(req.parameters.get("categoryID"), on: req.db)
      .unwrap(or: Abort(.notFound))
  // 3
  return acronymQuery.and(categoryQuery)
    .flatMap { acronym, category in
      acronym
        .$categories
        .attach(category, on: req.db)
        .transform(to: .created)
    }
}
acronymsRoutes.post(
  ":acronymID", 
  "categories", 
  ":categoryID", 
  use: addCategoriesHandler)

Querying the relationship

Acronyms and categories are now linked with a sibling relationship. But this isn’t very useful if you can’t view these relationships! Fluent provides functions that allow you to query these relationships. You’ve already used one above to create the relationship.

Acronym’s categories

Open AcronymsController.swift and add a new route handler after addCategoriesHandler(:_):

// 1
func getCategoriesHandler(_ req: Request) 
  throws -> EventLoopFuture<[Category]> {
  // 2
  Acronym.find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { acronym in
      // 3
      acronym.$categories.query(on: req.db).all()
    }
}
acronymsRoutes.get(
  ":acronymID", 
  "categories", 
  use: getCategoriesHandler)

Category’s acronyms

Open Category.swift and add a new property annotated with @Siblings below var name: String:

@Siblings(
  through: AcronymCategoryPivot.self, 
  from: \.$category,
  to: \.$acronym)
var acronyms: [Acronym]
// 1
func getAcronymsHandler(_ req: Request) 
  throws -> EventLoopFuture<[Acronym]> {
  // 2
  Category.find(req.parameters.get("categoryID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { category in
      // 3
      category.$acronyms.get(on: req.db)
    }
}
categoriesRoute.get(
  ":categoryID", 
  "acronyms", 
  use: getAcronymsHandler)

Removing the relationship

Removing a relationship between an acronym and a category is very similar to adding the relationship. Open AcronymsController.swift and add the following below getCategoriesHandler(req:):

// 1
func removeCategoriesHandler(_ req: Request) 
  throws -> EventLoopFuture<HTTPStatus> {
  // 2
  let acronymQuery = 
    Acronym.find(req.parameters.get("acronymID"), on: req.db)
      .unwrap(or: Abort(.notFound))
  let categoryQuery = 
    Category.find(req.parameters.get("categoryID"), on: req.db)
      .unwrap(or: Abort(.notFound))
  // 3
  return acronymQuery.and(categoryQuery)
    .flatMap { acronym, category in
      // 4
      acronym
        .$categories
        .detach(category, on: req.db)
        .transform(to: .noContent)
    }
}
acronymsRoutes.delete(
  ":acronymID", 
  "categories", 
  ":categoryID", 
  use: removeCategoriesHandler)

Where to go from here?

In this chapter, you learned how to implement sibling relationships in Vapor using Fluent. Over the course of this section, you learned how to use Fluent to model all types of relationships and perform advanced queries. The TIL API is fully featured and ready for use by clients.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now