Tutorial: How to write Models using Fluent

Martin Lasek
Oct 2, 2017 · 8 min read
Image for post
Image for post

This tutorial will show you how to implement a simple user model, store him to the database, get him back from the database and pass him to the view. 🍃

You can find the result of this tutorial on github here

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

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 --branch=vapor-2

Before we generate an Xcode project we would have to add the Fluent-Provider as a dependency and also change the package name within Package.swift:

// swift-tools-version:4.0import PackageDescriptionlet package = Package(
name: "yourProjectName", // changed
products: [
.library(name: "App", targets: ["App"]),
.executable(name: "Run", targets: ["Run"])
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "2.1.0")),
.package(url: "https://github.com/vapor/leaf-provider.git", .upToNextMajor(from: "1.1.0")),
.package(url: "https://github.com/vapor/fluent-provider.git", .upToNextMajor(from: "1.3.0")) // added
],
targets: [
.target(name: "App", dependencies: ["Vapor", "LeafProvider", "FluentProvider"], // added
exclude: [
"Config",
"Public",
"Resources",
]
),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App", "Testing"])
]
)

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:

yourProjectName/
├── Package.swift
├── Sources/
│ ├── App/
│ │ ├── Routes/
│ │ │ └── Routes.swift
│ │ └── Setup/
│ │ ├── Config+Setup.swift
│ │ └── Droplet+Setup.swift
│ └── Run/
├── Tests/
├── Config/
├── Public/
├── Dependencies/
└── Products/

Our first step is to add the FluentProvider in our Setup/Config+Setup.swift:

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

NOTE: No worries if Xcode complains about “no such module” it doesn’t know it better but it’s there — you can check it in Dependencies/ 😉

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 Vapor
import FluentProvider
final class User: Model {
var storage = Storage()
var username: String
init(username: String) {
self.username = username
}
func makeRow() throws -> Row {
var row = Row()
try row.set("username", username)
return row
}
init(row: Row) throws {
self.username = try row.get("username")
}
}

I kept it super simple so we can understand what is going on here. By inheriting from Model protocol we have to implement makeRow() and init(row: Row). The first function defines the representation of your user for the database table like for the column “username” set the value of the variable username. And the second function describes how to initialize your user from a database table entry like get the value at the column “username” and assign it to username. So far so clear. 😊

We have to conform our user to one more protocol to be able to store and fetch him from the database. In our User.swift at the very bottom add:

import Vapor
import FluentProvider
final class User: Model {
...
}
// MARK: Fluent Preparationextension User: Preparation {
static func prepare(_ database: Database) throws {
try database.create(self) { builder in
builder.id()
builder.string("username")
}
}
static func revert(_ database: Database) throws {
try database.delete(self)
}
}

With conforming to Preparation you define within prepare() what column fields your table shall have (and which type) and with revert() you make it possible to delete the user table.

We now have to register our user model within Setup/Config+Setup.swift so the prepare function we just wrote get’s called when starting the project:

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) // added
}
}

If you now cmd+r or run everything should be just fine. 😊

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

But you may have asked yourself wait a second. Database? What database? Well you can have a look into Config/fluent.json and find:

...
"driver": "memory",
...

It tells you what is used by fluent as a driver (database) that is memory here.

NOTE: If you want to learn how to connect a database like postgresql, mongodb or mysql, I wrote tutorials about them too, have a look at my-first-database at my repository. Each project links to a medium article.

We could now store and retrieve a user from the in memory database but before we do so we want to conform our user to one last protocol which enables us passing him to a view. So in your User.swift at the bottom add:

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

With makeNode() we define the representation of our user as a node object. All we do is creating a node object and setting the username at “username”.

Yup we don’t have any user in our database yet, but we’ll come to this in a second. For now let’s go to our Routes/Routes.swift and delete everything within setupRoutes() so it ends up looking like:

import Vaporextension Droplet {
func setupRoutes() throws {
// nothing in here
}
}

Define a get route at the url user retrieving all users from the database and pass them into a view as node object:

import Vaporextension Droplet {
func setupRoutes() throws {
get("user") { req in
let list = try User.all()
return try self.view.make("userview", ["userlist": list.makeNode(in: nil)])
}

}
}

NOTE: We use our User class to query the database.

We are retrieving all() user from the database. Then we return a view named userview into which we pass our variable list converted into node object. To make this work we have to create a userview.leaf file. Let’s do it!

Within Resources/Views/ delete all files you find in there (myview.leaf and mydataview.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/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container">
<h1> Userlist </h1>
<form method="POST" action="/user">
<div class="input-group">
<input type="text" name="username" class="form-control">
<span class="input-group-btn">
<input class="btn btn-default" type="submit" />
</span>
</div>
</form>
#loop(userlist, "user") {
<div class="row">
<div class="col-xs-12">
#(user.username)
</div>
</div>
}

</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 /user with the method post. We will implement that post route 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 #loop() is a leaf specific tag where we iterate over the userlist we passed earlier to the view as [“userlist”: …] and with #(user.username) we can print the value of a variable.

If you now cmd+r or run the project and fire up your site at /user you will only see the header, input field and a button. And that’s perfectly fine.

Image for post
Image for post

NOTE: watch your Xcode console to see where your project is running, for me it’s 127.0.0.1:8003 — you can change the port in Config/server.json

In our Routes/Routes.swift add the following code:

import Vaporextension Droplet {
func setupRoutes() throws {
get("user") { req in
...
}
post("user") { req in
guard let username = req.data["username"]?.string else {
return Response(status: .badRequest)
}
let user = User(username: username)
try user.save()
return Response(redirect: "/user")
}

}
}

When we submit our form in the view by hitting the submit button, it will send the input-field data url-encoded to the /user route as a post like:

username=MartinLasek

In our post route we grab the data at username which is the key we defined in our input-field as name=”username” (now we understand the importance) and try to get it as a string. Then we initiate our user with the username, call save() on him to store him into the database and redirect to our get(“user”) route.

If you now cmd+r or run your project and fire 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 😉


Martin Lasek

Written by

I'm an always optimistic, open minded and knowledge seeking fullstack developer passionate about UI/UX and changing things for the better :)

Martin Lasek

Written by

I'm an always optimistic, open minded and knowledge seeking fullstack developer passionate about UI/UX and changing things for the better :)

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store