ついにScalikeJDBCのFirst exampleです。
ScalikeJDBCはドキュメントが充実していて、scalikejdbc cookbookを読む事によりコーディングを進める事ができます。
が簡単に私自身最初につまったところを紹介していきたいと思います。
1.『sql』クラス
下記の例の様に sql で """ 内にSQL文を記述して実行する事ができます。
例
sql"""
insert into ms_user (id, name, created_timestamp) values (1, 'Alice', CURRENT_TIMESTAMP());
""".execute.apply()
普通にSQLを書いて、execute.apply()で実行可能です。
また、
例
val name = "Bob"
sql"""
insert into ms_user (id, name, created_timestamp) values (1, ${name}, CURRENT_TIMESTAMP());
""".execute.apply()
この様に nameで変数宣言をした値を埋め込む事もできます。
これは、string interpolation (文字列補間) をScalikeJDBCが利用しているから実行可能な構文です。もちろんSQLインジェクション対策も実施されています。
2.『sqls』クラス
sqlと異なり、部品としてSQLを利用可能です。
例
def findByUrl(url: String)(implicit session: DBSession = AutoSession): Option[Entry] = {
val whereName = sqls" url = ${url} "
toEntry(TrEntry.findBy(whereName))
}
TrEntry は別途 scalikejdbcGen(クラスの生成用ツール)を利用して生成したクラスです。
例
def findBy(where: SQLSyntax)(implicit session: DBSession = autoSession): Option[TrEntry] = {
withSQL {
select.from(TrEntry as te).where.append(where)
}.map(TrEntry(te.resultName)).single.apply()
}
部品として宣言したWhere条件を別途与えて、SELECT句を実行可能です。
3.『sql / sqls』の仕組みについて
"sql" や "sqls" は、string interpolation (文字列補間) を利用して実装されています。
string interpolation (文字列補間)は前述の通り、s"Hello, $name"と言った構文で変数を文字列の中に記述可能なScalaの言語仕様です。
Scalaでは、この文字列の先頭にあるs補間子の部分を独自に宣言できます。
ScalikeJDBCでは、"sql" や "sqls" を補間子として宣言しています。
以下の例では、"xxx"を補間子として宣言しています。
implicit class XxxHelper(val sc: StringContext) extends AnyVal {
def xxx(args: Any*): String = {
val strings = sc.parts.iterator
val expressions = args.iterator
var buf = new StringBuffer(replaceName(strings.next))
while(strings.hasNext) {
buf append expressions.next
buf append replaceName(strings.next)
}
buf.toString
}
private def replaceName(s: String) = s.replace("次郎","XXX")
}
XxxHelper の sc は、ダブルクォーテーションで囲われた文字列を受けとります。
文字列内に変数が記述されていると、その前後で分割されて渡されます(sc.parts.iteratorの理由)。
『def xxx』のargsは文字列内の変数が複数渡されます(args.iterator)。
それぞれを結合して、変数部分以外を置換して、結果を返しています。
上記のxxx補間子を利用することで文字列中の"次郎"を伏せ字"XXX"に置換できます。
val taro2 = Student("太郎","大阪",6)
val contents = xxx"次郎は、${taro2.firstName} と一緒に自宅に帰りました"
println(contents)
>XXXは、太郎 と一緒に自宅に帰りました
誰と一緒に帰ったかがわからなくなりました。
このようなカスタム補間子を利用して、"sql"と"sqls"は実装されています。
なお、
・"sql"で補間後の型はSQL[A, NoExtractor]
・"sqls"で補間後の型はSQLSyntax
となっています。
4.Select結果:単純なListとMap値
単純なMap型への変換が下記例の部分になります。
sql"select * from members".map(_.toMap).list.apply()
>List(Map(ID -> 1, NAME -> Alice, CREATED_AT -> 2016-01-01 23:26:52.644), Map(ID -> 2, NAME -> Bob, CREATED_AT -> 201・・・・
フィールド名をキーとし、値を保持したMap型の結果を複数件List型で取得できます。
5.Select結果:case classへの展開
1レコードを1オブジェクト(Entity)として表現した、"case class"にも簡単に展開できます。
// defines entity object and extractor
import org.joda.time._
case class Member(id: Long, name: Option[String], createdAt: DateTime)
object Member extends SQLSyntaxSupport[Member] {
override val tableName = "members"
def apply(rs: WrappedResultSet) = new Member(
rs.long("id"), rs.stringOpt("name"), rs.jodaDateTime("created_at"))
}
// find all members
val members: List[Member] = sql"select * from members".map(rs => Member(rs)).list.apply()
"case class" で宣言しているMemberに上記行で取得した結果を展開(rs => Member(rs))しています。 "rs => Member(rs)" は、rs を引数としてとるMember.apply(rs)を呼び出しています。
applyは特殊なメソッドで、コンパニオンオブジェクトに定義しておけば、メソッド名無しでインスタンスを生成してくれます。コンパニオンオブジェクトとは、 "case class"と対になって"object"宣言されたclassの事です。
6.Select結果:1項目のみ
件数だけを取得したい場合には、以下のように記述することでOption型で取得できます。
val count: Option[Int] = sql"select count(*) from members".map(_.int(1)).single().apply
Optionをやめたい場合には、以下のように最後にgetOrElseを入れることで、"取得できなかった場合"を想定した動きになります。
val count: Int = sql"select count(*) from members".map(_.int(1)).single().apply.getOrElse(0)
7.Insert/Update/Delete
登録、更新、削除も下記のように簡単に実行できます。
val name:String = "Tom"
sql"insert into members (name, created_at) values (${name}, current_timestamp)".update.apply()
val id:Int = 3
sql"update members set name = 'Sally' where id = ${id}".update.apply()
sql"delete members where id = ${id}".update.apply()
8.セッション
これまでSELECTやINSERTを説明してきましたが、紹介した構文を記載するだけでは、単純に実行できません。
JDBCドライバの初期化とセッションの宣言(ここではAutoSession)が必要になります。
// initialize JDBC driver & connection pool
Class.forName("org.h2.Driver")
ConnectionPool.singleton("jdbc:h2:mem:hello", "user", "pass")
// ad-hoc session provider on the REPL
implicit val session = AutoSession
Boilerplate(ボイラープレート) ・・・ 鋳型的なもの。決まり文句。
Scala に関する書籍やブログでみかける単語があります。初めて読んだときはこの単語の意図がつかめなかったのですが、"こういう処理をする場合には必ず'この記載'が必要"と言った決まり文句の事を表現しています。
記述が増えて、コードが長くなったり、引数が増えたりする事で本質的な箇所が見えにくくなります。一言で表現するとボイラープレートのコードが多いねと言う表現になるようです。共通関数、継承の仕組みで対応できるものもありますが、Scalaではimplicit(暗黙)の型等の仕組みを利用してボイラープレートのコードを減らすことができています。
ScalikeJDBCでは、データベースに対する処理を実行する際に必要になる接続・トランザクションの情報をimplicit parameterで渡すようになっています。
implicit parametarを利用することで、お決まりで必要になるパラメータが不要になっています。
実際には
sql"update members set name = 'Sally' where id = ${id}".update.apply()(session)
ですが、 "session"部分は省力して記載できます。
これはカリー化と呼ばれるテクニックによって実装されています。
apply()を実行した結果、sessionを引数にとる関数を返す(これがカリー化)が、sessionは implicit なパラメータのため暗黙的に宣言された値を利用して意識せずに実行できます。
9.トランザクション
AutoSession の場合、update.applyが呼ばれる度にコミットが実行されています。
First exampleではAutoSessionです。
// 一回のInsert文ごとにコミット。
Seq("Alice", "Bob", "Chris") foreach { name =>
sql"insert into members (name, created_at) values (${name}, current_timestamp)".update.apply()
}
以下のコードのようにわざと同じIDで登録するとBobの登録の際に一意制約エラーになります。
この場合には、当然ロールバックはかからずにAliceは登録されます。
Seq("Alice", "Bob", "Chris") foreach { name =>
sql"insert into members (id, name, created_at) values (1, ${name}, current_timestamp)".update.apply()
}
3人をセットで『登録するか』、『登録しない』にしたい場合には、以下の DB localTx ブロックで囲みます。
try {
val countResult = DB localTx { implicit session =>
Seq("Alice", "Bob", "Chris") foreach { name =>
sql"insert into members (id, name, created_at) values (1, ${name}, current_timestamp)".update.apply()
}
}
} catch {
case e:Exception => println(e.getMessage)
}
こうするとAliceは登録が一旦成功しますが、Bobのところでエラーになりロールバックがかかります。
おわりに
ScalikeJDBCはドキュメントが充実していて、本当にとっつき易いライブラリだと思います。
DBアクセスまわりの経験があまり無く、Scalaもがっつり経験が無いと、First exampleをいきなり読むのは厳しいのではと思って3回にわけて書いてみました。
皆様の参考になれば嬉しいです。