やっとデータベースへの接続を
- ファイルの読み書きでやっていたデータストアをデータベースにする
- MySQLデータベースへの接続
- ORMとしてSlickを使う
sealed trait
のマッピング- select, update の実装
MySQLデータベースへの接続
事前にテーブル作成、初期データ挿入
create table todo( id bigint(20) NOT NULL AUTO_INCREMENT, status varchar(31) NOT NULL, title varchar(255) NOT NULL, PRIMARY KEY (ID)) AUTO_INCREMENT=10000; insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の要件定義'); insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の設計'); insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の実装');
10000始まりのauto increment にしたい場合はcreate table の末尾にAUTO_INCREMENT=10000
をつけます。
また、insertの際はidをnull
で入れることでよしなにインクリメントしてくれます。
依存ライブラリの定義
build.sbtにjdbc, slick, mysql-connectorの依存関係を追加します。
libraryDependencies += jdbc libraryDependencies += "com.typesafe.slick" %% "slick" % "3.2.1" libraryDependencies += "mysql" % "mysql-connector-java" % "6.0.6"
データベースの設定書き込み
conf/application.conf に接続先データベースを定義
todo-db = { url = "jdbc:mysql://localhost/todo_play?characterEncoding=UTF8&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC" driver = com.mysql.jdbc.Driver user = "user" password = "password" connectionPool = disabled }
url のパラメータには呪文が書かれていますが、、、
characterEncoding=UTF8&useUnicode=true
: これは全角文字の文字化け防止`useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
これをいれないとTimezoneのパースでエラーになります。
SQLException: The server time zone value '���� (�W����)' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.]
まったく理解してないですが、サーバーのtimezoneをちゃんと設定してあげる必要があるんだけど、呪文によってJDBCの設定で動くようにしているんじゃないかなーくらいに予想してます。 stackoverflow.com
RepositoryクラスでDatabaseの読み込み
import slick.driver.MySQLDriver.api._ . . private val database = Database.forConfig("todo-db")
MySQLDriver
をインポートして、Database.forConfig("DB設定")
をすることでセッションをはれます。
ORMとしてSlick利用
Entity クラスを編集
Slick によるマッピングができるよう、Domain Entityを編集します。
import slick.driver.MySQLDriver.api._ /** * entity class of todo */ case class Todo(id: Int, var status: TodoStatus, title: String) class Todos(tag: Tag) extends Table[Todo](tag, "todo") { def id = column[Int]("id", O.PrimaryKey, O.AutoInc) def status = column[TodoStatus]("status") def title = column[String]("title") def * = (id, status, title) <> (Todo.tupled, Todo.unapply) } object Todos extends TableQuery(new Todos(_))
case class
に実装されるtupled
, unapply
を利用してデータの展開を行いますが、自然体だと
def status = column[TodoStatus]("status")
自作したsealed trait
のTodoStatusによってunapply
ができません。
sealed trait
にマッパーを実装
import slick.driver.MySQLDriver.api._ sealed trait TodoStatus /** * todo status */ object TodoStatus { case object UNDONE extends TodoStatus case object DOING extends TodoStatus case object DONE extends TodoStatus implicit val todoStatusMapper = MappedColumnType.base[TodoStatus, String]( _.toString, TodoStatus.withName(_) ) def withName(s: String) = s.toLowerCase match { case "undone" => UNDONE case "doing" => DOING case "done" => DONE } }
こんな感じでimplicit val としてマッパーを実装してあげると、unapply
が出来るようになります。
implicit val todoStatusMapper = MappedColumnType.base[TodoStatus, String]( _.toString, TodoStatus.withName(_) )
Repository クラスの実装
Slick の力を借りることで、とてもスッキリとした形でORマッピングが可能になっています。 可読性も高く、Repository クラスがビジネスロジックの実装に集中できる形ですね。
package services.repository import com.google.inject.Singleton import services.domain.{Todo, TodoStatus, Todos} import slick.driver.MySQLDriver.api._ import scala.concurrent.Await import scala.concurrent.duration.Duration /** * repository class for todo management * */ @Singleton class TodoRepository { private val database = Database.forConfig("todo-db") def getTodoList(): List[Todo] = { val f = database.run(Todos.sortBy(_.id).result) Await.result(f, Duration.Inf).toList } def updateTodoList(ids: List[Int], status: TodoStatus) = { val toBeUpdated = Todos.filter(row => row.id inSetBind ids) database.run(toBeUpdated.map(_.status).update(status)) } }
Slickを利用すると、クエリの結果は全てFuture
に包まれて帰ってきます。
結果を待ってからレスポンスを返すか、非同期にレスポンスを返すかは実装者の側で選択する必要があります。
ここでは、同期的に結果を待ってから返すようにしています。
val f = database.run(Todos.sortBy(_.id).result)
Await.result(f, Duration.Inf).toList
感想
- あっちこっちの設定で色々とハマりましたが、なんとかデータベースとの接続が出来ました。
- Slick は設定さえできれば、Repositoryクラスをかなり綺麗に書けて便利だなぁという印象です。
- DDL の生成とかも出来るようなので、先にテーブルを作ってしまったのはちょっと後悔。
- Scala の
apply
,unapply
,implicit
など、Scala特有の?暗黙的なクラスやオブジェクトの仕組みも少しずつ分かってきた気がします。 - 次は画面からのaddを実装したいですね。
以上です。なかなか苦しみましたが、非常に勉強になりました。