06.画像送信と保存の実装 - 画像変換サービス開発4

# 目次

* 【実装】データモデルの実装
* 【実装】クライアントからの画像の送信
* 【実装】画像ファイルのファイルシステムへの保存
* 【実装】O/Rマッパと DB マイグレーションツールの導入
* 【実装】画像ファイルのプロパティへデータストアを保存

# 【実装】データモデルの実装

前回で、HTML のクライアントと認証を実装した。
ここからは、実際に画像ファイルを Web アプリケーションで受け取り、保存するまでを実装していく。

それでは、データモデルを実装していく。

今回実装するのは、

* `PictureId`という値クラス
* `TwitterId`という値クラス
* `PictureProperty`というエンティティ

という3つのクラス。
値クラスとエンティティは、ドメイン駆動設計のテクニックの1つ。
値クラスは、値全体で一意であることを判断するクラス。
エンティティは、そこに割り当てられる ID で一意であることを判断するクラス。
例えば、電話番号は値で一意であることがわかるため値クラスと考え、ユーザー ID をエンティティと考える。
というのも、電話番号などのユーザー情報は変わったり被ったりすることがあるので一意に絞れないが、ユーザー ID はユーザーを一意に絞れるようになるから。

また、値クラスとするかエンティティとするかは決めの問題であり、実施の業務領域によっては扱いが変わる場合もある。
そのサービスで扱いやすいのはどちらなのかを議論して設計時に決めるべき項目となる。

既に、データモデルの SQL として、

```sql
create table picture_properties(
  picture_id bigint(20) not null auto_increment,
  status varchar(255) not null,
  twitter_id bigint(20) not null,
  file_name varchar(255) not null,
  content_type varchar(255) not null,
  overlay_text varchar(255) not null,
  overlay_text_size int(4) not null,
  original_filepath varchar(255) not null,
  converted_filepath varchar(255),
  created_time datetime not null,
  primary key (picture_id),
  index(twitter_id),
  index(created_time)
) engine=innodb charset=utf8mb4;
```

以上の MySQL の DDL を作っているので、それに忠実に実装していく。

まずは、`domain.entitiy`というパッケージを app フォルダ以下に作成し、この3つのクラスを実装していく。

```scala:app/domain/entity/PictureId.scala
package domain.entity

/**
  * 画像ID
  * @param value 画像IDの値
  */
case class PictureId(value: Long)
```

```scala:app/domain/entity/TwitterId.scala
package domain.entity

/**
  * 投稿者のTwitter ID
  *
  * @param value Twitter IDの値
  */
case class TwitterId(value: Long)
```

```scala:app/domain/entity/PictureProperty.scala
package domain.entity

import java.time.LocalDateTime
import com.google.common.net.MediaType

/**
  * 画像のプロパティ
  * @param id 画像ID
  * @param value 画像のプロパティの値
  */
case class PictureProperty(id: PictureId, value: PictureProperty.Value)

object PictureProperty {
  /**
    * 画像のステータス
    * @param value 画像のステータスの値
    */
  sealed abstract class Status(val value: String)

  object Status {
    // 変換成功
    case object Success extends Status("success")
    // 変換失敗
    case object Failure extends Status("failure")
    // 変換中
    case object Converting extends Status("converting")

    /**
      * 画像のステータスの値から画像のステータスに変換する
      * @param value 画像のステータスの値
      * @return 画像のステータス
      */
    def parse(value: String): Option[Status] =
      value match {
        case Success.value => Some(Success)
        case Failure.value => Some(Failure)
        case Converting.value => Some(Converting)
        case _ => None
      }

  }

  /**
    * 画像のプロパティの値
    * @param status 画像のステータス
    * @param twitterId 投稿者のTwitter ID
    * @param fileName 投稿された画像のファイル名
    * @param contentType 投稿された画像のContent-Type
    * @param overlayText 変換用テキスト
    * @param overlayTextSize 変換用テキストのサイズ
    * @param originalFilepath 変換前のファイルのパス
    * @param convertedFilepath 変換後のファイルのパス
    * @param createdTime 投稿された時間
    */
  case class Value(
                    status: Status,
                    twitterId: TwitterId,
                    fileName: String,
                    contentType: MediaType,
                    overlayText: String,
                    overlayTextSize: Int,
                    originalFilepath: Option[String],
                    convertedFilepath: Option[String],
                    createdTime: LocalDateTime
                  )

}
```

