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 and one-to-one relations ✨!
You can find the result of this tutorial on github here
Index
1. Create and generate a new project
2. one-to-many / one-to-one / many-to-many
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
9. Where to go from here
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
Before we generate an Xcode project we would have to change the package name within Package.swift:
// swift-tools-version:4.0
import PackageDescriptionlet package = Package(
name: "projectName",
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/leaf.git", from: "3.0.0-rc"),
.package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0-rc")
],
targets: [
.target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite"]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App"]),
]
)
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
│ │ ├── app.swift
│ │ ├── boot.swift
│ │ ├── configure.swift
│ │ └── routes.swift
│ └── Run/
│ └── main.swift
├── Tests/
├── Resources/
│ └── Views/
│ └── crud.leaf
├── Public/
├── Dependencies/
└── Products/
2. one-to-many / one-to-one / many-to-many
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. How is that? Alrighty, I see you’re sharp. It’s because for example in a one-to-many relation we have a user (poké-trainer) who can own more than one pokémon. But a pokemon like a charmander cannot be owned by more than one user (poké-trainer). Each pokemon gets an user_id so this id can be used to fetch the right user from the User-Table.
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 😊
So for completeness here’s how many-to-many would look like:
Here you would have a User-Table, a Class-Table and a relation table. As you can see a User can be in many classes just like May. She is in class moon and wood. Thus the relation table has two entries for her. One is referring to the class with the id 1 and another entry is referring to the class with the id 2. You can also see that a class can have many user. You can see that on the relation table we have two users (user_id: 1, 2) referring to the class_id: 1 (moon).
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 FluentSQLite
import Vaporfinal class Pokemon: SQLiteModel {
var id: Int?
var name: String
var level: String init(
id: Int? = nil,
name: String,
level: String
) {
self.id = id
self.name = name
self.level = level
}
}extension Pokemon: Migration {}
Now this is a simple model with no relation to a user. Let’s add the relation:
import FluentSQLite
import Vaporfinal class Pokemon: SQLiteModel {
var id: Int?
var name: String
var level: Int
var userID: User.IDinit(
id: Int? = nil,
name: String,
level: Int,
userID: User.ID
) {
self.id = id
self.name = name
self.level = level
self.userID = userID
}
}extension User: Content {}
extension User: Migration {}
extension User: Parameter {}extension Pokemon {
var user: Parent<Pokemon, User> {
return parent(\.userID)
}
}
Can you actually believe that it’s that simple? All we really need is adding a property to store the user id and another property with the relation.
The important part is the relation defined with the Parent<Child, Parent> struct where our Pokemon class is the child and our User class is the parent. Now accessing the property (here called user) will return the parent that can be found with providing the userID key path. Fluent now knows everything it needs to know for the relation. It knows both tables: Pokemon and User. And that from Pokemon we can get the parent with the value behind the property userID.
That means whenever we have a pokemon instance we will always be able to get the parent of it by accessing the user property.
Now add our Pokemon-Model to migrations in configure.swift:
import Vapor
import Leaf
import FluentSQLitepublic func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws { let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self) let leafProvider = LeafProvider()
try services.register(leafProvider)
try services.register(FluentSQLiteProvider()) config.prefer(LeafRenderer.self, for: ViewRenderer.self) var databases = DatabasesConfig()
try databases.add(
database: SQLiteDatabase(storage: .memory),
as: .sqlite
)
services.register(databases) var migrations = MigrationConfig()
migrations.add(model: User.self, database: .sqlite)
migrations.add(model: Pokemon.self, database: .sqlite)
services.register(migrations)
}
If you now cmd+r or run everything should built without any error 😊.
4. Adjusting Model: User (defining relation to Pokemons)
Now to the second part: define the relation between a User and his Pokemon. Let’s go to our Models/User.swift and add a Child relation to it so we are also able to get all pokemons of a user:
import FluentSQLite
import Vaporfinal class User: SQLiteModel {
var id: Int?
var username: Stringinit(id: Int? = nil, username: String) {
self.id = id
self.username = username
}
}extension User: Content {}
extension User: Migration {}
extension User: Parameter {}extension User {
var pokemons: Children<User, Pokemon> {
return children(\.userID)
}
}
5. Building Controller: PokemonController
Our goal is to be able to create a pokemon for a user. So 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:
import Vaporfinal class PokemonController { func create(_ req: Request) throws -> Future<Response> {
return try req.content
.decode(Pokemon.PokemonForm.self)
.flatMap { pokemonForm in return User
.find(pokemonForm.userId, on: req)
.flatMap { user in guard let userId = try user?.requireID() else {
throw Abort(.badRequest)
} let pokemon = Pokemon(
name: pokemonForm.name,
level: pokemonForm.level,
userID: userId
) return pokemon.save(on: req).map { _ in
return req.redirect(to: "/users")
}
}
}
}
}
You must have noticed that I use Pokemon.PokemonForm.self to what I decode the content to. I added a struct that will match exactly its properties to the fields of the form we will use to create a new pokemon in a few minutes. This is emerging as a best practice in the community. In Pokemon.swift add:
import FluentSQLite
import Vaporfinal class Pokemon: SQLiteModel {
var id: Int?
var name: String
var level: Int
var userID: User.ID init(
id: Int? = nil,
name: String,
level: Int,
userID: User.ID
) {
self.id = id
self.name = name
self.level = level
self.userID = userID
} struct PokemonForm: Content {
var name: String
var level: Int
var userId: Int
}
}extension Pokemon: Migration {}extension Pokemon {
var user: Parent<Pokemon, User> {
return parent(\.userID)
}
}
Now let’s add a new route for our fresh create function in routes.swift:
import Vaporpublic func routes(_ router: Router) throws { let userController = UserController()
router.get("users", use: userController.list)
router.post("users", use: userController.create)
router.post("users", User.parameter, "update", use: userController.update)
router.post("users", User.parameter, "delete", use: userController.delete) let pokemonController = PokemonController()
router.post("pokemon", use: pokemonController.create)
}
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 adjust the following line in our Controller/UserController.swift:
import Vaporfinal class UserController { func list(_ req: Request) throws -> Future<View> {
...
} func create(_ req: Request) throws -> Future<Response> {
...
} func update(_ req: Request) throws -> Future<Response> {
...
} func delete(_ req: Request) throws -> Future<Response> {
return try req.parameters.next(User.self).flatMap { user in
return try user.pokemons.query(on: req).delete().flatMap { _ in
return user.delete(on: req).map { _ in
return req.redirect(to: "/users")
}
}
}
}
}struct UserForm: Content {
var username: String
}
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>
#for(user in userlist) {
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<input type="text" name="username" class="form-control" value="#(user.username)" disabled>
#for(pokemon in user.pokemons) {
<li>#(pokemon.name)
}
</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>
Now this looks and feels like it would work but it wont. And the reason is because user is only having a query to its related pokemons stored in the property but not the pokemon instances themselves. Right now you’d get:
{"error":true,"reason":"Could not convert iterator data to array. (/Path/To/yourProject/Resources\/Views\/crud.leaf line: 28 column: 33 range: 1211..<1268)"}
What we want to do now is fetch the related pokemon before we pass both, user and pokemon into the view. At best wrap them even in an own struct:
It will make sense if you see it, so in Controllers/UserController.swift add:
import Vaporfinal class UserController { func list(_ req: Request) throws -> Future<View> {
let allUsers = User.query(on: req).all()
return allUsers.flatMap { users in let userViewList = try users.map { user in
return UserView(
user: user,
pokemons: try user.pokemons.query(on: req).all()
)
} let data = ["userViewlist": userViewList]
return try req.view().render("crud", data)
}
} func create(_ req: Request) throws -> Future<Response> {
...
} func update(_ req: Request) throws -> Future<Response> {
...
} func delete(_ req: Request) throws -> Future<Response> {
...
}
}struct UserForm: Content {
var username: String
}struct UserView: Encodable {
var user: User
var pokemons: Future<[Pokemon]>
}
We created a struct that conforms to Encodable. That struct would hold a user and an array of the user’s pokemons. It’s a Future array but that doesn’t bother us at all. You won’t notice it even, you’ll see 😊. Okay and inside our function all we really do is mapping each user to a UserView instance. Inside the map we pass the user and the result of the executed pokemon query into a new UserView instance.
Okay let’s adjust our curd.leaf file now to handle the new structure of data:
<!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>
#for(userView in userViewlist) {
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<input type="text" name="username" class="form-control" value="#(user.username)" disabled>
#for(pokemon in userView.pokemons) {
<li>#(pokemon.name)
}
</div>
</div>
</div>
}
</div> <div class="col-xs-12 col-sm-3">
<h2>Update</h2>
#for(userView in userViewlist) {
<form method="POST" action="/users/#(userView.user.id)/update">
<div class="input-group form-group">
<input type="text" name="username" class="form-control" value="#(userView.user.username)">
<span class="input-group-btn">
<input class="btn btn-primary" type="submit" value="update" />
</span>
</div>
</form>
}
</div> <div class="col-xs-12 col-sm-3">
<h2>Delete</h2>
#for(userView in userViewlist) {
<form method="POST" action="users/#(userView.user.id)/delete">
<div class="form-group input-group">
<input type="text" name="username" class="form-control" value="#(userView.user.username)" disabled>
<span class="input-group-btn">
<input class="btn btn-danger" type="submit" value="delete" />
</span>
</div>
</form>
}
</div>
</div>
</body>
</html>
We are only making sure to access the user inside our UserView wrapper and also that we are accessing pokemons inside our UserView wrapper. If you now run and try creating a new user, it will work just as expected. But we wont see any pokemons, yet. Let’s add a form to create a pokemon!
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> <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">
#for(userView in userViewlist) {
<option value="#(userView.user.id)">#(userView.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 🎉 🎉 !!
9. Where to go from here
You can find a list of all tutorials with example projects on Github here:
👉🏻 https://github.com/vaporberlin/vaporschool