Tutorial: How to build one-to-many relations

At the end of this tutorial you will know how to implement and work with one-to-many relations and I will show it to you using pokemons ✨!

This is the longest article I ever wrote due to our our code base getting bigger and bigger. I still gave my best to keep it approachable and overseeable 😊

You can find the result of this tutorial on github here

This tutorial is a natural follow-up of How to write CRUD using Leaf. You can either go for that tutorial first and come back later or be a rebel, skip it and read on 😊


Index

1. Create and generate a new project
2. one-to-many / one-to-one (explanation)
3. Building Model: Pokemon (with relation to User)
4. Adjusting Model: User (defining relation to Pokemons)
5. Building Controller: PokemonController
6. Adjusting Controller: UserController (delete related pokemons)
7. Adjusting View: list all pokemons of a user
8. Adjusting View: implement a form to create new pokemons


1. Create and generate a new project

We will use the outcome of the aforementioned tutorial as a template to create our new project:

vapor new projectName --template=vaporberlin/my-first-crud-using-leaf --branch=vapor-2

Before we generate an Xcode project we would have to change the package name within Package.swift:

// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "projectName", // changed
products: [
.library(name: "App", targets: ["App"]),
.executable(name: "Run", targets: ["Run"])
],
  dependencies: [
...
],
  targets: [
...
]
)

Now in the terminal at the root directory projectName/ execute:

vapor update -y

It may take a bit fetching the dependency, but when done you should have a project structure like this:

projectName/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── Controllers/
│ │ │ └── UserController.swift
│ │ ├── Models/
│ │ │ └── User.swift
│ │ ├── Routes/
│ │ │ └── Routes.swift
│ │ └── Setup/
│ │ ├── Config+Setup.swift
│ │ └── Droplet+Setup.swift
│ └── Run/
├── Tests/
├── Config/
├── Public/
├── Dependencies/
└── Products/

2. one-to-many / one-to-one (explanation)

The first step before we build our models that have relations we will have to figure out what kind of relation we need. Whether we need a many-to-many or one-to-many relation. Why is that? Ohw, I’m glad you asked! Because if we have a many-to-many relation we will need three database tables whereas for one-to-many we only need two. In our scenario we have user and pokemon for whom we want to build relations for. We know a user (poké-trainer) can own more than one pokemon. But a pokemon like a charmander cannot be owned by another user at the same time. That other user might also have a charmander, but that one is not the same. It has a different soul, character, database id 😉.

For our case we will need a User-Model and a Pokemon-Model (the latter has an attribute user_id, so it knows who owns it). That’s two tables:

   User-Table                    Pokemon-Table
┌----┬----------┐ ┌----┬-------------┬----------┬----------┐
│ id │ username │ │ id │ name │ level │ user_id │
├----┼----------┤ ├----┼-------------┼----------┼----------┤
│ 1 │ Brendan │ │ 1 │ charmander │ 13 │ 1 │
│ 2 │ May │ │ 2 │ pickachu │ 10 │ 2 │
└----┴----------┘ │ 3 │ bulbasaur │ 14 │ 1 │
│ 4 │ squirtle │ 11 │ 1 │
└----┴-------------┴----------┴----------┘
NOTE: if you now want to know how many pokemons an user owns, you can simple search in the pokemon-table looking for the users id.

For short: If you have a one-to-one relation it’s the same database structure. Two tables. All you need is to check whether an user owns a pokemon aready before you go and create a new one for him. And if he owns one already you just wont allow another creation by your code. That will end in one-to-one 😊

   User-Table                    Pokemon-Table
┌----┬----------┐ ┌----┬-------------┬----------┬----------┐
│ id │ username │ │ id │ name │ level │ user_id │
├----┼----------┤ ├----┼-------------┼----------┼----------┤
│ 1 │ Brendan │ │ 1 │ charmander │ 13 │ 1 │
│ 2 │ May │ │ 2 │ pickachu │ 10 │ 2 │
└----┴----------┘ └----┴-------------┴----------┴----------┘

3. Building Model: Pokemon (with relation to User)

Create a new swift file in Models/ and name it Pokemon.swift

NOTE: I used the terminal: touch Sources/App/Models/Pokemon.swift

You may have to re-generate your Xcode project with vapor xcode -y in order to let Xcode see your new directory.

In Models/Pokemon.swift include the following code:

