MonadicでReactiveなSlick3.xを使う
2016-03-30
QiitaScalaMonadic で 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
from: https://qiita.com/petitviolet/items/688cbf917df240407077