Tutorial: How to write Models using Fluent
1. Create 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-leaf-template=
2. Generate Xcode project
Before we generate an Xcode project we would have to add a database provider. There are each for every database. But for the sake of getting warm with the ORM Fluent we will use an in memory database. Therefor we add Fluent-SQLite as a dependency within our Package.swift:
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "projectName", // changed
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") // added
],
targets: [
.target(name: "App", dependencies: ["Vapor", "Leaf", "FluentSQLite"]), // added
.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, generating the Xcode project and opening it for you. When done you should have a project structure like this:
projectName/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── app.swift
│ │ ├── boot.swift
│ │ ├── configure.swift
│ │ └── routes.swift
│ └── Run/
│ └── main.swift
├── Tests/
├── Resources/
├── Public/
├── Dependencies/
└── Products/
If you see an error including “CNIOOpenSSL” when cmd+r you’re missing a dependency. Just run brew upgrade vapor and re-generate the project* ✌🏻😊*
3. Configure your project to use a SQLite database
Our first step is to add the FluentSQLiteProvider within our configure.swift:
import Vapor
import Leaf
import FluentSQLite // added
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
// Register routes to the router
let router = EngineRouter.default()
try routes(router)
services.register(router, as: Router.self)
let leafProvider = LeafProvider()
try services.register(leafProvider)
try services.register(FluentSQLiteProvider()) // added
config.prefer(LeafRenderer.self, for: ViewRenderer.self)
}
Next we will initiate a database service, add a SQLiteDatabase to it and register that database service:
import Vapor
import Leaf
import FluentSQLite
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
// Register routes to the router
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)
}
Finally we initiate and register a migration service that we will use later in order to introduce our Model to our database. For now add the following:
import Vapor
import Leaf
import FluentSQLite
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
// Register routes to the router
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 = DatabaseConfig()
try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite)
services.register(databases)
var migrations = MigrationConfig()
services.register(migrations)
}
4. Create your first model
Create a directory within Sources/App/ and name it **Models/ **and within that new directory create a new swift file called User.swift 😊
NOTE: I used the terminal executing mkdir Sources/App/Models/ and touch Sources/App/Models/User.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/User.swift include the following code:
import FluentSQLite
import Vapor
final class User: SQLiteModel {
var id: Int?
var username: String
init(id: Int? = nil, username: String) {
self.id = id
self.username = username
}
}
extension User: Content {}
extension User: Migration {}
I kept it super simple so we can understand what is going on here. By conforming to **SQLiteModel **we have to define an optional variable named idthat is of type int. It is optional simply because if we initiate a new user in order to store him to the database it’s not up to us to give him an id at that point. He will get an id assigned after storing him to the database.
The conformance to **Content **makes it possible so our User can convert into for example JSON using Codable if we would return him in a route. Or so he can convert into TemplateData which is used within a Leaf view. And due to Codable that happens automagically. Conforming to **Migration **is needed so Fluent can use Codable to create the best possible database table schema and also so we are able to add it to our migration service in our configure.swift:
import Vapor
import Leaf
import FluentSQLite
public func configure(
_ config: inout Config,
_ env: inout Environment,
_ services: inout Services
) throws {
// Register routes to the router
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 = DatabaseConfig()
try databases.add(database: SQLiteDatabase(storage: .memory), as: .sqlite)
services.register(databases)
var migrations = MigrationConfig()
migrations.add(model: User.self, database: .sqlite)
services.register(migrations)
}
If you now cmd+r
or run
everything should be just fine. 😊
5. Implement a GET route to list all users
Yup we don’t have any users in our database yet but we will create and store users using a form we will implement our own. So for now go to routes.swiftand delete everything in that file so it ends up looking like this:
import Vapor
import Leaf
public func routes(_ router: Router) throws {
// nothing in here
}
Define a get route at the url users fetching all users from the database and pass them into a view:
import Vapor
import Leaf
public func routes(_ router: Router) throws {
router.get("users") { req -> Future<View> in
return User.query(on: req).all().flatMap { users in
let data = ["userlist": users]
return try req.view().render("userview", data)
}
}
}
Wow. There’s a lot going on here. But no worries it’s way simpler than it looks and you’ll feel great once you read further and understand this black magic ✨
VAPOR 3 is all about Futures. And it comes from its nature being Async now.
6. Explaining Async
Let’s have a life example. In Vapor 2 if a boy was told by his girlfriend to buy her a soft ice and a donut. He would go to the ice wagon, order ice and wait until it’s ready. Then he would continue and go to a donut shop, buy one and go back to his his girlfriend with both.
With Vapor 3 that boy would go to the ice wagon, order ice and in the time the ice is made he would go to the donut shop and buy a donut. He comes back to the ice wagon when the ice is ready, gets it and goes back to his girlfriend with both.
Vapor 2: Boy was blocked by the ice order to finish until he can proceed.
Vapor 3: Boy works non-blocking and uses his waiting time for other tasks.
7. What the F.. uture
Let’s understand what is happening and why. We are using our User class to query the database. And you can read it like executing that query on the back of our **request. **Think of the request as being the boy. The one who does the work for us. The worker.
Okay so we don’t have an array of users but a future of an array of users: **Future<[Users]>. **And honestly.That’s it. And what I mean by that is: there’s is nothing fancy or special to it. That’s simply it. It’s just “wrapped” by a Future. The only thing that we care about is how in mothers name do we get our data out of the future if we want to work with it the way we are used to 😊
That’s where **map **or **flatMap **comes into play.
We choose map if the body of the call returns a non-future value.
someFuture.map { data in
return value
}
And we call **flatMap **if the body does return a future value.
someFuture.flatMap { data in
return Future<value>
}
There is only this simple rule. Because since you need to return something in each map function, that something is what tells you whether you use flatMapor map. The rule is: If that something is a Future you use flatMap and if it is “normal” data thus not a Future you use map.
So in our route we want to access the array of users in order to pass it to our view. So we need one of both map functions. And since we return whatever the render() function returns. And if we cmd+click on it we can see it’s a Future<View>, we have learned: if we return a Future use flatMap.
And that’s all we do here. No worries if it feels weird and new and not so intuitive. And you don’t feel like you would know when to use what and how. That’s what I am here for (hopefully) 😊. Follow along the tutorials and ask me or the community in Discord all kind of questions and believe me it will click! Honestly I wasn’t sure it would click for me until it did. Give it time ✨!
8. Create a view
Within Resources/Views/ delete all files you find in there (welcome.leafand whoami.leaf) and create a new file named **userview.leaf **and add:
<!DOCTYPE html>
<html>
<head>
<title>Model</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1 class="mt-3"> Userlist </h1>
<form method="POST" action="/users">
<div class="input-group">
<input type="text" name="username" class="form-control">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="submit">
Create
</button>
</div>
</div>
</form>
#for(user in userlist) {
<p class="mb-0">
#(user.username)
</p>
}
</body>
</html>
I have marked the interesting things here. With the <link> tag I just add bootstrap which is a **css **framework that makes our view look a bit nicer.
With the **<form> **tag we define what shall happen when we submit the form, which is firing at /users with the method **post. **We will implement that postroute in second.
The **<input type=”text” name=”username”> **is our input field to write a new username into and here the **name=”username” **is super important because **“username” **is the key where our text will be connected to. You’ll understand what I mean when we write our post route.
The **#for() **loopis a leaf specific tag where we iterate over the userlist we passed earlier to the view as [“userlist”: …] and with **#(user.username) **we can access our objects just like in swift.
If you now **cmd+r **or run the project and fire up your site at /users you will see the **header, input field **and a **button. **And that’s perfectly fine. The list of user will appear as soon as we have created some. Let’s do it 🚀!
9. Implement a POST route to store a user
In our routes.swift add the following code:
import Vapor
import Leaf
public func routes(_ router: Router) throws {
router.get("users") { req -> Future<View> in
...
}
router.post("users") { req -> Future<Response> in
return try req.content.decode(User.self).flatMap { user in
return user.save(on: req).map { _ in
return req.redirect(to: "users")
}
}
}
}
When we submit our form in the view by hitting the submit button, it will send the input-field data *form-url-encoded *to the /users route as a **post **like:
username=MartinLasek
Since our User model consists of only one property username and conforms to the protocol Content we are able to to decode the content that is sent by the form into an instance of our user. You can try what happens if you add another property to our User class like age of type **Int, **re-run the project and try submitting the form again. It will tell you that it couldn’t find an Int at the path age. Because our form doesn’t send **age=23 **alongside the username.
However since decode returns a Future of an user instance, we will have to use one of the map/flatMap functions in order to access it. Now a small side note. Both functions as a whole will result in a Future. I am not talking about the body of these functions. I am talking about the whole call. It’s important because it explains why we call flatMap on decode.
Again flatMap is used when the body will give us a Future. And we just learned that **flatMap **and map as a whole will always result in a Future. You can see that we return **user.save(on: req).map { … } **now since this one as a whole will result in a Future we know we have to use **flatMap **upfront.
Next one is easy because redirect is not returning a future so that’s why we use map upfront here 😊
To access a future value you either need **map or flatMap. **If your return value inside a map function is not a future you use **map **otherwise flatMap.
If you now **cmd+r **or **run **your project andfire up your site in your browser you will be able to create new users and see a growing list when doing so 🙌🏻
NOTE: Since we use an in memory database all data will be gone after **re-run **😉
10. Where to go from here
You can find a list of all tutorials with example projects on Github here:
👉🏻 https://github.com/vaporberlin/vaporschool