PictureProperty では、具体的にその状態を表す Status や、ID 以外の値である PictureProperty.Value 　も定義してある。
PictureProperty.Value　は内部的な値クラス。
このように入れ子になったエンティティを構築したりするのに、object を利用できる。
以上のデータモデルが無事実装でき、`sbt compile`でエラーなくコンパイルできれば、データモデルの構築は完了。

# 【実装】クライアントからの画像の送信

次は、クライアントから画像を送信するようにする。
この実装では、[Dropzone.js](https://www.dropzonejs.com/)のサンプル実装をそのまま組み込む。
既に JavaScript に関しては Webpack で作成した bundle.js で読み込まれているので、[使い方](https://www.dropzonejs.com/#usage)の通り、form  要素を組み込むだけで問題ない。
基本 POST を利用して送信されるため、form にタイプが hidden の input 要素その他のプロパティ要素を追加しておき、送信時に JavaScript で hidden の　input 要素を更新するように実装しよう。

`app/views/index.scala.html`を以下のように編集する。

```html
        </div>
        <div class="row center">
            <div class="col s12 blue z-depth-5">
                <form action="/pictures" class="dropzone" id="filedropzone">
                    <h2 class="blue-text text-lighten-5"> ドロップエリア </h2>
                    <input type="hidden" id="overlaytext" name="overlaytext">
                    <input type="hidden" id="overlaytextsize" name="overlaytextsize">
                </form>
            </div>
        </div>
        <br><br>
``` 

今回は、`/pictures`に POST の形式で送られるフォームとする。
オーバーレイを行うテロップのテキストと、そのテキストサイズは hidden のタイプの input 要素として追加した。
また [Dropzon.jsの設定](https://www.dropzonejs.com/#configuration)方法を見ながら、JavaScript で設定を行うように JavaScript を実装する。

`app/views/index.js`を以下のように実装する。

```js
import React from 'react';
import ReactDOM from 'react-dom';

// Dropzoneのドラッグ＆ドロップエリアエリアの設定
Dropzone.options.filedropzone = {
  paramName: 'file', // The name that will be used to transfer the file
  maxFilesize: 2, // MB
  dictDefaultMessage: '',
  thumbnailHeight: 200,
  thumbnailWidth: 200,
  init: function () {
    this.on('addedfile', function (file) {
      $('#overlaytext').val($('#overlaytext-shown').val());
      $('#overlaytextsize').val($('#overlaytextsize-shown').val());
    });
  },
  accept: function (file, done) {
    done();
    $('.dz-details').remove();
    $('.dz-progress').remove();
    $('.dz-error-message').remove();
    $('.dz-success-mark').remove();
    $('.dz-error-mark').remove();
  }
};

// Materializedの設定
$(function () {
  // Materialized Menu
```

* `file`というパラメータ名で POST する実装
* 画像の最大サイズを 2MB としたり、表示されるサムネイルのサイズを 200 x 200 としたりする設定

をした後、

* 不要なテキストを削除する設定
* 送信時に input 要素の val 属性を代入するなどの実装

がしてある。

```
node_modules/.bin/webpack --config conf/webpack.config.js
```

以上を実装してコンパイルした後、`sbt run`でアプリケーションを起動しよう。
そしてログインした後、実際に画像のドラッグ&ドロップを行って、Chrome のデベロッパーツールで確認してみよう。

ここでは Web API は実装していないので、`403 Forbidden`のレスポンスが返るはず。
AJAX のリクエストにおいて正しく画像が送信できていることを、Form Data の中身を見て確認しておこう。
(Network　タブ -> XHR　タブ ->　Name: pictures -> Headers タブ -> Form Data)

```
------WebKitFormBoundarybXXlQb0KmApua3Kg
Content-Disposition: form-data; name="overlaytext"


------WebKitFormBoundarybXXlQb0KmApua3Kg
Content-Disposition: form-data; name="overlaytextsize"


------WebKitFormBoundarybXXlQb0KmApua3Kg
Content-Disposition: form-data; name="file"; filename="HappyFace.jpg"
Content-Type: image/jpeg


------WebKitFormBoundarybXXlQb0KmApua3Kg--
```

以上のような情報が送信できていれば問題ない。
この中身を Play Framework で利用する部分はこれから実装していく。

# 【実装】画像ファイルのファイルシステムへの保存

次に、AJAX で情報を受け取る Web API の`/pictures`の POST メソッドを実装していく。

`controllers`パッケージに、`PictureController`クラスを作成し、以下のように実装していく。

```scala:app/controllers/PicturesController.scala
package controllers

import java.nio.file.{FileSystems, Files, Path, StandardCopyOption}
import java.time.{Clock, LocalDateTime}
import javax.inject.{Inject, Singleton}

import com.google.common.net.MediaType
import domain.entity.{PictureProperty, TwitterId}
import play.api.cache.SyncCacheApi
import play.api.libs.Files.TemporaryFile
import play.api.mvc._
import play.api.mvc.MultipartFormData.FilePart

import scala.concurrent.{ExecutionContext, Future}

@Singleton
class PicturesController @Inject()(
                                    cc: ControllerComponents,
                                    clock: Clock,
                                    executionContext: ExecutionContext,
                                    val cache: SyncCacheApi,
                                  ) extends TwitterLoginController(cc) {

  implicit val ec = executionContext
  val originalStoreDirPath = "./filesystem/original"

  def post = TwitterLoginAction.async { request =>
    (request.accessToken, request.body.asMultipartFormData) match {
      case (Some(accessToken), Some(form)) =>
        form.file("file") match {
          case Some(file) =>
            val storeDirPath = FileSystems.getDefault.getPath(originalStoreDirPath)
            if (!Files.exists(storeDirPath)) Files.createDirectories(storeDirPath)

            val originalFilepath =  FileSystems.getDefault.getPath(storeDirPath.toString, System.currentTimeMillis().toString)
            Files.copy(file.ref.path, originalFilepath, StandardCopyOption.COPY_ATTRIBUTES)
            val propertyValue = createPicturePropertyValue(TwitterId(accessToken.getUserId), file, form, originalFilepath)

            println(propertyValue)
            // TODO MySQLへのプロパティ保存とRedisへのタスクの保存

            Future.successful(Ok("Picture uploaded."))
          case _ => Future.successful(Unauthorized("Need picture data."))
        }
      case _ => Future.successful(Unauthorized("Need to login by Twitter and picture data."))
    }
  }

  private[this] def createPicturePropertyValue(
                                                twitterId: TwitterId,
                                                file: FilePart[TemporaryFile],
                                                form: MultipartFormData[TemporaryFile],
                                                originalFilePath: Path
                                              ): PictureProperty.Value = {
    val overlayText = form.dataParts.get("overlaytext").flatMap(_.headOption).getOrElse("")
    val overlayTextSize = form.dataParts.get("overlaytextsize").flatMap(_.headOption).getOrElse("60").toInt
    val contentType = MediaType.parse(file.contentType.getOrElse("application/octet-stream"))

    PictureProperty.Value(
      PictureProperty.Status.Converting,
      twitterId,
      file.filename,
      contentType,
      overlayText,
      overlayTextSize,
      Some(originalFilePath.toString),
      None,
      LocalDateTime.now(clock))
  }

}
```

Play Framework では、Controller のインスタンスの利用にも内部的に DI が利用されている。
DI で今後利用するインスタンスをもしシングルトンとして利用したい場合には、`@Singleton`を型の前につけることで、DI での利用時にシングルトンとして利用される。
PicturesController はシングルトンとして利用したいので、ここでは`@Singleton`をつける。

このタイミングで、OAuthController にも`@Singleton`アノテーションをつけておこう。

```scala:app/controllers/OAuthController.scala

package controllers

import javax.inject.{Inject, Singleton}

import infrastructure.twitter.{TwitterAuthenticator, TwitterException}
import play.api.Configuration
import play.api.cache.SyncCacheApi
import play.api.mvc.ControllerComponents

import scala.concurrent.duration._

@Singleton
class OAuthController @Inject()(
                                 cc: ControllerComponents,
                                 twitterAuthenticator: TwitterAuthenticator
```

なお実装した PicturesController の処理では、リクエストから情報を受け取り、実行フォルダ以下の`./filesystem/original`を作成した後、ファイルを UNIX タイムスタンプ名で保存する。

また、POST されてきた情報を元に、PictureProperty.Value のインスタンスを作成するというところまで実装する。
ここでは`TwitterLoginAction.async`という機能や Action の async メソッドを利用することで、[Async action](https://www.playframework.com/documentation/2.6.x/Migration22#async-actions)を利用する。
詳しい利用方法は、[Async actions のドキュメント](https://www.playframework.com/documentation/2.6.x/Migration22#async-actions)を参照すること。

また今回、[Clock](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/time/Clock.html)という現在のタイムゾーンを取得するためのインスタンスも DI によって取得する。
ただしこのインスタンスは、Play Framework の DI の設定において自動的に取得されるものではない。
そのため Module クラスを設定してインスタンスを肩に割り当ててやる必要がある。
`app`ディレクトリに`Module`クラスを作り、以下のように実装しよう。

```scala:app/Module.scala
import java.time.Clock

import com.google.inject.AbstractModule
import play.api.{Configuration, Environment}

class Module(environment: Environment,
             configuration: Configuration) extends AbstractModule {

  override def configure() = {
    bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
  }
}
```

以上のように、Play Framework の DI で設定されていない依存性注入に関しては、このように AbstractModule クラスを継承した、Module クラスを別途用意してやる必要がある。
ここでは、Clock の型が要求された時は、`Clock.systemDefaultZone`よりインスタンスを渡すように実装する。

なおこのように、外部からこのような環境の情報を注入してやることで、クラス自体のテストがやりやすくなるというメリットもある。
また大規模開発では、必要な部品のインスタンスを取得するだけでも難しいという場合があり、このように必要な部品のインスタンスを DI で取得できるのは慣れてしまえば便利という側面がある。
このように型とインスタンスを紐づけることを、バインディングという。
これら DI について詳しくは、[Play Framework の設定を利用したバインディングのドキュメント](https://www.playframework.com/documentation/2.6.x/ScalaDependencyInjection#Configurable-bindings)を見てみると良い。

そして、PicturesController の post メソッドと Module が実装された後は、`conf/routes`ファイルを以下のように記述する。

Play Framework ではデフォルトで CSRF のトークンのチェックが行われる。
しかしここでは、AJAX からのアクセスしか受け入れないため、CSRF のトークンをオフにする設定`+ nocsrf`と共に記述する。

```
GET     /logout                        controllers.OAuthController.logout
GET     /oauth_callback                controllers.OAuthController.oauthCallback(oauth_verifier: Option[String])

# REST API
+ nocsrf
POST    /pictures                      controllers.PicturesController.post

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)
```

以上で完成となる。
そして、`.gitignore`を以下のように編集しておく。

```
/.settings
/RUNNING_PID
node_modules
filesystem
```

これで画像ファイル自体が Git 管理から外れるので、安全に運用できる。

これでファイルの保存までは完成となる。
アプリケーションを起動しログインした後、画像を追加しよう。
そして、コンソールに`PictureProperty.Value`の内容が表示されることと、`filesystem/original`フォルダにファイルが保存されることを確認しよう。

？？？ コンソールに`PictureProperty.Value`の値が表示されず、エラーが出る。

# 【実装】O/Rマッパと DB マイグレーションツールの導入

ファイルの保存まで実装できれば、今度はそれを MySQL と Redis に保存していくだけ。

その前に必要なライブラリ群をインストールしておく。
まずは、MySQL と Redis を Scala で扱うためのライブラリを依存関係に加える。
MySQl を利用可能な O/R マッパとしては今回は[ScalikeJDBC](http://scalikejdbc.org/)を、Redis のクライアントとしては[scala-redis](https://github.com/debasishg/scala-redis)を導入する。

なお、O/R マッパには他に有名なもとして[Slick](http://slick.lightbend.com/)という Lightbend が開発している O/R マッパもある。
しかし、 ScalikeJDBC の方が SQL を利用したコードが書きやすいというメリットがある。
ここでは、コードのわかりやすさのために ScalikeJDBC を利用していく。

`build.sbt`　を以下のように編集する。

```
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test
libraryDependencies += ehcache
libraryDependencies += "javax.xml.bind" % "jaxb-api" % "2.3.1"
libraryDependencies += "org.twitter4j" % "twitter4j-core" % "4.0.6"
libraryDependencies += jdbc
libraryDependencies += evolutions
libraryDependencies += "org.scalikejdbc" %% "scalikejdbc"  % "3.0.2"
libraryDependencies += "org.scalikejdbc" %% "scalikejdbc-config" % "3.0.2"
libraryDependencies += "org.scalikejdbc" %% "scalikejdbc-play-dbapi-adapter" % "2.6.0-scalikejdbc-3.0"
libraryDependencies += "mysql" % "mysql-connector-java" % "5.1.36"
libraryDependencies += "net.debasishg" %% "redisclient" % "3.4"

// Adds additional packages into Twirl
//TwirlKeys.templateImports += "jp.ed.nnn.controllers._"
```

設定の方法は、ほぼ、 [ScalikeJDBC](http://scalikejdbc.org/) のドキュメントを参考にしたものとなる。
編集後、 Refresh project のリンクが出るので、再度プロジェクトを読み込み直そう。
そして、 Docker に接続できるよう、 MySQL の設定なども `conf/application.conf` に追記しておこう。

```
mojipic.documentrooturl="http://localhost:9000"
mojipic.consumerkey="LAdrAbR8HoDMK9ggZE3WBtJdr"
mojipic.consumersecret="WVOBr4JJ28uzIdTEiXAQnf2I9AjaSQTFsPVuLwSNXpmepK8dMf"

# JDBC settings
db.default.driver="com.mysql.jdbc.Driver"
db.default.url="jdbc:mysql://localhost:13306/mojipic"
db.default.username="root"
db.default.password="mysql"

# Connection Pool settings
db.default.poolInitialSize=10
db.default.poolMaxSize=20
db.default.connectionTimeoutMillis=1000

scalikejdbc.global.loggingSQLAndTime.enabled=true
scalikejdbc.global.loggingSQLAndTime.singleLineMode=false
scalikejdbc.global.loggingSQLAndTime.logLevel=debug
scalikejdbc.global.loggingSQLAndTime.warningEnabled=true
scalikejdbc.global.loggingSQLAndTime.warningThresholdMillis=5
scalikejdbc.global.loggingSQLAndTime.warningLogLevel=warn

# others are same as Play's defaults
play.modules.enabled += "scalikejdbc.PlayDBApiAdapterModule"

# Redis settings
mojipic.redis.host="localhost"
mojipic.redis.port=6379
```

以上も、それぞれ必要な情報を取得できるものとなる。
ここでは、データベース名を `mojipic` とした。

　なお、 Play Framework には [Evolutions](https://www.playframework.com/documentation/2.6.x/Evolutions#Running-evolutions-using-compile-time-DI) というデータベースのマイグレーションツールがある。
マイグレーションツールとは、 RDB のスキーマのバージョン管理とバージョンアップとダウンなどを管理してくれるツール。
なお、必要となる DDL は `conf/evolutions/default` 等フォルダを作成する。
そしてその中に `1.sql` という SQL を用意することで、自動的に作成を行ってくれるようになっている。
他の系列のデータベースを用意する場合には default 以外の名前にする。

ここでは、 `conf/evolutions/default/1.sql` を以下のように用意する。

```
conf/evolutions/default/1.sql

# picture_properties schema

# --- !Ups

create table picture_properties(
  picture_id bigint(20) not null auto_increment,
  status varchar(255) not null,
  twitter_id bigint(20) not null,
  file_name varchar(255) not null,
  content_type varchar(255) not null,
  overlay_text varchar(255) not null,
  overlay_text_size int(4) not null,
  original_filepath varchar(255) not null,
  converted_filepath varchar(255),
  created_time datetime not null,
  primary key (picture_id),
  index(twitter_id),
  index(created_time)
) engine=innodb charset=utf8mb4;

# --- !Downs

drop table picture_properties;
```

ここまで実装できたら、この Evolutions にテーブルを作成させてみよう。
Docker を立ち上げておき、 MySQL と Redis を立ち上げる。
Docker が利用できるコンソールより、

```
docker start mojipic-mysql
docker start mojipic-redis
```

以上の2つのコンテナを起動しておく。
既に、 `mojipic-mysql` と `mojipic-redis` という名前でコンテナを作成しているため、起動できれば OK 。
また、

```
docker exec -it mojipic-mysql mysql -u root -pmysql
```

で MySQL に接続し、

```
drop database mojipic;
create database mojipic;
```

を実行して、 `mojipic` データベースを作り直しておく。
そのまま

```
sbt run
```

で起動して、 http://localhost:9000/ にアクセスしてみよう。
すると、

```
Database 'default' needs evolution!
```

というエラーページが表示されるはず。

MySQL のコンソールより、

```:mysql_in_Docker
mysql> use mojipic;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed

mysql> show tables;
+--------------------+
| Tables_in_mojipic  |
+--------------------+
| picture_properties |
| play_evolutions    |
+--------------------+
2 rows in set (0.00 sec)
```

Evolutions と ScalikeJDBC を利用して作られたテーブルの様子を確認できる。
Evolutions はデータベースの中に DB のスキーマに関するバージョン情報を持つことにより、データベース定義のバージョン管理などを行う。

これで、 ScalikeJDBC と Evolutions が導入できた。

# 【実装】画像ファイルのプロパティへデータストアを保存

次に、具体的に MySQL や Redis にデータを保存していくところを実装していく。
手順としては、

1. MySQL への保存を担当する PicturePropertyRepository のインタフェースを作る
1. PicturePropertyRepository の実装の PicturePropertyRepositoryImpl を実装する
1. DI でバインディングして PicturePropertyRepository のインスタンスを取得できるようにする
1. 保存時に取得した PictureId を Redis にタスクとして保存できるようにする

という流れになる。

まずは、`domain.repository` パッケージを作成し、`domain.repository/PicturePropertyRepository` トレイトを実装する。

今回はデータを保存する部分の実装だけなので、以下のようになる。

```scala:app/domain/repository/PicturePropertyRepository.scala
package domain.repository


import domain.entity.{PictureId, PictureProperty}

import scala.concurrent.Future

trait PicturePropertyRepository {
  /**
    * 画像のプロパティを保存する
    *
    * @param value 画像のプロパティの値
    * @return Future.successful(PictureId)     新しく割り当てられた画像ID
    */
  def create(value: PictureProperty.Value): Future[PictureId]

}
```

非常にシンプル。
次に、`infrastracture.repository` パッケージを作成して、`infrastructure.repository/PicturePropertyRepository` クラスを実装する。

```scala:app/infrastructure/repository/PicturePropertyRepositoryImpl.scala
package infrastructure.repository

import domain.entity.{PictureId, PictureProperty}
import domain.repository.PicturePropertyRepository
import scalikejdbc._

import scala.concurrent.Future
import scala.util.Try

class PicturePropertyRepositoryImpl extends PicturePropertyRepository {

  def create(value: PictureProperty.Value): Future[PictureId] =
    Future.fromTry(Try {
      using(DB(ConnectionPool.borrow())) { db =>
        db.localTx { implicit session =>
          val sql =
            sql"""INSERT INTO picture_properties (
                 | status,
                 | twitter_id,
                 | file_name,
                 | content_type,
                 | overlay_text,
                 | overlay_text_size,
                 | original_filepath,
                 | converted_filepath,
                 | created_time
                 | ) VALUES (
                 | ${value.status.value},
                 | ${value.twitterId.value},
                 | ${value.fileName},
                 | ${value.contentType.toString},
                 | ${value.overlayText},
                 | ${value.overlayTextSize},
                 | ${value.originalFilepath.getOrElse(null)},
                 | ${value.convertedFilepath.getOrElse(null)},
                 | ${value.createdTime}
                 | )
              """.stripMargin
          PictureId(sql.updateAndReturnGeneratedKey().apply())
        }
      }
    })
}
```

ScalikeJDBC では、ほぼそのまま SQL 文を流用して実装できる。
ここでは、ConnectionPool という MySQL への接続をプーリングしているオブジェクトからコネクションを借りて、トランザクションを作って DB に挿入し、最終的に PictureId を Future として取得していく。

次に、これらのインスタンスを他のインスタンスから利用できるように Module クラスでバインディングを定義しよう。
`Module` を以下のように更新する。

```scala:app/Module.scala
import java.time.Clock

import com.google.inject.AbstractModule
import com.redis.RedisClient
import domain.repository.PicturePropertyRepository
import infrastructure.repository.PicturePropertyRepositoryImpl
import play.api.{Configuration, Environment}

class Module(environment: Environment,
             configuration: Configuration) extends AbstractModule {

  val redisHost = configuration.get[String]("mojipic.redis.host")
  val redisPort = configuration.get[Int]("mojipic.redis.port")

  def configure() = {
    bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
    bind(classOf[PicturePropertyRepository]).to(classOf[PicturePropertyRepositoryImpl])
    bind(classOf[RedisClient]).toInstance(new RedisClient(redisHost, redisPort))
  }
}
```

RedisClient も利用することが分かっているため、ここで DI のバインディングとして定義しておく。
この定義によって @Inject アノテーションがコンストラクタについたクラスで、

* `PicturePropertyRepository` が呼ばれれば、`PicturePropertyRepository` のインスタンス
* `RedisClient` が呼ばれれば、 new された `RedisClient` のインスタンス

が渡されるようになる。
では PicturePropertyRepository と RedisClient を利用して、PicturesController を以下のように修正して MySQL への保存と、Redis へのタスクの保存を行おう。

Redis へのキーを保存するためのクラスもこのタイミングで作っておく。
`infrasctucture.redis` パッケージを作成し、`RedisKeys` オブジェクトを以下のように作る。

```scala:app/infrastructure/redis/RedisKeys.scala
package infrastructure.redis

object RedisKeys {
  val Tasks = "mojipic:tasks"
}
```

このキー `mojipic:tasks` で、変換予定の PictureId を保存する。
そして、`app/controllers/PicturesController.scala` を以下のように実装する。

```scala:app/controllers/PicturesController.scala
...
import javax.inject.Inject

import com.google.common.net.MediaType
import com.redis.RedisClient
import domain.entity.{PictureProperty, TwitterId}
import domain.repository.PicturePropertyRepository
import infrastructure.redis.RedisKeys
import play.api.cache.SyncCacheApi
import play.api.libs.Files.TemporaryFile
import play.api.mvc._
...
...
                                    clock: Clock,
                                    executionContext: ExecutionContext,
                                    val cache: SyncCacheApi,
                                    picturePropertyRepository: PicturePropertyRepository,
                                    redisClient: RedisClient
                                  ) extends TwitterLoginController(cc) {

  implicit val ec = executionContext
...
...
            val originalFilepath =  FileSystems.getDefault.getPath(storeDirPath.toString, System.currentTimeMillis().toString)
            Files.copy(file.ref.path, originalFilepath, StandardCopyOption.COPY_ATTRIBUTES)
            val propertyValue = createPicturePropertyValue(TwitterId(accessToken.getUserId), file, form, originalFilepath)
            val pictureId = picturePropertyRepository.create(propertyValue)
            pictureId.map({ (id) =>
              redisClient.rpush(RedisKeys.Tasks, id.value)
              Ok("Picture uploaded.")
            })
            Future.successful(Ok("Picture uploaded."))
          case _ => Future.successful(Unauthorized("Need picture data."))
        }
...
```

これで、MySQL への保存と、Redis へのタスクのキューイングは完了。
`sbt run` で起動後、ログインして、画像ファイルをアップロードしてみよう。
また MySQL にて、

```:Terminal
$ docker exec -it mojipic-mysql mysql -u root -pmysql
```

で MySQL のコンソールを開き、

```:MySQL_on_Docker
mysql> use mojipic;
Database changed

mysql> select * from picture_properties;
+------------+------------+------------+-----------+--------------+--------------+-------------------+-------------------------------------+--------------------+---------------------+
| picture_id | status     | twitter_id | file_name | content_type | overlay_text | overlay_text_size | original_filepath                   | converted_filepath | created_time        |
+------------+------------+------------+-----------+--------------+--------------+-------------------+-------------------------------------+--------------------+---------------------+
|          1 | converting |   97052502 | a.jpg     | image/jpeg   | LGTM         |                60 | ./filesystem/original/1626958287465 | NULL               | 2021-07-22 21:51:28 |
+------------+------------+------------+-----------+--------------+--------------+-------------------+-------------------------------------+--------------------+---------------------+
1 row in set (0.01 sec)
```

のように表示され、Redis でも、

```:Terminal
$ docker exec -it mojipic-redis redis-cli -h localhost -p 6379
```

で redis-cli を起動後、

```:redis-cli_on_Docker
localhost:6379> LRANGE mojipic:tasks 0 0
1) "1"
```

のように、画像の変換を行、PictureId が数値として表示されれば成功。
ここまでで、画像ファイルのプロパティへデータストアを保存する実装は完了。

# まとめ

* ドメイン駆動設計ではデータモデルを値クラスとエンティティという2つを表現する手法がある
* Play Framework で DI のバインディングの設定を独自に行う際には Module クラスを利用する
* データベースのスキーマをバージョン管理するために、マイグレーションツールを利用する