この記事は以下の記事の続きです。
ScalaでPlayFramework: ビュー編
公式ドキュメントはこちら
Play 2.5.10 documentation
この記事の内容は、だいたい以下のページに書かれています。
DBはMySQLを使います。入っていない方はbrew
とかで入れてください。
$ mysql --version
mysql Ver 14.14 Distrib 5.7.11, for osx10.11 (x86_64) using EditLine wrapper
接続にはAnorm
というものを使います。
もう一つSlick
と言うものもありますが、全然追えてないので、この記事では対象外とします。
Anorm
も内容が沢山あって、本記事ではとりあえずDBと繋ぐくらいのレベルですので、詳しくは公式ドキュメントを参照してください。
DBに接続する
MySQLに接続してクエリを実行し、結果を取得することを目指します。
いくつかの設定
コードを書く前に設定しなければならないことがあります。
まずは、build.sbt
でMySQLのJDBC
とAnorm
を依存ライブラリに追加します。
...
libraryDependencies ++= Seq(
jdbc,
cache,
ws,
"org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test,
"mysql" % "mysql-connector-java" % "5.1.40",
"com.typesafe.play" %% "anorm" % "2.5.2"
)
バージョンはこちらで確認してください。
Download Connector/J
playframework/anorm
次に、conf/application.conf
の下の方にあるJDBC Datasourceの部分にDB情報を追加します。
## JDBC Datasource
...
#
db {
# You can declare as many datasources as you want.
# By convention, the default datasource is named `default`
...
# これを追加
default.driver=com.mysql.jdbc.Driver
default.url="jdbc:mysql://localhost/play_sample?characterEncoding=UTF8"
default.username=root
default.password=password
...
}
とりあえず名前はdefault
にしておきましょう。
もし複数のDBに接続したい場合は、default
の部分を任意の名前に変えてDB情報を列挙します。
上の例は、localhost
にあるplay_sample
というDBに、root
というユーザ(パスワードはpassword
)でアクセスします。
モデルを作る
まずはパッケージを作りましょう。app
以下にmodels
というパッケージを作ります。
その下にDBAccess
というファイルを作り、以下のように書きます。
package models
import javax.inject._
import play.api.mvc._
import play.api.db._
import anorm._
import anorm.SqlParser._
@Singleton
class DBAccess @Inject()(db: Database) {
}
これでconf/application.conf
で設定したdefault
のDBを使用することができます。
default
以外を使用するには以下のようにします。
...
@Singleton
class DBAccess @Inject()(@NamedDatabase("DB名") db: Database) {
}
import anorm._
のところでエラーが出ますが、問題なく動作します。テンプレートもそうだけど、どうにかならないのかな?
接続する
DBに接続するには以下のようにします。
...
db.withConnection { implicit c =>
// 何かしら処理
}
...
これを使っておけば、スコープを外れたときに勝手に接続をクローズしてくれます。
レコードを取得してみる
以下のテーブルからレコードを取得してみます。
テーブル名:language
id:Int(11)/auto_increment | name:text | experienece:Int(3) |
---|---|---|
1 | Scala | 0 |
2 | Python | 5 |
3 | Swift | 1 |
コードは以下のようになります。
...
class DBAccess @Inject()(db: Database) {
val parser = int("id") ~ str("name") ~ int("experience")
def languageList: List[Int~String~Int] = {
db.withConnection { implicit c =>
SQL("SELECT * FROM language ORDER BY id").as(parser *)
}
}
}
...
色々出てきましたね。
まず、これ。
SQL("SELECT * FROM language ORDER BY id")
SQLです。はい。SQL書きます。
次に、SQLの後についているこれ。
as(parser.*)
SQLの結果の各レコードをパースするパーサを渡します。
今回対象のクエリで取得できるレコードは、カラムが左から、Int(11), text, Int(3)となるので、これをパースできるパーサを作ります。
それがこれ。ダブルクオートで囲われているのはカラム名です。
val parser = int("id") ~ str("name") ~ int("experience")
~
はAnorm
の演算子らしく、イメージとしてはタプルを作ってると思えばいいみたいです。
後ろにくっついている*
は、レコードが0個以上あるときにつけます。
必ず1個以上になる場合は+
が使えます。正規表現と一緒ですね。
主キーによる検索など、結果が1個になるときはsingle
、0または1個の場合はsingleOpt
になります。
as(parser.*) // 0以上
as(parser.+) // 1以上
as(parser.single) // 1
as(parser.singleOpt) // 1 or 0
そして返り値はレコードの分だけInt~String~Int
ができるのでList[Int~String~Int]
になります。
def languageList: List[Int~String~Int] = {
ちゃんと取得できているか確認してみましょう。
コントローラに以下を書きます。インジェクションの部分変更があるので忘れずに。
...
@Singleton
class HomeController @Inject()(db: DBAccess) extends Controller {
...
def languageList = Action {
Ok(db.languageList.toString)
}
...
ルーティングにも追加します。
...
GET /language_list controllers.HomeController.languageList
...
/language_list
にアクセスしてみると、こんなものが表示されました。
List(~(~(1,Scala),0), ~(~(2,Python),5), ~(~(3,Swift),1))
どうも取れてそうですが、使いにくそうです。
パーサを改善、と言うかマッパーを噛ませてみます。
...
class DBAccess @Inject()(db: Database) {
val parser = int("id") ~ str("name") ~ int("experience")
val mapper = parser.map {
case id ~ name ~ experience => Map("id" -> id, "name" -> name, "experience" -> experience)
}
def languageList: List[Map[String,Any]] = {
db.withConnection { implicit c =>
SQL("SELECT * FROM language ORDER BY id").as(mapper.*)
}
}
}
...
マッパーが増えました。パース結果にカラム名を追加して、マップとして返します。
val mapper = parser.map {
case id ~ name ~ experience => Map("id" -> id, "name" -> name, "experience" -> experience)
}
SQLの後につけるのをパーサではなくマッパーにしました。
SQL("SELECT * FROM language ORDER BY id").as(mapper.*)
マッパーを使うようにしたので、返り値も変わりました。
def languageList: List[Map[String,Any]] = {
再び/language_list
にアクセスしてみます。
List(Map(id -> 1, name -> Scala, experience -> 0), Map(id -> 2, name -> Python, experience -> 5), Map(id -> 3, name -> Swift, experience -> 1))
使えそうになりました。
レコードを挿入してみる
取得に比べれば随分楽です。
...
def insert: Option[Long] = {
db.withConnection { implicit c =>
SQL("insert into language(name, experience) values('Java', 4)").executeInsert()
}
}
...
挿入はSQLの後にexecuteInsert()
をくっつけます。
返り値は、主キーがauto_increment
で単一レコードの挿入の場合は、挿入されたレコードの主キーになります。
それ以外の、複数レコードの更新だったり、主キーが文字列だったりの場合は、文字列のリストになります。
...
def insertTwo: List[String] = {
db.withConnection { implicit c =>
SQL("insert into language(name, experience) values('C', 5), ('Ruby', 2)").executeInsert(str.+)
}
}
...
あ、executeInsert()
にもパーサを渡すのね。
レコードを更新・削除してみる
今度はexecuteUpdate()
を使います。予想通りかな?
...
def update: Int = {
db.withConnection { implicit c =>
SQL("update language set experience = 999 where name = 'Scala'").executeUpdate()
}
}
def delete: Int = {
db.withConnection { implicit c =>
SQL("delete from language where name = 'C'").executeUpdate()
}
}
...
返り値は更新に成功したレコードの件数です。
値を後から入れるやつ(名前わからない)
SQLに{id}
(idは任意の文字列)と書いておいて、あとでon
を付け足すとできます。
...
def search(language: String): Option[Map[String,Any]] = {
db.withConnection { implicit c =>
SQL("SELECT * FROM language where name = {language}").on("language" -> language).as(mapper.singleOpt)
}
}
...
on
はマップと同じ要領で書きます。
トランザクション
db.withConnection
をdb.withTransaction
に変えましょう。それだけ。
おわりに
今回はレコードの取得ですごく苦労しました。
最初はパーサの書き方が全くわからず、公式ドキュメントとにらめっこしながら、書いてはエラー書いてはエラー・・・
パーサ書くにはカラムを全部書かなければいけない気がしますが、カラム数が多いと大変そうですね。
Slick
だったらこんなに大変では無いのでしょうか?なんかやり方間違えてるのかな・・・?
一応目的は達成しましたが、不明点がとても残ってます。引き続き使ってみて徐々に理解していこうと思います。
記事書いている途中で疲れてきて、文体とか崩れてしまっているかと思いますが、直す気力が無いので許してください。。。
ScalaでPlayFrameworkシリーズは一旦これでおしまいです。
指摘とかアドバイスとかあったらぜひください。お願いします!