import Vapor
import FluentProvider
final class Pokemon: Model {
let storage = Storage()
let name: String
let level: Int
let userId: Int
  init(name: String, level: Int, userId: Int) {
self.name = name
self.level = level
self.userId = userId
}
  init(row: Row) throws {
name = try row.get("name")
level = try row.get("level")
userId = try row.get(User.foreignIdKey)
}
  func makeRow() throws -> Row {
var row = Row()
try row.set("name", name)
try row.set("level", level)
try row.set(User.foreignIdKey, userId)
return row
}
}
NOTE: We use User.foreignIdKey here but this is really nothing special, it’s a string. It’s just safer not to type “user_id” ourself. Reduces typos mypos 😊

Now let’s conform our Pokemon-Model to Preparation:

import Vapor
import FluentProvider
final class Pokemon: Model {
...
}
// MARK: Preparation
extension Pokemon: Preparation {
static func prepare(_ database: Database) throws {
try database.create(self) { builder in
builder.id()
builder.string("name")
builder.string("level")
builder.foreignId(for: User.self)
}
}
  static func revert(_ database: Database) throws {
try database.delete(self)
}
}

The important part here is builder.foreignId(for: User.self) which is the first part for us to define the relation between our Pokemon and the User.

Since we want to display information about our Pokemon in the view as well, we’ll need to conform the Pokemon-Model to NodeRepresentable:

import Vapor
import FluentProvider
final class Pokemon: Model {
...
}
// MARK: Preparation
extension Pokemom: Preparation {
...
}
// MARK: NodeRepresentable
extension Pokemon: NodeRepresentable {
func makeNode(in context: Context?) throws -> Node {
var node = Node(context)
try node.set("name", name)
try node.set("level", level)
try node.set("user_id", userId)
return node
}
}

Now add our Pokemon-Model to preparations in Setup/Config+Setup.swift

import LeafProvider
import FluentProvider
extension Config {
  public func setup() throws {
...
}
  /// Configure providers
private func setupProviders() throws {
...
}
  /// Add all models that should have their
/// schemas prepared before the app boots
private func setupPreparations() throws {
preparations.append(User.self)
preparations.append(Pokemon.self) // added
}
}

If you now cmd+r or run everything should built without any error 😊.

Note: make sure to select Run as a scheme next to your button before running the app

4. Adjusting Model: User (defining relation to Pokemons)

Now to the second part: define the relation between a User and his Pokemon. In our Models/User.swift add the following:

import Vapor
import FluentProvider
final class User: Model {
...
}
// MARK: Fluent Preparation
extension User: Preparation {
...
}
// MARK: Node
extension User: NodeRepresentable {
...
}
// MARK: Children (Pokemon)
extension User {
var pokemons: Children<User, Pokemon> {
return children()
}
}

We extend our user here and define a computed property that returns the users children. You can read Children<User, Pokemon> as Children<From, To> so the relation is defined from User-to-Pokemon as in one-to-many. And that’s it for the relation!

Now the only thing left is to add the children to our node within our makeNode() function so we can actually access them in the view:

import Vapor
import FluentProvider
final class User: Model {
...
}
// MARK: Fluent Preparation
extension User: Preparation {
...
}
// MARK: Node
extension User: NodeRepresentable {
func makeNode(in context: Context?) throws -> Node {
var node = Node(context)
try node.set("id", id)
try node.set("username", username)
try node.set("pokemons", try pokemons.all()) // added
return node
}
}
// MARK: Children (Pokemon)
extension User {
...
}

5. Building Controller: PokemonController

In our Controllers/ directory create a new file and name it PokemonController.swift

NOTE: I used the terminal, just execute:
touch Sources/App/Controllers/PokemonController.swift

You may have to re-generate your Xcode project with vapor xcode -y in order to let Xcode see your new file.

We’ll write a create function inside Controllers/PokemonController.swift:

final class PokemonController {
  func create(_ req: Request) throws -> ResponseRepresentable {
    /// check userId is provided and of type int
guard let userId = req.data["userId"]?.int else {
return Response(status: .badRequest)
}
    /// try to find user with given id (just an existence check)
guard try User.find(userId) != nil else {
return Response(status: .badRequest)
}
    /// check name is given and of type String
guard let pokemonName = req.data["name"]?.string else {
return Response(status: .badRequest)
}
    /// check level is given and of type Int
guard let level = req.data["level"]?.int else {
return Response(status: .badRequest)
}
    /// initiate new pokemon
let pokemon = Pokemon(name: pokemonName, level: level, userId: userId)
    /// save new pokemon to database
try pokemon.save()
    return Response(redirect: "/user")
}
}

Let’s not forget to create a new route for this in our Routes/Routes.swift:

