VaporとMySQLでToDoAPIを作ってみた!

More than 1 year has passed since last update.


はじめに

こんにちは。こんばんは。スマートテック・ベンチャーズ 10日目STVオタク担当の @nnsnodnb です。

今後サーバサイドも全部Swiftできるようになるために生まれてきたのではないかと思われている(少なくとも自分の中では)

サーバサイドSwiftについてまた触ってみました。Vaporで行こうと思います。

Kitura+MySQL が上手くできなかったいい感じにできた人いらしたら教えてくださいエロい人...


環境


  • macOS Sierra 10.12.1 (個人所有のMacが修理中のため知人のMacを借りてます)

  • Xcode 8.1

  • Swift 3.0.1

  • mysql Ver 14.14 Distrib 5.7.16, for osx10.12 (x86_64) using EditLine wrapper


準備


MySQL準備

Homebrewでインストールを進めます。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

$ brew install mysql
$ sudo mysql.server start

あとはDB作成等


Vapor準備

$ curl -sL check.vapor.sh | bash

✅ Compatible
$ curl -sL toolbox.vapor.sh | bash
✅ Compatible
Downloading...
Compiling...
Installing...
Vapor Toolbox v1.0.3 Installed
Use vapor --help and vapor <command> --help to learn more.


プロジェクトファイル準備

$ vapor new vaporTodoApi

Cloning Template [Done]

スクリーンショット 0028-12-04 午前3.55.04.png

$ tree .

.
├── Config
│   ├── app.json
│   ├── clients.json
│   ├── crypto.json
│   ├── droplet.json
│   ├── production
│   │   └── app.json
│   └── servers.json
├── Localization
│   ├── default.json
│   ├── en-US.json
│   ├── es-US.json
│   ├── nl-BE.json
│   └── nl-NL.json
├── Package.swift
├── Procfile
├── Public
│   ├── images
│   │   └── vapor-logo.png
│   └── styles
│   └── app.css
├── README.md
├── Resources
│   └── Views
│   ├── base.leaf
│   └── welcome.leaf
├── Sources
│   └── App
│   ├── Controllers
│   │   └── PostController.swift
│   ├── Models
│   │   └── Post.swift
│   └── main.swift
├── app.json
└── license

Vaporコマンドで実行するとここまで自動的にしてくれるのはいいですね!

ちなみにXcodeで作業したいときはnewしたあとに以下のようにやればできます。

$ vapor xcode -n

$ vapor xcode mysql -y

自動的にXcodeが開くはずです。


MySQL接続設定

Config/mysql.json を作成して情報を付加する


mysql.json

{

"host": "127.0.0.1",
"user": "root",
"password": "",
"database": "todo",
"encoding": "utf8"
}

DATABASEは todo という名前で新規作成しておいてください。

ユーザ等は各自適当に変更お願いします:muscle:


Swift Package Manager設定


Package.swift

import PackageDescription

let package = Package(
name: "vaporTodoApi",
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 1),
.Package(url: "https://github.com/vapor/mysql-provider.git", majorVersion: 1, minor: 1) // 追加
],
exclude: [
"Config",
"Database",
"Localization",
"Public",
"Resources",
"Tests",
]
)


$ vapor build --mysql

Fetching Dependencies [Done]
Building Project [Done]

Xcodeで作業されている方は基本的に Build & Run で大丈夫かと思います。

もしダメだったら上のコマンドを叩いてください:bow:


サンプルソースコード


Sources/App/main.swift

import Vapor

import VaporMySQL

let drop = Droplet(
preparations: [Task.self],
providers: [VaporMySQL.Provider.self]
)

drop.resource("tasks", TaskController())

drop.run()


もともとあるかと思われる Post.swift を変更するなりなんなり


Sources/App/Model/Task.swift

import Vapor

import Fluent
import Foundation

final class Task: Model {
var id: Node?
var title: String
var rank: String

init(title: String, rank: String) {
self.title = title
self.rank = rank
}

init(node: Node, in context: Context) throws {
id = try node.extract("id")
title = try node.extract("title")
rank = try node.extract("rank")
}

func makeNode(context: Context) throws -> Node {
return try Node(node: [
"id": id,
"title": title,
"rank": rank
])
}
}

extension Task {
public convenience init?(from title: String, rank: String) throws {
self.init(title: title, rank: rank)
}
}

extension Task: Preparation {
static func prepare(_ database: Database) throws {
try database.create("tasks") { tasks in
tasks.id()
tasks.string("title")
tasks.string("rank")
}
}

static func revert(_ database: Database) throws {
try database.delete("tasks")
}
}


こちらも、もともとあるかと思われる PostViewController.swift を変更するなりなんなり


Sources/App/Model/TaskViewController.swift

import Vapor

import HTTP

