MonadicでReactiveだと言われているSlick3.xを使ってみる。
本記事時点での最新はは3.1.1となっている。
公式のサンプルだとh2を使っていたが、今回はローカルに立てたMySQLを使用する。
Getting Started — Slick 3.1.1 documentation
準備
事前にuser
テーブルを作っておく
Create Table: CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into user values (1, "alice", "alice@example.com");
insert into user values (2, "bob", "bob@example.com");
導入
build.sbtにslickを追加する。
MySQLを使用するので、そのjdbcの依存関係も追加。
libraryDependencies ++= Seq(
"com.typesafe.slick" %% "slick" % "3.1.1",
"org.slf4j" % "slf4j-nop" % "1.7.20",
"mysql" % "mysql-connector-java" % "5.1.35"
)
次に、resources/application.confにMySQLの設定を記述する。
DB名はsample-db
としている。
mysql-local = {
url = "jdbc:mysql://localhost/sample-db"
driver = com.mysql.jdbc.Driver
user = "user"
password = "password"
connectionPool = disabled
keepAliveConnection = true
}
DBに接続してSELECTを実行
MySQLに接続してSQLを実行してみる。
object Main extends App {
val db: JdbcBackend.DatabaseDef = Database.forConfig("mysql-local")
import slick.driver.MySQLDriver.api._
val sql = sql"select * from user".as[(Int, String, String)]
val f = db.run(sql)
Await.result(f, Duration.Inf) foreach println
}
これを./activator run
すると
(1,alice,alice@example.com)
となって正しくselect出来た。
クエリの宣言と実行が分離されたAPIとなっている。
独自クラスにmappingする
上の例だと(Int, String, String)
にselectした結果をmappingしたが、独自クラスにmappingしてみる。
user
テーブルに対応するクラスを用意する。
case class User(id: Int, name: String, email: String)
これに対応させるにはこのように書けばよい。
implicit val getUserResult = GetResult { r => User(r.<<, r.<<, r.<<) }
val f: Future[Seq[User]] = db.run(sql"select * from user".as[User])
これでUser
クラスのインスタンスとして結果が得られる
User(1,alice,alice@example.com)
ここで気になるのがこの行。
implicit val getUserResult = GetResult { r => User(r.<<, r.<<, r.<<) }
このr
はPositionedResult
という型で、カラムが定義されている順番に依存してしまっている。
カラム名で値を取ってくるにはこのようにすれば良い。
implicit val getUserResult = GetResult { r =>
User(r.rs.getInt("id"), r.rs.getString("name"), r.rs.getString("email"))
}
テーブルにアクセスするクラスを実装する
SQLを生で書いてGetResult
を使ってインスタンス化するのもいいが、
データアクセスするクラスに抽象化しておきたい。
その場合はTable
やTableQuery
を使用してuser
テーブルのmappingを実装する。
import slick.driver.MySQLDriver.api._
case class User(id: Int, name: String, email: String)
class Users(tag: Tag) extends Table[User](tag, "user") {
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def email = column[String]("email")
def * = (id, name, email) <> (User.tupled, User.unapply)
}
object Users extends TableQuery(new Users(_))
このように実装しておくとUsers
に対してコレクション操作のようなAPIを使用してDBからselectが出来るようになる。
注意点として、filter
で==
を使用する時は===
を使う必要があるらしい。
同様に、!=
のかわりに=!=
が必要となる。
// select * from user;
db.run(Users.result)
// select * from user where name = 'alice';
db.run(Users.filter{ _.name === "alice" }.result)
// select email from user where name like 'alice%';
db.run(
Users
.filter { _.name.startsWith("alice") }
.map { _.email }
.result
)
ちなみに、実行されたSQLを見るには以下のようにすれば見れる。
val query = Users.filter { _.id =!= 2 }.map { _.name }
println(query.result.statements)
これでこのように出力される。
List(select `name` from `user` where not (`id` = 2))
insert/update/delete
それぞれも直感的に使えるようになっている。
コメントに載せてあるSQLはstatements
で得られる文字列ではなく、実際に実行されるもの。
-
insert
-
+=
を使う(Users.insertOrUpdate
とかもある)
// insert into user values (3, 'charles', 'charles@example.com') db.run( Users += User(3, "charles", "charles@example.com") )
-
-
update
-
update
を使う
// update user set name = 'alice-updated' where name like 'alice%'; db.run( Users .filter { _.name.startsWith("alice") } .map(_.name) .update("alice-updated") )
-
-
delete
-
delete
を使う
// delete from user where name = 'alice'; val f = db.run( Users.filter{ _.name === "alice" }.delete )
-
Monadic Slick
ScalaでMonadic、つまりfor式を使ってselectを実行してみる。
そのためにもう1つテーブルを用意する。
Create Table: CREATE TABLE `hobby` (
`id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`content` text,
KEY `user_id` (`user_id`),
CONSTRAINT `hobby_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into hobby values (1, 1, 'tennis');
insert into hobby values (2, 2, 'soccer');
insert into hobby values (3, 2, 'music');
対応するモデルは以下。Users
とほぼ同じだが、今回はforeignKey
が入っている。
case class Hobby(id: Int, userId: Int, content: String)
class Hobbies(tag: Tag) extends Table[Hobby](tag, "hobby") {
def id = column[Int]("id", O.PrimaryKey)
def userId = column[Int]("user_id")
def content = column[String]("content")
def user = foreignKey("user_FK", userId, Users) (
_.id,
onUpdate = ForeignKeyAction.Cascade,
onDelete = ForeignKeyAction.Cascade
)
def * = (id, userId, content) <> (Hobby.tupled, Hobby.unapply)
}
object Hobbies extends TableQuery(new Hobbies(_))
// Usersと同様に使用できる
// db.run(Hobbies.result) foreach println
このHobbyテーブルと前述のUserテーブルを使ってMonadicなselectを行う。
かなり直感的に使用できる。
val q = for {
user <- Users
hobby <- Hobbies if hobby.userId === user.id
} yield { (user.name, hobby.content) }
db.run(q.result) foreach println
実行すると以下の結果が得られ、想定通りに実行されていることがわかる。
Vector((alice,tennis), (bob,soccer), (bob,music))
実行されたSQLはこのようになっている。
List(select x2.`name`, x3.`content` from `user` x2, `hobby` x3 where x3.`user_id` = x2.`id`)
Reactive Slick
Slick3からはReactiveであると言われている 公式
具体的にはDBからのSELECT結果をReactiveStreamとして受け取ることが出来るようになっている。
val query = Users.filter { _.id =!= 2 }.result
val stream: DatabasePublisher[User] = db.stream(query)
stream
.mapResult { user => user.name }
.foreach { println }
.andThen { case _ => system.terminate() }
このDatabasePublisher
はorg.reactivestreams.Publisher
を実装している。
また、Akka-Streamと組み合わせることも可能。
val source = Source.fromPublisher(stream)
val flow = Flow[String].map(s => s"mapped: $s")
val future = source via flow runForeach println
Await.result(future andThen { case _ => system.terminate() }, Duration.Inf)
所感
DBへの問い合わせ自体をIOモナドのように宣言と実行を切り離した上で、実行結果をFuture
かReactiveに扱えるようになっている。
単純に使うならFuture
で良さそうだが、Reactiveなアプリケーションを実装しているなら相性も良さそうに感じる。
DAOを実装するには簡単に出来る上に、スキーマからコードを自動生成する機能もあるようなので非常に便利。
Schema Code Generation — Slick 3.1.1 documentation