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 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
}
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
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:
Model
Userlist
#for(user in userlist) {
#(user.username)
}
I have marked the interesting things here. With the tag I just add bootstrap which is a **css **framework that makes our view look a bit nicer.
With the **