final class TaskController: ResourceRepresentable {
func index(request: Request) throws -> ResponseRepresentable {
return try Task.all().makeNode().converted(to: JSON.self)
}

func create(request: Request) throws -> ResponseRepresentable {
var task = try request.post()
try task.save()
return task
}

func show(request: Request, task: Task) throws -> ResponseRepresentable {
return task
}

func delete(request: Request, task: Task) throws -> ResponseRepresentable {
try task.delete()
return JSON([:])
}

func clear(request: Request) throws -> ResponseRepresentable {
try Task.query().delete()
return JSON([])
}

func update(request: Request, item task: Task) throws -> ResponseRepresentable {
let new = try request.post()
var task = task
task.title = new.title
task.rank = new.rank
try task.save()
return task
}

func replace(request: Request, task: Task) throws -> ResponseRepresentable {
try task.delete()
return try create(request: request)
}

func makeResource() -> Resource<Task> {
return Resource(
index: index,
store: create,
show: show,
replace: replace,
modify: update,
destroy: delete,
clear: clear
)
}
}

extension Request {
func post() throws -> Task {
guard let json = json else {
throw Abort.badRequest
}
return try Task(node: json)
}
}


今回はDBのテーブル構成は id , title , rank というフラグのない謎構成です。


ソースコードが書けたらとりあえず実行してみよか?

$ vapor build --mysql

$ vapor run

Xcodeの方はそのまま Build & Run で行けるかと思います。

Runが通れば自動的にDBにテーブルを作成してくれます。


404ページ

スクリーンショット 2016-12-07 午前1.19.38.png

404ページは始めからパッケージに同梱されているらしいです!感謝:pray:


HTTPメソッドアクセスについて


GET

GETリクエストについては index()show() が呼ばれます。


index()

func index(request: Request) throws -> ResponseRepresentable {

return try Task.all().makeNode().converted(to: JSON.self)
}

見ての通り全てを取り出してJSON形式にしてレスポンスしてくれます。

スクリーンショット 2016-12-07 午前1.24.44.png

$ curl http://127.0.0.1:8080/tasks


show()

func show(request: Request, task: Task) throws -> ResponseRepresentable {

return task
}

詳細情報取得みたいなやつです。

スクリーンショット 2016-12-07 午前1.27.19.png

$ curl http://127.0.0.1:8080/tasks/id

id のところはデータのIDです。


POST

POSTリクエストは create() が呼ばれます。

func create(request: Request) throws -> ResponseRepresentable {

var task = try request.post()
try task.save()
return task
}

POSTで受け取ったデータをインサートしてるだけですね。Realmとか使ってる人なら見覚えがあるかと...あとはNoSQL使いの皆様とか

スクリーンショット 2016-12-07 午前1.32.52.png

$ curl http://127.0.0.1:8080/tasks -X POST -d '{"title": "hoge", "rank": "中"}' -H "Content-Type: application/json" -H "Accept: application/json"


PUT

PUTリクエストは update() が呼ばれます。

func update(request: Request, item task: Task) throws -> ResponseRepresentable {

let new = try request.post()
var task = task
task.title = new.title
task.rank = new.rank
try task.save()
return task
}

これについてはちょっと仕様がわからないんですが、古いデータを削除して新しくインサートしているような動作をしているようです。

もしかしたら私のやり方が間違っているのかもしれませんが...

スクリーンショット 2016-12-07 午前1.38.29.png

$ curl http://127.0.0.1:8080/tasks/id -X PUT -d '{"title": "あっぷでーと", "rank": "低"}' -H "Content-Type: application/json" -H "Accept: application/json"


DELETE

DELETEリクエストは delete() が呼ばれます。

func delete(request: Request, task: Task) throws -> ResponseRepresentable {

try task.delete()
return JSON([:])
}

Vaporの中身をみていないのでなんともわかりませんが、 id でfilterして削除しているのではないかと思います。

スクリーンショット 2016-12-07 午前1.40.57.png

$ curl http://127.0.0.1:8080/tasks/id -X DELETE


Clearについて

clear() なんですが、呼び方がわからないのでとりあえず放置しますが、(知ってる人教えてください)

呼ぶと全てのキーを削除してしまうので下みたいにならないようにしましょうねw

syazai_kaiken.png


補足

func makeResource() -> Resource<Task> {

return Resource(
index: index,
store: create,
show: show,
replace: replace,
modify: update,
destroy: delete,
clear: clear
)
}

makeResource() に書かれているようなキーの右側を任意のメソッド名に変更してあげると思い通りにしてくれます。


最後に

ざっと、デフォルトでつぎ込まれている機能のみ紹介しましたが、 filter なども実装されているとのことなのでGETパラメータでほげほげできそうですね。

https://vapor.github.io/documentation/fluent/query.html

また、今回使用したソースコードはGitHubに公開しています。

よかったら見てみてください!

https://github.com/nnsnodnb/vapor-todo-api


明日の11日目は、また私の担当になります。

モバイルバックエンドについての記事になります。主にサーバ周りの話をしていきます。

インフラもちょっと話したいです。お兄さんたちに怒られない程度に...