VaporからDockerで立てたMySQL8に接続し、Leafで表示する

前回の記事 [VaporからDockerで立てたMySQL8に接続する](https://qiita.com/takoikatakotako/items/1700b6ddd97d8e4e6524) では Vapor から Docker に立てた MySQL に接続するところまで実装しました。
今回の記事では、接続したデータベースから値を取り出し、テンプレートエンジンである Leaf で使っていこうと思います。

リポジトリはこちら [O-Junpei/sommelier-vapor](https://github.com/O-Junpei/sommelier-vapor)

まず Docker で立てた MySQL にテーブルを作成し、検証用のデータを追加します。
IT企業の技術ブログをまとめを作りたいので、企業ブログの記事の情報が入る `article` テーブルを作成します。

```sql:01_ddl.sql
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記事を追加します。

```sql:02_data.sql
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 コンテナ作成時に２つの `.sql` ファイルがコンテナの新規作成時に実行されます。

```yml:docker-compose.yml
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](https://hub.docker.com/_/mysql)


`article` テーブルに対応する `Article` クラスを作成します。
`entity` プロパティにテーブル名を設定し、カラム名と同名のプロパティを作成します。

```swift:Article.swift
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` を使用することで、カラム名と異なるプロパティ名にすることもできます。

```swift:Article.swift
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](https://docs.vapor.codes/3.0/fluent/models/)
[Option to convert to snake_case ](https://github.com/vapor/fluent/issues/454)



作成した `Article`クラスの設定を `configure.swift` で行います。

```swift:configure.swift
/// Configure migrations
var migrations = MigrationConfig()
migrations.add(model: Article.self, database: .mysql)
services.register(migrations)
```

このこと関連する内容の記事を書いてくださった方がおりますので紹介します。
[本番運用するアプリでモデルの自動マイグレーションを使ってはいけない](https://qiita.com/5t111111/items/572196e38df88646c847)　　

これで MySQL のデータを簡単に扱うことができます。

![file.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/bbadca03-87a4-bf75-ca23-cec560b7cc6a.png)


```swift:ArticleController.swift
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
    }
}
```

```swift:routes.swift
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)
        ])
    }
}
```

```html:2chmm.leaf
#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")

```