PlayFramework Anormを使用してDBへ問い合わせを行う

個人開発したアプリの宣伝
目的地が設定できる手帳のような使い心地のTODOアプリを公開しています。
Todo with Location

Todo with Location

  • Yoshiko Ichikawa
  • Productivity
  • Free

スポンサードリンク

AnormはJDBCインスタンスを利用して、シンプルなDBレコード操作を提供する。

JDBCで書くSQLとの違いは、

  • ScalaオブジェクトにマッピングするParserを定義することで、ユーザが定義したcase class型のListを作成することが簡略化できる。
  • PreparedStatementのplaceholderに{bindKey}のように名前付きのplaceholderが定義できる


Anormの導入

build.sbt

libraryDependencies += jdbc
libraryDependencies += "org.postgresql" % "postgresql" % "42.2.12"
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.5"

DBドライバはRDBに応じたドライバを選択すればよい。

また、JDBCのインスタンスを利用するのでapplication.conf

db.default.driver=org.postgresql.Driver
db.default.url="jdbc:postgresql://localhost:5432/dbname"
db.default.user=username
db.default.password=userpassword

を定義しておく


AnormでSQLを記述する

JDBCインスタンスはControllerより@Injectで受け取ればよい。

class HomeController @Inject()(db:Database, val controllerComponents: ControllerComponents) extends BaseController {


Insert

以下のように記述することでPreparedStatementに変数をセットして実行できる。

db.withConnection{ implicit connection =>
  val sequence = SQL("Insert into yourTableName (column1, column2)values({column1}, {column2})")
    .on("column1" -> "value1", "column2" -> "value2")
    .executeInsert()
}

executeInsert()はdatabaseConnectionを受け取るんだけど、implicitの引数定義なので、implicit connection =>とconnectionをimplicitとして定義しておけば引数の記述を省略できる。

sequenceにはinsertしたprimary keyが返されていた。


Update & Delete

以下の例はDeleteの例だけど、Updateも中のSQLが変わるだけで、使用するメソッドに変わりはない。

db.withConnection{ implicit connection =>
  val affectedRows = SQL("delete from yourTableName where id = {id}").on("id" -> 100).executeUpdate()
}

多分、executeUpdate()の結果は変更を与えた行数が返されるのだと思う。


Select

Selectはまず結果cursolのParserを用意しておく。Parser連結末尾の~(チルダ)を忘れずに。

例としてJoinしたSelect結果に対応したParserを作成してみた。

case class JoinedRecord(id:Int, field1:String, field2:String, field3:String)
val columnParser = {
  SqlParser.int("yourTableName.id") ~
  // .column1は取り出すcolumn名を記述する
  SqlParser.str("yourTableName.column1") ~
  SqlParser.double("yourTableName.column2") ~
  SqlParser.double("joinedTableName.column3")
}map{
  case id ~ column1 ~ column2 ~ column3 => JoinedRecord(id, column1, column2, column3)
}

上記のパーサーは問い合わせ結果のをJoinedRecordというcase classのフィールドに変換するパーサーとなる。

SqlParserに与える引数はtable名.カラム名となる。

Selectの実行は

db.withConnection{ implicit connection =>
  val result:List[JoinedRecord] = SQL("select * from yourTableName join joinedTableName on yourTableName.id = joinedTableName.id").as(columnParser.*)
}

末尾の*(アスタリスク)は結果を全行取得するという意味。

実行結果は定義したcase class JoinedRecord型のListが返却される。


トランザクション

これはAnormというかJDBCなんだけど、transactionを宣言する時は、db.withTransaction()でbegin宣言をしたconnectionが高階関数に渡される。なので、このconnectionを使用してSQLを発行すればよい。SQL等が失敗して例外throwした時は自動的にrollbackされる。

db.withTransaction{ implicit connection =>
}

また以下のように明示的にtransactionをcommit or rollbackすることもできる。

// db.withTransaction{ implicit connection =>と同義
implicit val connection = db.getConnection(false)
try {
    SQL().on().executeUpdate()
    connection.commit()
}catch{
  case e:SQLException => connection.rollback
}


その他

ControlleにJDBCインスタンスを@Injectする性質上、Fat Controllerに陥りやすい。

注入されたDBインスタンスは別途、Repositoryクラスなどを用意して、そちら側で使用するのが良いと思う。

複数のRepository間でbeginされたconnectionを取り回す。など必要な場合はRepositoryはConnectionタイプを受け取る設計でも良いと思う。