LoginSignup
17
18

More than 5 years have passed since last update.

ScalaでPlayFramework: モデル編

Posted at

この記事は以下の記事の続きです。
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のJDBCAnormを依存ライブラリに追加します。

build.sbt
...
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情報を追加します。

conf/application.conf
## 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というファイルを作り、以下のように書きます。

app/models/DBAccess.scala
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以外を使用するには以下のようにします。

app/models/DBAccess.scala
...
@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

コードは以下のようになります。

app/models/DBAccess.scala
...
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] = {

ちゃんと取得できているか確認してみましょう。
コントローラに以下を書きます。インジェクションの部分変更があるので忘れずに。

app/controller/HomeController.scala
...
@Singleton
class HomeController @Inject()(db: DBAccess) extends Controller {
...
  def languageList = Action {
    Ok(db.languageList.toString)
  }
...

ルーティングにも追加します。

conf/routes
...
GET     /language_list              controllers.HomeController.languageList
...

/language_listにアクセスしてみると、こんなものが表示されました。

List(~(~(1,Scala),0), ~(~(2,Python),5), ~(~(3,Swift),1))

どうも取れてそうですが、使いにくそうです。
パーサを改善、と言うかマッパーを噛ませてみます。

app/models/DBAccess.scala
...
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))

使えそうになりました。

レコードを挿入してみる

取得に比べれば随分楽です。

app/models/DBAccess.scala
...
  def insert: Option[Long] = {
    db.withConnection { implicit c =>
      SQL("insert into language(name, experience) values('Java', 4)").executeInsert()
    }
  }
...

挿入はSQLの後にexecuteInsert()をくっつけます。
返り値は、主キーがauto_incrementで単一レコードの挿入の場合は、挿入されたレコードの主キーになります。
それ以外の、複数レコードの更新だったり、主キーが文字列だったりの場合は、文字列のリストになります。

app/models/DBAccess.scala
...
  def insertTwo: List[String] = {
    db.withConnection { implicit c =>
      SQL("insert into language(name, experience) values('C', 5), ('Ruby', 2)").executeInsert(str.+)
    }
  }
...

あ、executeInsert()にもパーサを渡すのね。

レコードを更新・削除してみる

今度はexecuteUpdate()を使います。予想通りかな?

app/models/DBAccess.scala
...
  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を付け足すとできます。

app/models/DBAccess.scala
...
  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.withConnectiondb.withTransactionに変えましょう。それだけ。

おわりに

今回はレコードの取得ですごく苦労しました。
最初はパーサの書き方が全くわからず、公式ドキュメントとにらめっこしながら、書いてはエラー書いてはエラー・・・
パーサ書くにはカラムを全部書かなければいけない気がしますが、カラム数が多いと大変そうですね。
Slickだったらこんなに大変では無いのでしょうか?なんかやり方間違えてるのかな・・・?
一応目的は達成しましたが、不明点がとても残ってます。引き続き使ってみて徐々に理解していこうと思います。
記事書いている途中で疲れてきて、文体とか崩れてしまっているかと思いますが、直す気力が無いので許してください。。。
ScalaでPlayFrameworkシリーズは一旦これでおしまいです。
指摘とかアドバイスとかあったらぜひください。お願いします!

17
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
18