import Vapor
extension Droplet {
func setupRoutes() throws {

let userController = UserController(drop: self)
get("user", handler: userController.list)
post("user", handler: userController.create)
post("user", ":id", "update", handler: userController.update)
post("user", ":id", "delete", handler: userController.delete)
    let pokemonController = PokemonController()  // added
post("pokemon", handler: pokemonController.create) // added

}
}

6. Adjusting Controller: UserController (delete related pokemons)

Now that we’re able to create new pokemons for a user we’ll also need to consider them when deleting a user. So add the following line in our Controller/UserController.swift:

final class UserController {

...
  func list(_ req: Request) throws -> ResponseRepresentable {
...
}
  func create(_ req: Request) throws -> ResponseRepresentable {
...
}
  func update(_ req: Request) throws -> ResponseRepresentable {
...
}
  func delete(_ req: Request) throws -> ResponseRepresentable {
    guard let userId = req.parameters["id"]?.int else {
return Response(status: .badRequest)
}
    guard let user = try User.find(userId) else {
return Response(status: .badRequest)
}
    try user.pokemons.delete()  // added
try user.delete()
return Response(redirect: "/user")
}
}

7. Adjusting View: list all Pokemons of a user

we will first adjust our view to also print all pokemons of a user within Resources/Views/crud.leaf:

NOTE: Select crud.leaf and go to Editor>Syntax Coloring>HTML 😉
<!DOCTYPE html>
<html>
<head>
<title>CRUD</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1 class="text-center"> CRUD </h1>
<div class="row">

<div class="col-xs-12 col-sm-3">
<h2>Create</h2>
...
</div>
      <div class="col-xs-12 col-sm-3">
<h2>Read</h2>
#loop(userlist, "user") {
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<input type="text" name="username" class="form-control" value="#(user.username)" disabled>
<ul>
#loop(user.pokemons, "pokemon") {
<li>#(pokemon.name)</li>
}
</ul>

</div>
</div>
</div>
}
</div>
      <div class="col-xs-12 col-sm-3">
<h2>Update</h2>
...
</div>
      <div class="col-xs-12 col-sm-3">
<h2>Delete</h2>
...
</div>
</div>
</body>
</html>

All we did here is to loop on the property pokemons of a user and print the name for each one 🍃


8. Adjusting View: implement a form to create new pokemons

In the next step we will create a form where we have first: a dropdown to select a user we want the pokemon to be created for and second: the fields (name and level) we need for a new pokemon:

<!DOCTYPE html>
<html>
<head>
<title>CRUD</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1 class="text-center"> CRUD </h1>
<div class="row">

<div class="col-xs-12 col-sm-3">
<h2>Create</h2>
...
</div>
      <div class="col-xs-12 col-sm-3">
<h2>Read</h2>
...
</div>
      <div class="col-xs-12 col-sm-3">
<h2>Update</h2>
...
</div>
      <div class="col-xs-12 col-sm-3">
<h2>Delete</h2>
...
</div>
</div>
    <h1 class="text-center">Pokemon</h1>
<div class="row">
<div class="col-xs-12">

<form method="POST" action="/pokemon" id="pokemon-form">

<div class="form-group">
<h4>User</h4>
<select class="form-control" name="userId" form="pokemon-form">
#loop(userlist, "user") {
<option value="#(user.id)">#(user.username)</option>
}
</select>
</div>
          <div class="form-group">
<h4>Name</h4>
<input type="text" name="name" class="form-control" />
</div>
          <div class="form-group">
<h4>Level</h4>
<input type="text" name="level" class="form-control" />
</div>
          <input type="submit" value="create" class="form-control btn btn-success"/>
        </form>
      </div>
</div>
  </body>
</html>

Let me explain what we did here - some things are really important! I assume you are already familiar with how form-tags work from How to write CRUD using Leaf - but we have something new here. We have given our form-tag an id and we have used a select-tag where we refer to our form using that id. This is super important otherwise values from our dropdown wont be sent! This was new to me - I though it will just work as long as it’s within the form. Just the way input-tags work. Ohw how naive.. Turned out you can have your select-tag wherever you want! As long as you refer to your form with form=”id-of-the-form” the values will be sent along as soon as the from is submitted! 🤓 👍🏻

NOTE: In order to sent values from a dropdown (select-tag) alongside a form you need to give your form an id and refer from your select-tag to that form using that id!

Our final cmd+r or run and refresh of our site and that’s it! You successfully implemented one-to-many relations 🎉 🎉 !!


Thank you a lot for reading! If you have any questions or improvements — write a comment! I would love to hear from you! 😊