Tutorial: How to write Models using Fluent

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+ror 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


I am really happy you read my article! If you have any suggestions or improvements of any kind let me know! I’d love to hear from you! 😊

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,279评论 0 10
  • W先生和L小姐是朋友介绍认识的,第一次见面,她对他印象并不算太好。他们约在公园门口见,互相打过招呼以后就陷入了沉默...
    茵小茵阅读 815评论 10 9