Scala のケースクラスに制約を持たせる
Scala のケースクラスで値に制約を持たせたい場合にどうするか。
例えば、以下のケースクラスで amount
の値を 0 以上となるように制限し、0 未満ならインスタンス化を失敗させる事を考えてみます。
case class Quantity(amount: Int)
使用した環境は以下
- Scala 2.12.7
ソースは http://github.com/fits/try_samples/tree/master/blog/20181009/
ケースクラスの値を制限
まず、最も単純なのは以下のような実装だと思います。
case class Quantity(amount: Int) { if (amount < 0) throw new IllegalArgumentException(s"amount($amount) < 0") }
これだと、例外が throw されてしまい関数プログラミングで扱い難いので Try[Quantity]
や Option[Quantity]
等を返すようにしたいところです。
そこで、以下のようにケースクラスを abstract
化して、コンパニオンオブジェクトへ生成関数を定義する方法を使ってみました。
sample.scala
import scala.util.{Try, Success, Failure} sealed abstract case class Quantity private (amount: Int) object Quantity { def apply(amount: Int): Try[Quantity] = if (amount >= 0) Success(new Quantity(amount){}) else Failure(new IllegalArgumentException(s"amount($amount) < 0")) } println(Quantity(1)) println(Quantity(0)) println(Quantity(-1)) // この方法では Quantity へ copy がデフォルト定義されないため // copy は使えません(error: value copy is not a member of this.Quantity) // // println(Quantity(1).map(_.copy(-1)))
実行結果は以下の通りです。
実行結果
> scala sample.scala Success(Quantity(1)) Success(Quantity(0)) Failure(java.lang.IllegalArgumentException: amount(-1) < 0)
上記 sample.scala
では、以下を直接呼び出せないようにしてケースクラスの勝手なインスタンス化を防止しています。
- (a) コンストラクタ(new)
- (b) コンパニオンオブジェクトへデフォルト定義される apply
- (c) ケースクラスへデフォルト定義される copy
そのために、下記 2点を実施しています。
- (1) コンストラクタの private 化 : (a) の防止
- (2) ケースクラスの abstract 化 : (b) (c) の防止
(1) コンストラクタの private 化
以下のように private
を付ける事でコンストラクタを private 化できます。
コンストラクタの private 化
case class Quantity private (amount: Int)
これで (a) new Quantity(・・・)
の実行を防止できますが、以下のように (b) の apply や (c) の copy を実行できてしまいます。
検証例
scala> case class Quantity private (amount: Int) defined class Quantity scala> new Quantity(1) <console>:14: error: constructor Quantity in class Quantity cannot be accessed in object $iw new Quantity(1) scala> Quantity(1) res1: Quantity = Quantity(1) scala> Quantity.apply(2) res2: Quantity = Quantity(2) scala> Quantity(3).copy(30) res3: Quantity = Quantity(30)
(2) ケースクラスの abstract 化
ケースクラスを abstract
化すると、通常ならデフォルト定義されるコンパニオンオブジェクトの apply やケースクラスの copy を防止できるようです。
そのため、(1) と組み合わせることで (a) ~ (c) を防止できます。
ケースクラスの abstract 化とコンストラクタの private 化
sealed abstract case class Quantity private (amount: Int)
以下のように Quantity.apply
は定義されなくなります。
検証例
scala> sealed abstract case class Quantity private (amount: Int) defined class Quantity scala> new Quantity(1){} <console>:14: error: constructor Quantity in class Quantity cannot be accessed in <$anon: Quantity> new Quantity(1){} ^ scala> Quantity.apply(1) <console>:14: error: value apply is not a member of object Quantity Quantity.apply(1)
このままだと何もできなくなるため、実際はコンパニオンオブジェクトへ生成用の関数が必要になります。
例
sealed abstract case class Quantity private (amount: Int) object Quantity { def create(amount: Int): Quantity = new Quantity(amount){} }
備考
今回の方法は、以下の書籍に記載されているような ADTs(algebraic data types)と Smart constructors をより安全に定義するために活用できると考えています。
Functional and Reactive Domain Modeling
- 作者: Debasish Ghosh
- 出版社/メーカー: Manning Publications
- 発売日: 2016/10/24
- メディア: ペーパーバック
- この商品を含むブログを見る
quill で DDL を実行
quill は Scala 用の DB ライブラリで、マクロを使ってコンパイル時に SQL や CQL(Cassandra)を組み立てるのが特徴となっています。
quill には Infix
という機能が用意されており、これを使うと FOR UPDATE
のような(quillが)未サポートの SQL 構文に対応したり、select 文を直接指定したりできるようですが、CREATE TABLE
のような DDL(データ定義言語)の実行は無理そうでした。
そこで、API やソースを調べてみたところ、SQL を直接実行する probe
や executeAction
という関数を見つけたので、これを使って CREATE TABLE を実行してみたいと思います。
ソースは http://github.com/fits/try_samples/tree/master/blog/20180502/
はじめに
今回は Gradle を使ってビルド・実行し、DB には H2 を(インメモリーで)使います。
build.gradle
apply plugin: 'scala' apply plugin: 'application' mainClassName = 'sample.SampleApp' repositories { jcenter() } dependencies { compile 'org.scala-lang:scala-library:2.12.6' compile 'io.getquill:quill-jdbc_2.12:2.4.2' runtime 'com.h2database:h2:1.4.192' runtime 'org.slf4j:slf4j-simple:1.8.0-beta2' }
DB の接続設定は以下のようにしました。
ctx
の部分は任意の文字列を用いることができ、H2JdbcContext
を new する際の configPrefix 引数で指定します。
src/main/resources/application.conf
ctx.dataSourceClassName=org.h2.jdbcx.JdbcDataSource ctx.dataSource.url="jdbc:h2:mem:sample" ctx.dataSource.user=sa
1. probe・executeAction で DDL を実行
それでは、probe
と executeAction
をそれぞれ使って CREATE TABLE を実行してみます。
JdbcContext
における probe
の戻り値は Try[Boolean]
で executeAction
の戻り値は Long
となっています。
sample1/src/main/scala/sample/SampleApp.scala
package sample import io.getquill.{H2JdbcContext, SnakeCase} case class Item(itemId: String, name: String) case class Stock(stockId: String, itemId: String, qty: Int) object SampleApp extends App { lazy val ctx = new H2JdbcContext(SnakeCase, "ctx") import ctx._ // probe を使った CREATE TABLE の実行 val r1 = probe("CREATE TABLE item(item_id VARCHAR(10) PRIMARY KEY, name VARCHAR(10))") println(s"create table1: $r1") // executeAction を使った CREATE TABLE の実行 val r2 = executeAction("CREATE TABLE stock(stock_id VARCHAR(10) PRIMARY KEY, item_id VARCHAR(10), qty INT)") println(s"create table2: $r2") // item への insert println( run(query[Item].insert(lift(Item("item1", "A1")))) ) println( run(query[Item].insert(lift(Item("item2", "B2")))) ) // stock への insert println( run(query[Stock].insert(lift(Stock("stock1", "item1", 5)))) ) println( run(query[Stock].insert(lift(Stock("stock2", "item2", 3)))) ) // item の select println( run(query[Item]) ) // stock の select println( run(query[Stock]) ) // Infix を使った select val selectStocks = quote( infix"""SELECT stock_id AS "_1", name AS "_2", qty AS "_3" FROM stock s join item i on i.item_id = s.item_id""".as[Query[(String, String, Int)]] ) println( run(selectStocks) ) }
実行結果は以下の通りで CREATE TABLE に成功しています。
probe の結果は Success(false)
で executeAction の結果は 0
となりました。
実行結果
> cd sample1 > gradle run ・・・ [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. create table1: Success(false) create table2: 0 1 1 1 1 List(Item(item1,A1), Item(item2,B2)) List(Stock(stock1,item1,5), Stock(stock2,item2,3)) List((stock1,A1,5), (stock2,B2,3)) ・・・
2. モナドの利用
quill には IO
モナドが用意されていたので、これを使って処理を組み立ててみます。
IO は run
の代わりに runIO
を使う事で取得でき、IO の結果は performIO
で取得します。
probe の結果である Try[A]
は IO.fromTry
を使う事で IO にできます。
また、クエリー query[A]
では flatMap 等が使えるので for 内包表記で直接合成できましたが(selectStocks
の箇所)、query[A].insert(・・・)
は flatMap 等を使えなかったので runIO しています。(insertItemAndStock
の箇所)
sample2/src/main/scala/sample/SampleApp.scala
package sample import io.getquill.{H2JdbcContext, SnakeCase} case class Item(itemId: String, name: String) case class Stock(stockId: String, itemId: String, qty: Int) object SampleApp extends App { lazy val ctx = new H2JdbcContext(SnakeCase, "ctx") import ctx._ // CREATE TABLE val createTables = for { it <- probe("CREATE TABLE item(item_id VARCHAR(10) PRIMARY KEY, name VARCHAR(10))") st <- probe("CREATE TABLE stock(stock_id VARCHAR(10) PRIMARY KEY, item_id VARCHAR(10), qty INT)") } yield (it, st) // item と stock へ insert val insertItemAndStock = (itemId: String, name: String, stockId: String, qty: Int) => for { _ <- runIO( query[Item].insert(lift(Item(itemId, name))) ) _ <- runIO( query[Stock].insert(lift(Stock(stockId, itemId, qty))) ) } yield () // stock と item の select(stock と該当する item をタプル化) val selectStocks = quote { for { s <- query[Stock] i <- query[Item] if i.itemId == s.itemId } yield (i, s) } // 処理の合成 val proc = for { r1 <- IO.fromTry(createTables) _ <- insertItemAndStock("item1", "A1", "stock1", 5) _ <- insertItemAndStock("item2", "B2", "stock2", 3) r2 <- runIO(selectStocks) } yield (r1, r2) // 結果 println( performIO(proc) ) // トランザクションを適用する場合は以下のようにする //println( performIO(proc.transactional) ) }
実行結果
> cd sample2 > gradle run ・・・ [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting... [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. ((false,false),List((Item(item1,A1),Stock(stock1,item1,5)), (Item(item2,B2),Stock(stock2,item2,3)))) ・・・
Akka の FileIO でファイルを読み書き
Akka (akka-stream) の FileIO
を使ってファイルの読み書きを行ってみます。
今回のソースは http://github.com/fits/try_samples/tree/master/blog/20170131/
はじめに
akka-stream では Java 用の API は akka.stream.javadsl
パッケージに、Scala 用の API は akka.stream.scaladsl
パッケージに定義されています。
主要なクラスやメソッドは javadsl と scaladsl で概ね共通化されているようですが、Source
の recover
メソッドは deprecated の有無が違っていました。
Source の recover/recoverWith メソッドの deprecated 状況
パッケージ | deprecated 有り | deprecated 無し | 備考 |
---|---|---|---|
akka.stream.scaladsl | recoverWith | recover | ソースは akka/stream/scaladsl/Flow.scala |
akka.stream.javadsl | recover | recoverWith | ソースは akka/stream/javadsl/Source.scala |
ここで、deprecated の理由が @deprecated("Use recoverWithRetries instead.", "2.4.4")
のようなので、scaladsl の方が正しく、javadsl の方は deprecated するメソッドが違っているのだと思います ※。
※ recoverWith の代わりに recoverWithRetries を使えるが (引数が 1つ増えただけなので) recover は引数の型がそもそも違う
Scala の場合
まずは Scala で実装してみます。
ファイル用の Sink は FileIO.toPath
で、Source は FileIO.fromPath
で取得できます。
ファイルの入出力の型は akka.util.ByteString
となっており、以下では map を使って String との変換を行っています。
Source の recover
メソッドを使えば、例外が発生していた場合に代用の値へ差し替えて処理を繋げられます。
今回は、ファイルが存在しない等で IOException が発生した際に、"invalid file, ・・・" という文字列へ差し替えるために recover を使っています。
src/main/scala/SampleApp.scala
import akka.actor.ActorSystem import akka.stream.ActorMaterializer import akka.stream.scaladsl.{FileIO, Source} import akka.util.ByteString import java.nio.file.Paths import java.io.IOException object SampleApp extends App { implicit val system = ActorSystem() import system.dispatcher // ExecutionContext を implicit implicit val materializer = ActorMaterializer() // ファイルへの書き込み(sample1.txt へ "sample data" を出力) val res1 = Source.single("sample data") .map(ByteString.fromString) .runWith(FileIO.toPath(Paths.get("sample1.txt"))) // ファイルの読み込み(sample2.txt の内容を println) val res2 = FileIO.fromPath(Paths.get("sample2.txt")) .map(_.utf8String) .recover { case e: IOException => s"invalid file, ${e}" } .runForeach(println) res1.flatMap(_ => res2).onComplete(_ => system.terminate) }
ビルドと実行
Gradle のビルド定義ファイルは以下の通りです。
build.gradle
apply plugin: 'scala' apply plugin: 'application' mainClassName = 'SampleApp' repositories { jcenter() } dependencies { compile 'org.scala-lang:scala-library:2.12.1' compile 'com.typesafe.akka:akka-stream_2.12:2.5-M1' }
sample2.txt ファイルの無い状態で実行します。
実行結果1
> gradle -q run invalid file, java.nio.file.NoSuchFileException: sample2.txt
sample2.txt を作成して実行します。
実行結果2
> echo %time% > sample2.txt > gradle -q run 22:11:48.03
Java の場合
次に Java で実装してみます。
Scala と概ね同じですが、akka.stream.javadsl.Source でも recover
の引数が scala.PartialFunction
となっており、多少の工夫が必要です。
recover の引数に合う scala.PartialFunction は Akka の akka.japi.pf.PFBuilder
で作れるので、以下では Match.match
から PFBuilder を取得して使っています。
なお、Akka では scala.PartialFunction を組み立てるための Java 用の API が akka.japi.pf
パッケージにいくつか用意されています。
src/main/java/SampleApp.java
import akka.actor.ActorSystem; import akka.japi.pf.Match; import akka.japi.pf.PFBuilder; import akka.stream.ActorMaterializer; import akka.stream.Materializer; import akka.stream.javadsl.FileIO; import akka.stream.javadsl.Source; import akka.util.ByteString; import java.io.IOException; import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; public class SampleApp { public static void main(String... args) { final ActorSystem system = ActorSystem.create(); final Materializer materializer = ActorMaterializer.create(system); // ファイルへの書き込み(sample1.txt へ "sample data" を出力) CompletableFuture<?> res1 = Source.single("sample data") .map(ByteString::fromString) .runWith(FileIO.toPath(Paths.get("sample1.txt")), materializer) .toCompletableFuture(); // scala.PartialFunction のビルダー定義 PFBuilder<Throwable, String> pfunc = Match.match(IOException.class, e -> "invalid file, " + e); // ファイルの読み込み(sample2.txt の内容を println) CompletableFuture<?> res2 = FileIO.fromPath(Paths.get("sample2.txt")) .map(ByteString::utf8String) .recover(pfunc.build()) .runForeach(System.out::println, materializer) .toCompletableFuture(); CompletableFuture.allOf(res1, res2) .handle((v, e) -> system.terminate()) .join(); } }
ビルドと実行
Gradle のビルド定義ファイルは以下の通りです。
build.gradle
apply plugin: 'application' mainClassName = 'SampleApp' repositories { jcenter() } dependencies { compile 'com.typesafe.akka:akka-stream_2.12:2.5-M1' }
sample2.txt ファイルの無い状態で実行します。
実行結果1
> gradle -q run ・・・\src\main\java\SampleApp.javaは非推奨のAPIを使用またはオーバーライドしています。 ・・・ invalid file, java.nio.file.NoSuchFileException: sample2.txt
recover が deprecated されている件で警告メッセージが出力されますが、上述したように recover を deprecated しているのが誤りだと思われるので無視します。
sample2.txt を作成して実行します。
実行結果2
> echo %time% > sample2.txt > gradle -q run ・・・ 22:14:16.00
Gradle で ScalaPB を使う
ScalaPB であればビルドツールに sbt を使う方が簡単かもしれませんが、引き続き Gradle を使います。
- Gradle 3.0
- ScalaPB 0.5.40
今回作成したソースは http://github.com/fits/try_samples/tree/master/blog/20160905/
proto ファイル
前回と同じファイルですが、ファイル名に -
を含むと都合が悪いようなので ※ ファイル名だけ変えています。
※ ScalaPB 0.5.40 では、デフォルトで proto ファイル名が そのままパッケージ名の一部となりました (パッケージ名は <java_package オプションの値>.<protoファイル名> )
ちなみに、java_outer_classname
のオプション設定は無視されるようです。
proto/addressbook.proto (.proto ファイル)
syntax = "proto3"; package sample; option java_package = "sample.model"; option java_outer_classname = "AddressBookProtos"; message Person { string name = 1; int32 id = 2; string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phone = 4; } message AddressBook { repeated Person person = 1; }
Gradle ビルド定義
基本的な構成は前回と同じですが、ScalaPBC
を実行してソースを生成する等、Scala 用に変えています。
build.gradle
apply plugin: 'scala' apply plugin: 'application' // protoc によるソースの自動生成先 def protoDestDir = 'src/main/protoc-generated' // proto ファイル名 def protoFile = 'proto/addressbook.proto' mainClassName = 'SampleApp' repositories { jcenter() } configurations { scalapbc } dependencies { scalapbc 'com.trueaccord.scalapb:scalapbc_2.11:0.5.40' compile 'org.scala-lang:scala-library:2.11.8' compile 'com.trueaccord.scalapb:scalapb-runtime_2.11:0.5.40' } task scalapbc << { mkdir(protoDestDir) javaexec { main = 'com.trueaccord.scalapb.ScalaPBC' classpath = configurations.scalapbc args = [ protoFile, "--scala_out=${protoDestDir}" ] } } compileScala { dependsOn scalapbc source protoDestDir } clean { delete protoDestDir }
サンプルアプリケーション
こちらも前回と同じ処理内容ですが、ScalaPB 用の実装となっています。
src/main/scala/SampleApp.scala
import java.io.ByteArrayOutputStream import sample.model.addressbook.Person import Person.PhoneNumber import Person.PhoneType._ object SampleApp extends App { val phone = PhoneNumber("000-1234-5678", HOME) val person = Person(name = "sample1", phone = Seq(phone)) println(person) val output = new ByteArrayOutputStream() try { person.writeTo(output) println("----------") val restoredPerson = Person.parseFrom(output.toByteArray) println(restoredPerson) } finally { output.close } }
ビルドと実行
ビルドと実行の結果は以下の通りです。
前回と違って今回のビルド(scalapbc の実施)には python コマンドが必要でした。※
※ python コマンドを呼び出せるように環境変数 PATH 等を設定しておきます 今回は Python 2.7 を使用しました
実行結果
> gradle run ・・・ :scalapbc protoc-jar: protoc version: 300, detected platform: windows 10/amd64 protoc-jar: executing: [・・・\Temp\protoc8428481850206377506.exe, --plugin=protoc-gen-scala=・・・\Temp\protocbridge9000836851429371052.bat, proto/addressbook.proto, --scala_out=src/main/protoc-generated] :compileScala ・・・ :run name: "sample1" phone { number: "000-1234-5678" type: HOME } ---------- name: "sample1" phone { number: "000-1234-5678" type: HOME } BUILD SUCCESSFUL
scalapbc タスクの実行によって以下のようなソースが生成されました。
- src/main/protoc-generated/sample/model/addressbook/AddressBook.scala
- src/main/protoc-generated/sample/model/addressbook/AddressbookProto.scala
- src/main/protoc-generated/sample/model/addressbook/Person.scala
java_package
オプションの設定値は反映されていますが、java_outer_classname
オプション設定の方は無視されているようです。
Gradle と Querydsl Scala を使った Querydsl SQL のコード生成
前回 の JPA に続き、今回は Gradle と Querydsl Scala を使って Querydsl SQL のコード生成を試します。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150810/
はじめに
Querydsl SQL の場合は Scala も Java と同じ要領でコードを生成します。 (Java の場合は 「Gradle を使った Querydsl SQL のコード生成」 参照)
ただし、MetaDataExporter
へ Scala 用の serializerClass 等を設定する必要があります。
Gradle ビルド定義
Gradle 用のビルド定義ファイルは以下のようになります。
build.gradle
apply plugin: 'scala' // Querydsl のソース生成先パッケージ名 ext.modelPackage = 'sample.model' // Querydsl のソース生成先ディレクトリ ext.qdslDestDir = 'src/main/qdsl-generated' // DB接続 URL ext.dbUrl = 'jdbc:mysql://localhost:3306/jpa_sample?user=root' buildscript { repositories { jcenter() } dependencies { classpath 'com.querydsl:querydsl-sql-codegen:4.0.3' classpath 'com.querydsl:querydsl-scala:4.0.3' classpath 'org.scala-lang:scala-library:2.11.7' // MySQL へ接続してコード生成する場合 classpath 'mysql:mysql-connector-java:5.1.36' } } repositories { jcenter() } dependencies { compile 'com.querydsl:querydsl-scala:4.0.3' compile 'com.querydsl:querydsl-sql:4.0.3' compile 'org.scala-lang:scala-library:2.11.7' } // コード生成 task generate << { def con = new com.mysql.jdbc.Driver().connect(dbUrl, null) def exporter = new com.querydsl.sql.codegen.MetaDataExporter() exporter.packageName = modelPackage exporter.targetFolder = new File(qdslDestDir) exporter.serializerClass = com.querydsl.scala.sql.ScalaMetaDataSerializer exporter.typeMappings = com.querydsl.scala.ScalaTypeMappings.create() // Bean のコードも生成する場合は以下を有効化 //exporter.beanSerializerClass = com.querydsl.scala.ScalaBeanSerializer exporter.createScalaSources = true exporter.export(con.metaData) con.close() } compileScala { dependsOn generate sourceSets.main.scala.srcDir qdslDestDir } clean { delete qdslDestDir }
DB は MySQL を使用し、「JPA における一対多のリレーションシップ - EclipseLink」 で使ったものと同じテーブルを使用します。
使用する DB のテーブル定義 (DDL)
CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL, `price` decimal(10,0) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `product_variation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_id` bigint(20) NOT NULL DEFAULT 0, `color` varchar(10) NOT NULL, `size` varchar(10) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
サンプルアプリケーション1
それでは簡単なサンプルアプリケーションを作成し実行してみます。
ビルド定義
先程の build.gradle へ少しだけ手を加え、サンプルアプリケーション sample.SampleApp
を実行するようにしました。
build.gradle
apply plugin: 'scala' apply plugin: 'application' ext.modelPackage = 'sample.model' ext.qdslDestDir = 'src/main/qdsl-generated' ext.dbUrl = 'jdbc:mysql://localhost:3306/jpa_sample?user=root' mainClassName = 'sample.SampleApp' buildscript { ・・・ } ・・・ dependencies { compile 'com.querydsl:querydsl-scala:4.0.3' compile 'com.querydsl:querydsl-sql:4.0.3' compile 'org.scala-lang:scala-library:2.11.7' runtime 'mysql:mysql-connector-java:5.1.36' runtime 'org.slf4j:slf4j-nop:1.7.12' } task generate << { def con = new com.mysql.jdbc.Driver().connect(dbUrl, null) def exporter = new com.querydsl.sql.codegen.MetaDataExporter() exporter.packageName = modelPackage exporter.targetFolder = new File(qdslDestDir) exporter.serializerClass = com.querydsl.scala.sql.ScalaMetaDataSerializer exporter.typeMappings = com.querydsl.scala.ScalaTypeMappings.create() //exporter.beanSerializerClass = com.querydsl.scala.ScalaBeanSerializer exporter.createScalaSources = true exporter.export(con.metaData) con.close() } ・・・
generate タスクを実行すると以下のファイルが生成されます。
- src/main/qdsl-generated/sample/model/QProduct.scala
- src/main/qdsl-generated/sample/model/QProductVariation.scala
実行クラス
単純な insert・select 処理を実装しました。
src/main/scala/sample/SampleApp.scala
package sample import com.querydsl.sql.dml.SQLInsertClause import com.querydsl.sql.{SQLQuery, MySQLTemplates} import java.util.Properties import java.sql.DriverManager import scala.collection.JavaConversions._ import sample.model.{QProduct, QProductVariation} object SampleApp extends App { val conf = new Properties() conf.load(getClass.getClassLoader.getResourceAsStream("db.properties")) val con = DriverManager.getConnection(conf.getProperty("url"), conf) con.setAutoCommit(false) val templates = new MySQLTemplates() val p = QProduct as "p" val v = QProductVariation as "v" // product へ insert val pid: Long = new SQLInsertClause(con, templates, p) .set(p.name, s"sample${System.currentTimeMillis()}") .set(p.price, 1500L) .executeWithKey(p.id) // product_variation へ insert new SQLInsertClause(con, templates, v) .set(v.productId, pid).set(v.color, "Green").set(v.size, "L").addBatch() .set(v.productId, pid).set(v.color, "Blue").set(v.size, "S").addBatch() .execute() con.commit() val query = new SQLQuery(con, templates) // product と product_variation を join して select val res = query.from(p) .join(v).on(v.productId.eq(p.id)) .where(p.price.between(1300, 2500)) .select(p.id, p.name, p.price, v.color, v.size) .fetch() // id, name, price でグルーピング val groupedRes = res.groupBy(x => (x.get(p.id), x.get(p.name), x.get(p.price))) println(groupedRes) con.close() }
DB 接続設定ファイル
DB の接続設定に以下のプロパティファイルを使用します。
src/main/resources/db.properties
url=jdbc:mysql://localhost:3306/jpa_sample?characterEncoding=utf8 user=root password=
実行
実行結果は以下の通りです。
実行結果
> gradle run :compileJava UP-TO-DATE :generate :compileScala :processResources :classes :run Map((3,sample1439089472290,1500) -> ArrayBuffer([3, sample1439089472290, 1500, Green, L], [3, sample1439089472290, 1500, Blue, S]))
サンプルアプリケーション2
次は Bean を使ったサンプルです。
ビルド定義
exporter.beanSerializerClass = com.querydsl.scala.ScalaBeanSerializer
を有効化し、Bean のコード生成を行うようにしました。
build.gradle
apply plugin: 'scala' apply plugin: 'application' ext.modelPackage = 'sample.model' ext.qdslDestDir = 'src/main/qdsl-generated' ext.dbUrl = 'jdbc:mysql://localhost:3306/jpa_sample?user=root' mainClassName = 'sample.SampleApp2' buildscript { ・・・ } ・・・ dependencies { compile 'com.querydsl:querydsl-scala:4.0.3' compile 'com.querydsl:querydsl-sql:4.0.3' compile 'org.scala-lang:scala-library:2.11.7' compile 'org.apache.commons:commons-dbcp2:2.1.1' runtime 'mysql:mysql-connector-java:5.1.36' runtime 'org.slf4j:slf4j-nop:1.7.12' } task generate << { def con = new com.mysql.jdbc.Driver().connect(dbUrl, null) def exporter = new com.querydsl.sql.codegen.MetaDataExporter() exporter.packageName = modelPackage exporter.targetFolder = new File(qdslDestDir) exporter.serializerClass = com.querydsl.scala.sql.ScalaMetaDataSerializer exporter.typeMappings = com.querydsl.scala.ScalaTypeMappings.create() // Bean のコード生成を有効化 exporter.beanSerializerClass = com.querydsl.scala.ScalaBeanSerializer exporter.createScalaSources = true exporter.export(con.metaData) con.close() } ・・・
generate タスクを実行すると以下のファイルが生成されます。
- src/main/qdsl-generated/sample/model/Product.scala
- src/main/qdsl-generated/sample/model/ProductVariation.scala
- src/main/qdsl-generated/sample/model/QProduct.scala
- src/main/qdsl-generated/sample/model/QProductVariation.scala
実行クラス
処理内容は、サンプルアプリケーション1 と同じですが、com.querydsl.scala.sql.SQL
トレイトを使って Connection
を直接扱わなくても済むようにしています。
SQL
トレイトの tx
メソッドへ DB 処理 (insert や select 等) を渡します。
tx
では大まかに以下のような処理を実行するようです。
- (1)
DataSource
からConnection
取得 (setAutoCommit を false へ設定) - (2) 引数で渡した処理の実行
- (3) コミット or ロールバック
- (4)
Connection
のclose
なお、Bean を使って insert する場合は populate
メソッドを使います。
src/main/scala/sample/SampleApp2.scala
package sample import com.querydsl.scala.sql.SQL import com.querydsl.sql.{SQLTemplates, MySQLTemplates} import org.apache.commons.dbcp2.BasicDataSourceFactory import java.util.Properties import javax.sql.DataSource import scala.collection.JavaConversions._ import sample.model.{Product, ProductVariation, QProduct, QProductVariation} // com.querydsl.scala.sql.SQL トレイトの実装 case class QueryDSLHelper(dataSource: DataSource, templates: SQLTemplates) extends SQL object SampleApp2 extends App { val product = (name: String, price: Long) => { val res = new Product() res.name = name res.price = price res } val variation = (productId: Long, color: String, size: String) => { val res = new ProductVariation() res.productId = productId res.color = color res.size = size res } val conf = new Properties() conf.load(getClass.getClassLoader.getResourceAsStream("db.properties")) val dataSource = BasicDataSourceFactory.createDataSource(conf) val qdsl = QueryDSLHelper(dataSource, new MySQLTemplates()) val p = QProduct as "p" val v = QProductVariation as "v" qdsl.tx { // product へ insert val pid = qdsl.insert(p) .populate(product(s"test${System.currentTimeMillis()}", 2000L)) .executeWithKey(p.id) // product_variation へ insert qdsl.insert(v) .populate(variation(pid, "Red", "M")).addBatch() .populate(variation(pid, "Yellow", "F")).addBatch() .execute() } qdsl.tx { // product と product_variation を join して select val res = qdsl.from(p) .join(v).on(v.productId.eq(p.id)) .where(p.price.between(1300, 2500)) .select(p.id, p.name, p.price, v.color, v.size) .fetch() // id, name, price でグルーピング val groupedRes = res.groupBy(x => (x.get(p.id), x.get(p.name), x.get(p.price))) println(groupedRes) } }
DB 接続設定ファイル
commons-dbcp2 用のプロパティファイルを使いました。
src/main/resources/db.properties
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/jpa_sample?characterEncoding=utf8 username=root password=
実行
実行結果は以下の通りです。
実行結果
> gradle run :compileJava UP-TO-DATE :generate :compileScala :processResources :classes :run Map((3,sample1439089472290,1500) -> ArrayBuffer([3, sample1439089472290, 1500, Green, L], [3, sample1439089472290, 1500, Blue, S]), (4,test1439089637936,2000) -> ArrayBuffer([4, test1439089637936, 2000, Red, M], [4, test1439089637936, 2000, Yellow, F]))
Gradle と Querydsl Scala を使った Querydsl JPA のコード生成
Gradle と Querydsl Scala を使って Querydsl JPA の Scala 用コード生成を試してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150727/
はじめに
「Gradle を使った Querydsl JPA のコード生成」 ではアノテーションプロセッサを使って Querydsl JPA のコードを生成しましたが、Scala の場合は com.querydsl.codegen.GenericExporter
クラスを使うようです。
GenericExporter
でコード生成するには JPA のエンティティクラスをロードできなければなりません。 (つまり、エンティティクラスを事前にコンパイルしておく必要あり)
Gradle ビルド定義
エンティティクラスを Querydsl のコード生成前にコンパイルするため、今回は以下のようにエンティティクラスだけをコンパイルするタスク modelCompile
と Querydsl 用のコードを生成するタスク generate
を追加しました。
番号 | 概要 | タスク名 |
---|---|---|
(1) | エンティティクラスをコンパイル | modelCompile |
(2) | (1) のエンティティクラスを使って Querydsl JPA の Scala 用コードを生成 | generate |
(3) | (2) で生成したソースをビルド | compileScala |
(1) では src/main/scala-model
へ配置したエンティティクラスのソース (Scala) をビルドして build/classes/main
へ出力します。
(2) では com.querydsl.codegen.GenericExporter
を使って Scala 用の Querydsl JPA コードを src/main/qdsl-generated
へ生成します。
(3) で (2) の生成したソースをビルドできるように sourceSets.main.scala.srcDir
へ src/main/qdsl-generated
を追加しています。
なお、(2) で (1) のクラスをロードできるように buildscript
の classpath
へ build/classes/main
を追加しているのですが、これが原因で初回実行時や clean 直後は (1) と (2) を別々に実行する必要があります。
これは、build/classes/main
へクラスファイルが配置されていない状態 ((1) の実施前) で Gradle を実行すると given scan urls are empty. set urls in the configuration
とメッセージが出力され、以降のタスクで build/classes/main
をクラスパスとして認識しない事が原因です。
build.gradle
apply plugin: 'scala' // スキャン対象の JPA エンティティクラスのパッケージ名 ext.modelPackage = 'sample.model' // JPA エンティティクラスのソースディレクトリ ext.modelSourceDir = 'src/main/scala-model' // Querydsl のソース生成先ディレクトリ ext.qdslDestDir = 'src/main/qdsl-generated' buildscript { // JPA エンティティクラスのビルド結果の出力先ディレクトリ // buildscript の classpath へ設定する必要があるため、ここで定義している ext.destDir = "$buildDir/classes/main" repositories { jcenter() } dependencies { classpath 'com.querydsl:querydsl-codegen:4.0.2' classpath 'com.querydsl:querydsl-scala:4.0.2' classpath 'org.scala-lang:scala-library:2.11.7' classpath 'javax:javaee-api:7.0' // コード生成時に JPA エンティティクラスをロードさせるための設定 classpath files(destDir) } } repositories { jcenter() } dependencies { compile 'com.querydsl:querydsl-jpa:4.0.2' compile 'com.querydsl:querydsl-scala:4.0.2' compile 'org.scala-lang:scala-library:2.11.7' compile 'org.apache.commons:commons-dbcp2:2.1' compile 'javax:javaee-api:7.0' } // (1) JPA エンティティクラスをコンパイル task modelCompile(type: ScalaCompile) { // ソースディレクトリ source = modelSourceDir // クラスパスの設定 (buildscript のクラスパスを設定) classpath = buildscript.configurations.classpath // クラスファイルの出力先 destinationDir = file(destDir) // 以下が必須 (ファイル名やパスは何でも良さそう) scalaCompileOptions.incrementalOptions.analysisFile = file("${buildDir}/tmp/scala/compilerAnalysis/compileCustomScala.analysis") } // (2) Querydsl JPA の Scala 用コードを生成 task generate(dependsOn: 'modelCompile') { def exporter = new com.querydsl.codegen.GenericExporter() // コード生成先ディレクトリの設定 exporter.targetFolder = file(qdslDestDir) exporter.serializerClass = com.querydsl.scala.ScalaEntitySerializer exporter.typeMappingsClass = com.querydsl.scala.ScalaTypeMappings exporter.entityAnnotation = javax.persistence.Entity exporter.embeddableAnnotation = javax.persistence.Embeddable exporter.embeddedAnnotation = javax.persistence.Embedded exporter.skipAnnotation = javax.persistence.Transient exporter.supertypeAnnotation = javax.persistence.MappedSuperclass // Scala ソースの出力 exporter.createScalaSources = true // コード生成の実施 exporter.export(modelPackage) } // (3) ソースをビルド compileScala { // generate タスクとの依存設定 dependsOn generate // Querydsl のコード生成先ディレクトリを追加 sourceSets.main.scala.srcDir qdslDestDir } clean { delete qdslDestDir }
サンプルアプリケーション
それでは簡単なサンプルアプリケーションを作成し実行してみます。
ビルド定義
先程の build.gradle へ少しだけ手を加え、EclipseLink と MySQL を使ったサンプルアプリケーション sample.SampleApp
を実行するようにしました。
build.gradle
apply plugin: 'scala' apply plugin: 'application' ext.modelPackage = 'sample.model' ext.modelSourceDir = 'src/main/scala-model' ext.qdslDestDir = 'src/main/qdsl-generated' // 実行クラス mainClassName = 'sample.SampleApp' buildscript { ・・・ } ・・・ dependencies { compile 'com.querydsl:querydsl-scala:4.0.2' compile 'com.querydsl:querydsl-jpa:4.0.2' compile 'org.scala-lang:scala-library:2.11.7' compile 'org.apache.commons:commons-dbcp2:2.1' compile 'javax:javaee-api:7.0' // 実行用の依存ライブラリ runtime 'org.eclipse.persistence:eclipselink:2.6.1-RC1' runtime 'mysql:mysql-connector-java:5.1.36' runtime 'org.slf4j:slf4j-nop:1.7.12' } ・・・
JPA エンティティクラス
「JPA における一対多のリレーションシップ - EclipseLink」 で使った JPA エンティティクラスを Scala で実装し直しました。
src/main/scala-model/sample/model/Product.scala
package sample.model import javax.persistence._ import java.util.ArrayList import java.util.List import java.math.BigDecimal @Entity class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = _ var name: String = _ var price: BigDecimal = _ @OneToMany(fetch = FetchType.EAGER, cascade= Array(CascadeType.ALL)) @JoinColumn(name = "product_id") val variationList: List[ProductVariation] = new ArrayList() override def toString = s"Product(id: ${id}, name: ${name}, price: ${price}, variationList: ${variationList})" }
src/main/scala-model/sample/model/ProductVariation.scala
package sample.model import javax.persistence._ @Entity @Table(name = "product_variation") class ProductVariation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = _ var color: String = _ var size: String = _ override def toString = s"ProductVariation(id: ${id}, color: ${color}, size: ${size})" }
実行クラス
Querydsl JPA を使った単純な検索処理を行います。
src/main/scala/sample/SampleApp.scala
package sample import sample.model.Product import sample.model.ProductVariation import sample.model.QProduct import com.querydsl.jpa.impl.JPAQuery import javax.persistence.Persistence import java.math.BigDecimal import scala.collection.JavaConversions._ object SampleApp extends App{ def product(name: String, price: BigDecimal, variationList: ProductVariation*) = { val res = new Product() res.name = name res.price = price variationList.foreach(res.variationList.add) res } def variation(color: String, size: String) = { val res = new ProductVariation() res.color = color res.size = size res } val emf = Persistence.createEntityManagerFactory("jpa") val em = emf.createEntityManager() val tx = em.getTransaction() tx.begin() val p1 = product( "sample" + System.currentTimeMillis(), new BigDecimal(1250), variation("White", "L"), variation("Black", "M") ) em.persist(p1) tx.commit() val p = QProduct as "p" val query = new JPAQuery[Product](em) // Querydsl JPA による検索 val res = query.from(p).where(p.name.startsWith("sample")).fetch() // 結果の出力 res.foreach(println) em.close() }
実行
「JPA における一対多のリレーションシップ - EclipseLink」 で使った DB や JPA 設定ファイルを使って実行します。
src/main/resources/META-INF/persistence.xml
<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="jpa"> <class>sample.model.Product</class> <class>sample.model.ProductVariation</class> <properties> <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa_sample" /> <property name="javax.persistence.jdbc.user" value="root" /> <property name="eclipselink.logging.level" value="FINE" /> </properties> </persistence-unit> </persistence>
初回実行時や clean 直後は、modelCompile
と generate
以降のタスクを分けて実行する必要があります。 (上の方でも書きましたが buildscript
の classpath
へ build/classes/main
を設定している事が原因です)
エンティティクラスのコンパイル (modelCompile タスクの実行)
> gradle modelCompile given scan urls are empty. set urls in the configuration :modelCompile
以下のファイルが生成されます。
- src/main/qdsl-generated/sample/model/QProduct.scala
- src/main/qdsl-generated/sample/model/QProductVariation.scala
実行結果 (run タスクの実行)
> gradle run :compileJava UP-TO-DATE :modelCompile :generate :compileScala :processResources :classes :run ・・・ Product(id: 3, name: sample1, price: 100, variationList: [ProductVariation(id: 4, color: Black, size: M), ProductVariation(id: 5, color: White, size: L)]) Product(id: 4, name: sample1437821487341, price: 1250, variationList: [ProductVariation(id: 6, color: White, size: L), ProductVariation(id: 7, color: Black, size: M)])
Apache Spark でロジスティック回帰
以前 ※ に R や Julia で試したロジスティック回帰を Apache Spark の MLlib (Machine Learning Library) を使って実施してみました。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20150427/
※「 R でロジスティック回帰 - glm, MCMCpack 」、「 Julia でロジスティック回帰-glm 」
はじめに
R の時と同じデータを使いますが、ヘッダー行を削除しています。(「R でロジスティック回帰 - glm, MCMCpack」 参照)
データ data4a.csv
8,1,9.76,C 8,6,10.48,C 8,5,10.83,C ・・・
データ内容は以下の通り。個体 i それぞれにおいて 「 個の観察種子のうち生きていて発芽能力があるものは 個」 となっています。
項目 | 内容 |
---|---|
N | 観察種子数 |
y | 生存種子数 |
x | 植物の体サイズ |
f | 施肥処理 (C: 肥料なし, T: 肥料あり) |
体サイズ x
と肥料による施肥処理 f
が種子の生存する確率(ある個体 i から得られた種子が生存している確率)にどのように影響しているかをロジスティック回帰で解析します。
MLlib によるロジスティック回帰
今回は org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS
を使用します。
LogisticRegressionWithLBFGS について
LogisticRegressionWithLBFGS
で以前と同様のロジスティック回帰を実施するには以下が必要です。
setIntercept
で true を設定
この値が false (デフォルト値) の場合、結果の intercept 値が 0 になります。
なお、今回のように二項分布を使う場合は numClasses の値を変更する必要はありませんが (デフォルト値が 2 のため)、応答変数が 3状態以上の多項分布を使う場合は setNumClasses
で状態数に応じた値を設定します。
LabeledPoint について
LogisticRegressionWithLBFGS
へ与えるデータは LabeledPoint
で用意します。
R や Julia では 応答変数 ~ 説明変数1 + 説明変数2 + ・・・
のように応答変数と説明変数を指定しましたが、LabeledPoint
では下記のようにメンバー変数で表現します。
メンバー変数 | 応答変数・説明変数 |
---|---|
label | 応答変数 |
features | 説明変数 |
値は Double
とする必要がありますので、f 項目のような文字列値は数値化します。
更に、二項分布を使う場合 (numClasses = 2) は応答変数の値が 0 か 1 でなければなりません。
LabeledPoint への変換例
例えば、以下のようなデータを応答変数 y 項目、説明変数 x と f 項目の LabeledPoint
へ変換する場合
変換前のデータ (N = 8, y = 6)
8,6,10.48,C
次のようになります。
変換後のデータイメージ
LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 1.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 0.0, features: Vector(10.48, 0.0)) LabeledPoint(label: 0.0, features: Vector(10.48, 0.0))
8個(N)の中で 6個(y)生存していたデータのため、
label
(応答変数) の値が 1.0 (生存) のデータ 6個と 0.0 のデータ 2個へ変換します。
ちなみに、f 項目の値が C
の場合は 0.0、T
の場合は 1.0 としています。
実装
実装してみると以下のようになります。
LogisticRegression.scala
import org.apache.spark.SparkContext import org.apache.spark.mllib.classification.LogisticRegressionWithLBFGS import org.apache.spark.mllib.regression.LabeledPoint import org.apache.spark.mllib.linalg.Vectors object LogisticRegression extends App { // f項目の値を数値へ変換 val factor = (s: String) => s match { case "C" => 0 case _ => 1 } val sc = new SparkContext("local", "LogisticRegression") // データの準備 (100行のデータ -> 800個の LabeledPoint) val rdd = sc.textFile(args(0)).map(_.split(",")).flatMap { d => val n = d(0).toInt val x = d(1).toInt // 説明変数の値 val v = Vectors.dense(d(2).toDouble, factor(d(3))) // 応答変数が 1 のデータ x 個と 0 のデータ n - x 個を作成 List.fill(x)( LabeledPoint(1, v) ) ++ List.fill(n -x)( LabeledPoint(0, v) ) } // ロジスティック回帰の実行 val res = new LogisticRegressionWithLBFGS() // .setNumClasses(2) //省略可 .setIntercept(true) .run(rdd) println(res) }
ビルド
以下のような Gradle ビルド定義ファイルを使って実行します。
build.gradle
apply plugin: 'scala' apply plugin: 'application' mainClassName = 'LogisticRegression' repositories { jcenter() } dependencies { compile 'org.scala-lang:scala-library:2.11.6' compile('org.apache.spark:spark-mllib_2.11:1.3.1') { // ログ出力の抑制 exclude module: 'slf4j-log4j12' } // ログ出力の抑制 runtime 'org.slf4j:slf4j-nop:1.7.12' } run { if (project.hasProperty('args')) { args project.args.split(' ') } }
不要な WARN ログ出力を抑制するため以下のファイルも用意しました。
src/main/resources/log4j.properties
log4j.rootLogger=off
実行
実行結果は以下の通りです。
実行結果
> gradle run -Pargs=data4a.csv :clean :compileJava UP-TO-DATE :compileScala :processResources :classes :run (weights=[1.952347703282676,2.021401680901667], intercept=-19.535421113192506) BUILD SUCCESSFUL
以前に実施した R の結果 (Estimate の値) とほとんど同じ値になっています。
R の glm 関数による結果
Coefficients: Estimate Std. Error z value Pr(>|z|) (Intercept) -19.5361 1.4138 -13.82 <2e-16 *** x 1.9524 0.1389 14.06 <2e-16 *** fT 2.0215 0.2313 8.74 <2e-16 ***