前回の記事 VaporからDockerで立てたMySQL8に接続する では Vapor から Docker に立てた MySQL に接続するところまで実装しました。
今回の記事では、接続したデータベースから値を取り出し、テンプレートエンジンである Leaf で使っていこうと思います。
リポジトリはこちら O-Junpei/sommelier-vapor
まず Docker で立てた MySQL にテーブルを作成し、検証用のデータを追加します。
IT企業の技術ブログをまとめを作りたいので、企業ブログの記事の情報が入る article
テーブルを作成します。
create table article(
article_id int(11) not null auto_increment,
title varchar(255),
article_url varchar(255),
site_name varchar(255),
site_url varchar(255),
primary key (article_id)
);
サンプルデータとして、以下の6記事を追加します。
SET CHARACTER_SET_CLIENT = utf8mb4;
SET CHARACTER_SET_CONNECTION = utf8mb4;
insert into article (article_id, title, article_url, site_name, site_url)
values (1, 'Amazon Elasticsearch ServiceをつかったRDSのスロークエリの集計と監視', 'https://techlife.cookpad.com/entry/2019/12/27/000000', 'クックパッド開発者ブログ', 'https://techlife.cookpad.com');
insert into article (article_id, title, article_url, site_name, site_url)
values (2, 'プロと読み解くRuby 2.7 NEWS', 'https://techlife.cookpad.com/entry/2019/12/25/121834', 'クックパッド開発者ブログ', 'https://techlife.cookpad.com');
insert into article (article_id, title, article_url, site_name, site_url)
values (3, 'センサクッキング 技術検証とユーザー体験検証', 'https://techlife.cookpad.com/entry/2019/12/18/110000', 'クックパッド開発者ブログ', 'https://techlife.cookpad.com');
insert into article (article_id, title, article_url, site_name, site_url)
values (4, '[小ネタ]WorkSpacesのディレクトリが消せないし理由も分からない?そんな時はAWS Directory Serviceを見てみよう', 'https://dev.classmethod.jp/cloud/aws/how-to-delete-ad-for-workspaces-and-find-the-cause/', 'クラスメソッド発「やってみた」系技術メディア | Developers.IO', 'https://dev.classmethod.jp');
insert into article (article_id, title, article_url, site_name, site_url)
values (5, '[レポート] 可観測性は AI ・メトリクス・ログの幸せな結婚を夢見るか? AIOps の雄、Moogsoft の CEO が語る #AIM310 #reinvent', 'https://dev.classmethod.jp/cloud/aws/201912-report-reinvent-2019-aim310/', 'クラスメソッド発「やってみた」系技術メディア | Developers.IO', 'https://dev.classmethod.jp');
insert into article (article_id, title, article_url, site_name, site_url)
values (6, '[レポート] Container Insight, FireLens, AppMesh を使ってコンテナ環境 (ECS/EKS/Fargate) の可観測性を向上させる #CON328 #reinvent', 'https://dev.classmethod.jp/cloud/aws/201912-report-reinvent-2019-con328/', 'クラスメソッド発「やってみた」系技術メディア | Developers.IO', 'https://dev.classmethod.jp');
01_ddl.sql
と 02_data.sql
を initdb
ディレクトリに入れ、initdb
ディレクトリ は /docker-entrypoint-initdb.d
ディレクトリ としてマウントするようにしました。
こうすることで、MySQL コンテナ作成時に2つの .sql
ファイルがコンテナの新規作成時に実行されます。
services:
db:
image: mysql:8.0.18
container_name: sommelier-mysql
environment:
MYSQL_ROOT_PASSWORD: sommelier
MYSQL_DATABASE: sommelier_local
MYSQL_USER: sommelier
MYSQL_PASSWORD: sommelier
TZ: 'Asia/Tokyo'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
ports:
- 3306:3306
volumes:
- ./Database/initdb:/docker-entrypoint-initdb.d
参考: DockerHub MySQL Initializing a fresh instance
article
テーブルに対応する Article
クラスを作成します。
entity
プロパティにテーブル名を設定し、カラム名と同名のプロパティを作成します。
import FluentMySQL
import Vapor
struct Article: Decodable, MySQLModel {
var id: Int?
static let entity = "article" // テーブル名
var article_id: Int?
var title: String?
var article_url: String?
var site_name: String?
var site_url: String?
}
extension Article: Content { }
extension Article: Migration { }
CodingKeys
を使用することで、カラム名と異なるプロパティ名にすることもできます。
import FluentMySQL
import Vapor
struct Article: Decodable, MySQLModel {
var id: Int?
static let entity = "article" // テーブル名
var articleId: Int?
var title: String?
var articleUrl: String?
var site_name: String?
var siteUrl: String?
private enum CodingKeys: String, CodingKey {
case articleId = "article_id"
case title = "title"
case articleUrl = "article_url"
case site_name = "site_name"
case siteUrl = "site_url"
}
}
extension Article: Content { }
extension Article: Migration { }
参考:
Models - Vapor Docs
Option to convert to snake_case
作成した Article
クラスの設定を configure.swift
で行います。
/// Configure migrations
var migrations = MigrationConfig()
migrations.add(model: Article.self, database: .mysql)
services.register(migrations)
このこと関連する内容の記事を書いてくださった方がおりますので紹介します。
本番運用するアプリでモデルの自動マイグレーションを使ってはいけない
これで MySQL のデータを簡単に扱うことができます。
import Vapor
import FluentMySQL
final class ArticleController {
static func blogs(req: Request) throws -> Future<[Blog]> {
let cookpadArticles: Future<[Article]> = Article.query(on: req).filter(\.site_url == "https://techlife.cookpad.com") .all()
let classmethodArticles: Future<[Article]> = Article.query(on: req).filter(\.site_url == "https://dev.classmethod.jp") .all()
let blogs = map(cookpadArticles, classmethodArticles) { (cookpadArticles, classmethodArticles) -> ([Blog]) in
let cookpadBlog = Blog(name: "クックパッド開発者ブログ", url: "https://techlife.cookpad.com", articles: cookpadArticles)
let classmethodBlog = Blog(name: "クラスメソッド発「やってみた」系技術メディア | Developers.IO", url: "https://dev.classmethod.jp", articles: classmethodArticles)
return [cookpadBlog, classmethodBlog]
}
return blogs
}
}
import Vapor
public func routes(_ router: Router) throws {
// 2chmm page
router.get { req -> Future<View> in
return try req.view().render("2chmm", [
"blogs": ArticleController.blogs(req: req)
])
}
}
#set("title") { 技術ブログまとめ }
#set("body") {
<ul>
#for(blog in blogs) {
<li>
<a href="#(blog.url)"><h1>#(blog.name)</h1></a>
<ul>
#for(article in blog.articles) {
<li><a href="#(article.article_url)">#(article.title)</h1></a></li>
}
</ul>
</li>
}
</ul>
}
#embed("base")