この前、ScalaのString Interpolationを初めて自分で定義してみたので、ちょっと練習的にネタを書いてみました。
ターゲットは、JPQLです。
JPQLを使ってクエリを投げる時には、
em.createQuery("SELECT u FROM User u WHERE age > :age", User.class) .setParameter("age", "...") .getResultList();
みたいなことを書くわけですが(この例では名前付きパラメータですが、インデックスベースの位置指定パラメータ「?1」や「?」などでもOKです)、これをちょっと変えてみたいなぁと思いまして。
というわけで、練習を兼ねてString Interpolationを使ってもうちょっと簡単に書けないか試してみました。
使い方のイメージとしては、
jpql"SELECT u FROM User u WHERE age > $age"
.asQuery(classOf[User])(entityManager)
.getResultList
みたいな感じで?
では、いってみましょう。
依存関係の定義
JPAの実装にはHibernateを、データベースにはMySQLを、テスティングフレームワークにはScalaTestを使用することにしたので、依存関係の定義はこんな感じです。
build.sbt
name := "jpql-string-interpolation" version := "0.0.1-SNAPSHOT" scalaVersion := "2.10.3" organization := "littlewings" fork in run := true libraryDependencies ++= Seq( "org.hibernate" % "hibernate-entitymanager" % "4.2.8.Final", "mysql" % "mysql-connector-java" % "5.1.26" % "runtime", "org.scalatest" %% "scalatest" % "2.0" % "test" )
jpql補間子を定義する
それでは、JPQL用の補間子を定義します。
src/main/scala/javaee6/web/stringinterpolation/JpqlStringInterpolation.scala
package javaee6.web.stringinterpolation object JpqlStringInterpolation { implicit class JpqlStringInterpolationWrapper(val sc: StringContext) extends AnyVal { def jpql(params: Any*): Jpql = Jpql.bind(sc.parts.map(StringContext.treatEscapes).mkString("?"))((1 to params.size).zip(params): _*) } }
これで、
jpql"SELECT u FROM User u WHERE id = $id"
みたいなことを書くと、
"SELECT u FROM User u WHERE id = ?"
というようなクエリになります。このクエリと、実際にバインドされたパラメータとインデックスは以下のクラスに渡します。
src/main/scala/javaee6/web/stringinterpolation/Jpql.scala
package javaee6.web.stringinterpolation import javax.persistence.{EntityManager, Query} object Jpql { def bind(queryAsString: String)(params: (Int, Any)*): Jpql = new Jpql(queryAsString, params: _*) } class Jpql(queryAsString: String, params: (Int, Any)*) { def asQuery[T](resultClass: Class[T])(implicit entityManager: EntityManager): Query = params.foldLeft(entityManager.createQuery(queryAsString, resultClass)) { case (query, (index, value)) => query.setParameter(index, value) } def stripMargin: Jpql = Jpql.bind(queryAsString.stripMargin)(params: _*) def stripMargin(marginChar: Char): Jpql = Jpql.bind(queryAsString.stripMargin(marginChar))(params: _*) }
実際に、JPAのQueryに変換するのは、こちらのクラスです。asQueryでJPAのQueryに変換しますが、EntityManagerはImplicit Parameterとするようにしました。
あと、stripMarginくらいは使えるようにしておきました。
JPAを使う準備
永続性ユニットと、利用するEntityの定義。
src/main/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="javaee6.web.pu" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" /> <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8" /> <property name="javax.persistence.jdbc.user" value="kazuhira" /> <property name="javax.persistence.jdbc.password" value="password" /> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.format_sql" value="true" /> </properties> </persistence-unit> </persistence>
src/main/scala/javaee6/web/entity/User.scala
package javaee6.web.entity import scala.beans.BeanProperty import java.util.Objects import javax.persistence.{Column, Entity, Id, Table, Version} object User { def apply(id: Int, firstName: String, lastName: String, age: Int): User = { val user = new User user.id = id user.firstName = firstName user.lastName = lastName user.age = age user } } @SerialVersionUID(1L) @Entity @Table(name = "user") class User extends Serializable { @Id @BeanProperty var id: Int = _ @Column(name = "first_name") @BeanProperty var firstName: String = _ @Column(name = "last_name") @BeanProperty var lastName: String = _ @Column(name = "age") @BeanProperty var age: Int = _ @Column(name = "version_no") @BeanProperty var versionNo: Int = _ override def toString(): String = s"id = $id, firstName = $firstName, lastName = $lastName, age = $age, versionNo = $versionNo" override def equals(other: Any): Boolean = other match { case ou: User => id == ou.id && firstName == ou.firstName && lastName == ou.lastName && age == ou.age case _ => false } override def hashCode: Int = Objects.hashCode(id, firstName, lastName, age) }
使うテーブルの定義は、こちら。
mysql> SHOW CREATE TABLE user; +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | user | CREATE TABLE `user` ( `id` int(9) NOT NULL DEFAULT '0', `first_name` varchar(10) DEFAULT NULL, `last_name` varchar(10) DEFAULT NULL, `age` int(3) DEFAULT NULL, `version_no` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.05 sec)
使ってみる
では、ここまで定義したjpql補間子とEntityを使ったテストコードを書いてみます。
import文と初期化処理。
package javaee6.web.stringinterpolation import scala.collection.JavaConverters._ import javax.persistence.{EntityManager, EntityManagerFactory, Persistence} import javaee6.web.entity.User import javaee6.web.stringinterpolation.JpqlStringInterpolation._ import org.scalatest.{BeforeAndAfterAll, FunSpec} import org.scalatest.Matchers._ class JpqlStringInterpolationSpec extends FunSpec with BeforeAndAfterAll { override def beforeAll(): Unit = { val emf = Persistence.createEntityManagerFactory("javaee6.web.pu") val em = emf.createEntityManager val tx = em.getTransaction tx.begin() em.createNativeQuery("TRUNCATE TABLE user").executeUpdate() em.flush() tx.commit() tx.begin() Array(User(1, "カツオ", "磯野", 11), User(2, "ワカメ", "磯野", 9), User(3, "タラオ", "フグ田", 3)).foreach(em.persist) tx.commit() }
常に、データ再登録。
とりあえず、普通にJPQLを使ってみますか。
describe("Standard Jpql") { it("find One id equals & literal") { val emf = Persistence.createEntityManagerFactory("javaee6.web.pu") val em = emf.createEntityManager em.createQuery("SELECT u FROM User u WHERE id = 1", classOf[User]) .getSingleResult should be (User(1, "カツオ", "磯野", 11)) } }
クエリには、リテラルを直接埋め込んでいます。
このコードを実行すると、HibernateのログにはこういうSQLが出力されます。
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.first_name as first_na3_0_, user0_.last_name as last_nam4_0_, user0_.version_no as version_5_0_ from user user0_ where user0_.id=1
では、続いて同じようなことを定義したjpql補間子を使ってみると
describe("Jpql String Interpolation Spec") { describe("select") { it("find One id equals & numeric literal") { val emf = Persistence.createEntityManagerFactory("javaee6.web.pu") implicit val em = emf.createEntityManager jpql"SELECT u FROM User u WHERE id = ${1}" .asQuery(classOf[User]) .getSingleResult should be (User(1, "カツオ", "磯野", 11)) }
バインド変数が適用された形で実行されます。また、EntityManagerを束縛している変数には、「implicit」を付与しています。
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.first_name as first_na3_0_, user0_.last_name as last_nam4_0_, user0_.version_no as version_5_0_ from user user0_ where user0_.id=?
変数を使ってみます。
it("find One id equals And age equals") { val emf = Persistence.createEntityManagerFactory("javaee6.web.pu") implicit val em = emf.createEntityManager val id = 2 val age = 9 jpql"SELECT u FROM User u WHERE id = $id AND age = $age" .asQuery(classOf[User]) .getSingleResult should be (User(2, "ワカメ", "磯野", 9)) }
raw string literal。
it("find One id,age & raw string literal, stripMargin") { val emf = Persistence.createEntityManagerFactory("javaee6.web.pu") implicit val em = emf.createEntityManager val id = 2 val age = 9 jpql"""|SELECT u | FROM User u | WHERE id = $id | AND age = $age""" .stripMargin .asQuery(classOf[User]) .getSingleResult should be (User(2, "ワカメ", "磯野", 9)) }
複数件。
it("find Collection age > 5") { val emf = Persistence.createEntityManagerFactory("javaee6.web.pu") implicit val em = emf.createEntityManager val age = 5 jpql"SELECT u FROM User u WHERE age > $age" .asQuery(classOf[User]) .getResultList should contain theSameElementsAs (Array(User(1, "カツオ", "磯野", 11), User(2, "ワカメ", "磯野", 9))) }
ちゃんと、テストにもパスしますよ。
> test 〜省略〜 [info] JpqlStringInterpolationSpec: [info] Standard Jpql [info] - find One id equals & literal [info] Jpql String Interpolation Spec [info] select [info] - find One id equals & numeric literal [info] - find One id equals And age equals [info] - find One id,age & raw string literal, stripMargin [info] - find Collection age > 5 [info] Run completed in 5 seconds, 708 milliseconds. [info] Total number of tests run: 5 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 5, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed.
String Interpolationなので、こういう風に存在しない変数を書いたりすると
val id = 2 val age = 9 jpql"SELECT u FROM User u WHERE id = $id AND age = $ag"
コンパイルエラーになります。
src/test/scala/javaee6/web/stringinterpolation/JpqlStringInterpolationSpec.scala:59: not found: value ag [error] jpql"SELECT u FROM User u WHERE id = $id AND age = $ag" [error] ^ [error] one error found [error] (test:compile) Compilation failed
バインドする手間もなくて、スッキリ。
と言いたいところですが、NamedQueryには手が出せないとか、インデックスベースのバインド変数の指定はJPA的にはよろしくないらしくて、Hibernateからは警告されたりします。
WARN: [DEPRECATION] Encountered positional parameter near line 1, column 53. Positional parameter are considered deprecated; use named parameters or JPA-style positional parameters instead.
あと、JPAを使っていてAPIとして気になるのは、QueryのgetResultListのシグネチャが
java.util.List getResultList()
なことですかね…。ジェネリクス使えるんだから、List
まあ、ちょっと遊んでみました的なネタでした。
ソースは、こちらに置いています。
https://github.com/kazuhira-r/javaee6-scala-examples/tree/master/jpql-string-interpolation
こういうのの活用は、先人であるScalikeJDBCで文化を学んできた方がいいのかなぁ…。
ScalikeJDBC
http://scalikejdbc.org/