Spring MVC で Controller を動的に切り替える - RequestMappingHandlerMapping のサブクラス利用

Spring MVC では、基本的に @RequestMapping アノテーションで指定した URL パターンに合致する Controller のメソッドを実行し、org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping クラスがその処理を担っています。

そこで、試しに RequestMappingHandlerMapping のサブクラスを使って、実行対象の Controller (@RequestMapping のパス違い) を動的に切り替えるようにしてみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150507/

はじめに

今回は、以下のように Query string (URL) の debug パラメータ有無によって、実行する Controller を切り替える処理を RequestMappingHandlerMapping のサブクラスで実現します。

URL 実行するメソッド
/sample/xxx SampleController.sample
/sample/xxx?debug DebugSampleController.sample

ただし、Controller を切り替えなくても他にやり様はいくらでもありますので、本件の実用性は低いと思います。

実装

Controller クラス

まずは Controller を 2つ用意します。

src/main/java/sample/controller/SampleController.java
package sample.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SampleController {
    @RequestMapping("/sample/{id}")
    @ResponseBody
    public String sample(@PathVariable("id") String id) {
        return "sample: " + id + ", " + System.currentTimeMillis();
    }
}

2つ目には @RequestMapping パスの先頭に /debug を付けました。

src/main/java/sample/controller/DebugSampleController.java
package sample.controller;
・・・
@Controller
public class DebugSampleController {
    @RequestMapping("/debug/sample/{id}")
    @ResponseBody
    public String sample(@PathVariable("id") String id) {
        return "debug-sample: " + id + ", " + new Date();
    }
}

RequestMappingHandlerMapping サブクラス

次に、本題の RequestMappingHandlerMapping サブクラスを実装します。

とりあえず、下記のように実装すれば別の Controller を呼び出せます。

  • (1) lookupHandlerMethod メソッドをオーラーライドし lookupPath を変更
  • (2) HttpServletRequest.getServletPath() の値を (1) に合わせて変更

(1) を実施しただけでは内部的なパスのチェック処理に引っかかるので (2) も合わせて実施する必要があります。

下記では、Query string へ debug が付いていた場合に Controller を選出するパス (lookupPath) の先頭に /debug を追加するように実装し、getServletPath() の戻り値を変更するために HttpServletRequestWrapper のサブクラスを使っています。

src/main/java/sample/mapping/SampleRequestMappingHandlerMapping.java
package sample.mapping;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class SampleRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
    // (1)
    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        return super.lookupHandlerMethod(
            changePath(lookupPath, request), 
            new SampleHttpServletRequest(request)
        );
    }

    // Controller を選出するパスを書き換える
    private String changePath(String path, HttpServletRequest request) {
        if (request.getParameter("debug") != null) {
            return "/debug" + path;
        }
        return path;
    }

    class SampleHttpServletRequest extends HttpServletRequestWrapper {
        public SampleHttpServletRequest(HttpServletRequest req) {
            super(req);
        }
        // (2) 
        @Override
        public String getServletPath() {
            return changePath(super.getServletPath(), this);
        }
    }
}

設定クラス

SampleRequestMappingHandlerMapping を適用するように Bean 定義を行います。

src/main/java/sample/config/WebConfig.java
package sample.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import sample.mapping.SampleRequestMappingHandlerMapping;

@Configuration
@EnableWebMvc
public class WebConfig {
    @Bean
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new SampleRequestMappingHandlerMapping();
    }
}

実行クラス

今回は Spring Boot を使って実行しますので、そのための実行クラスも用意します。

src/main/java/sample/Application.java
package sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {
    public static void main(String... args) {
        SpringApplication.run(Application.class, args);
    }
}

ビルド定義

Gradle のビルド定義ファイルは以下の通りです。 (Spring Boot 利用)

build.gradle
apply plugin: 'spring-boot'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:1.2.3.RELEASE'
}

実行

bootRun タスクを実行し、Tomcat 上で Web アプリケーションを起動しておきます。

起動
> gradle bootRun

・・・
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.3.RELEASE)
・・・
2015-05-03 16:43:04.306  INFO 4860 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-05-03 16:43:04.308  INFO 4860 --- [           main] sample.Application                       : Started Application in 5.001 seconds (JVM running for 6.5)

/sample/xxx/sample/xxx?debug へアクセスすると、?debug の有無で実行結果 (実行対象 Controller) が変化する事を確認できました。

動作確認
$ curl http://localhost:8080/sample/abc
sample: abc, 1430639107957

$ curl http://localhost:8080/sample/abc?debug
debug-sample: abc, Sun May 03 16:45:12 JST 2015

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_i } 個の観察種子のうち生きていて発芽能力があるものは { y_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 ***

Java 8 でグルーピング処理 - List<V> を Map<K, V> へ変換

Java 8 で List<V>Map<K, V> へ変換するようなグルーピング処理をいくつか試してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150420/

はじめに

今回は、下記をリスト化した List<Data>id でグルーピングして Map<String, Data> へ変換します。

class Data {
    private String id;
    private String name;

    public Data(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }
    ・・・
}

Java 8 より前のバージョンでは以下のようにすると思います。

拡張 for 利用
List<Data> dataList = Arrays.asList(
    new Data("d1", "sample1"),
    new Data("d2", "sample2"),
    new Data("d3", "sample3")
);

Map<String, Data> res = new HashMap<>();
for (Data d : dataList) {
    res.put(d.getId(), d);
}

また、Java 8 で Map<String, List<Data>> へ変換するなら Collectors.groupingBy を使うだけです。

groupingBy で Map<String, List > へ変換
Map<String, List<Data>> res = dataList.stream().collect(
    Collectors.groupingBy(Data::getId)
);

(1) forEach

まずは、拡張 for の変わりに forEach メソッドを使用する方法です。

Map<String, Data> res = new HashMap<>();
dataList.forEach(d -> res.put(d.getId(), d));

(2) toMap

次は、Collectors.toMap を使用する方法です。

toMap の 2引数版

Map<String, Data> res = dataList.stream().collect(
    Collectors.toMap(Data::getId, d -> d)
);

もしくは

Map<String, Data> res = dataList.stream().collect(
    Collectors.toMap(Data::getId, UnaryOperator.identity())
);

ここで、2引数版の toMap メソッドには以下のような注意点があります。

  • 同一キーを持つオブジェクトを複数含んでいると IllegalStateException を throw する

例えば、以下は IllegalStateException となります。

IllegalStateException が発生するコード例
List<Data> dataList2 = Arrays.asList(
    new Data("d1", "sample1"),
    new Data("d2", "sample2"),
    new Data("d3", "sample3"),
    new Data("d1", "sample1-b") // d1 が重複
);

// IllegalStateException: Duplicate key Data(d1, sample1) が発生
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d)
);
IllegalStateException エラー内容
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Data(d1, sample1)

IllegalStateException を発生させないようにするには、3引数版の toMap を使います。

toMap の 3引数版

第 3引数で同一キーの値が複数あった場合にどちらを選択するかを指定します。

最初の要素を採用する場合
// 結果 [ d1: Data(d1, sample1), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d, (d1, d2) -> d1)
);
最後の要素を採用する場合
// 結果 [ d1: Data(d1, sample1-b), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.toMap(Data::getId, d -> d, (d1, d2) -> d2)
);

(3) groupingBy + collectingAndThen

あまり実用的では無いと思いますが、groupingBy と collectingAndThen を組み合わせる方法も考えられます。

// 結果 [ d1: Data(d1, sample1), d2: Data(d2, sample2), d3: Data(d3, sample3) ]
Map<String, Data> res = dataList2.stream().collect(
    Collectors.groupingBy(
        Data::getId,
        Collectors.collectingAndThen(
            Collectors.toList(),
            a -> a.get(0) //最初の要素を採用
        )
    )
);

toList を minBy 等で代用する事も可能です。

Map<String, Data> res = dataList2.stream().collect(
    Collectors.groupingBy(
        Data::getId,
        Collectors.collectingAndThen(
            Collectors.minBy((a, b) -> 0),
            a -> a.get()
        )
    )
);

(4) collect の 3引数版

最後に、3引数版の collect を使う方法です。

パラレル実行で使用する第 3引数が必須となっている点が微妙だと思います。 (下記 Map::putAll の箇所を null にすると NullPointerException となります)

Map<String, Data> res = dataList.stream().collect(
    HashMap::new,
    (m, d) -> m.put(d.getId(), d),
    Map::putAll
);

Gradle を使った Querydsl SQL のコード生成

前々回前回 に続き、今回は Querydsl SQL のコード生成を Gradle で実施してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150413/

なお、Querydsl 4.0 からパッケージ名等が変更になるようなのでご注意ください。(github の master ブランチでは com.querydsl となっています)

Gradle を使ったコード生成

Querydsl SQL の場合は Querydsl JPA・MongoDB と違って DB からメタデータを取得してコードを生成します。

querydsl-sql-codegen モジュールを使用し、アノテーションプロセッサではなく Ant 用のタスクや API を使ってコードを生成します。

(a) Ant 用のタスクを使用

まずは Ant 用のタスククラス com.mysema.query.sql.ant.AntMetaDataExporter を Gradle から使ってコード生成してみます。

基本的に、DB への接続が発生するため JDBC ドライバーが必要となります。 また、コード生成のログを出力するには slf4j を使います。(slf4j を使わなくても支障はありません)

今回は MySQL を使いました。

build1.gradle
apply plugin: 'java'
// DB 接続 URL
def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root'
// コード生成先ディレクトリ
def qdslDestDir = 'src/main/qdsl-generated'
// 生成するコードの所属パッケージ
def qdslDestPackage = 'sample.model'

repositories {
    jcenter()
}

configurations {
    querydsl
}

dependencies {
    // コード生成用の依存モジュール定義
    querydsl 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3'
    querydsl 'mysql:mysql-connector-java:5.1.35'
    querydsl 'org.slf4j:slf4j-simple:1.7.12'

    compile 'com.mysema.querydsl:querydsl-sql:3.6.3'
}

task generate << {
    // Ant 用のタスク定義
    ant.taskdef(
        name: 'export', 
        classname: 'com.mysema.query.sql.ant.AntMetaDataExporter', 
        classpath: configurations.querydsl.asPath
    )

    // コード生成の実行
    ant.export(
        jdbcDriverClass: 'com.mysql.jdbc.Driver',
        dbUrl: qdslDbUrl,
        targetSourceFolder: qdslDestDir,
        targetPackage: qdslDestPackage
    )
}

compileJava {
    dependsOn generate
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

ビルド結果は以下の通りです。
事前に MySQL を起動し、テーブル等を作成しておきます。

ビルド例
> gradle -b build1.gradle build

:generate
[ant:export] [main] INFO com.mysema.query.sql.codegen.MetaDataExporter - Exported product successfully
[ant:export] [main] INFO com.mysema.query.sql.codegen.MetaDataExporter - Exported variation successfully
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

generate タスクの実行時に以下のようなソースコードが生成されました。

  • src/main/qdsl-generated/sample/model/QProduct.java
  • src/main/qdsl-generated/sample/model/QVariation.java

ちなみに、今回は以下のようなテーブルを用意しています。

DDL
CREATE TABLE `product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `variation` (
  `product_id` int(11) NOT NULL,
  `size` varchar(10) NOT NULL,
  `color` varchar(10) NOT NULL,
  PRIMARY KEY (`product_id`,`size`,`color`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(b) API を使用

次は、コード生成の APIcom.mysema.query.sql.codegen.MetaDataExporter クラス)を Gradle から直接使います。

MetaDataExporter クラスへ出力パッケージや出力ディレクトリなどを設定した後、export メソッドDatabaseMetaData を渡せばコードを生成します。

なお、今回は使っていませんが、com.mysema.query.sql.Configuration で特定のカラムの型 (コード生成の Java の型) を変更するような事も可能です。

build2.gradle
apply plugin: 'java'

def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root'
def qdslDestDir = 'src/main/qdsl-generated'
def qdslDestPackage = 'sample.model'

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        // コード生成用の依存モジュール定義
        classpath 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3'
        classpath 'mysql:mysql-connector-java:5.1.35'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'com.mysema.querydsl:querydsl-sql:3.6.3'
}

task generate << {
    // DB 接続
    def con = new com.mysql.jdbc.Driver().connect(qdslDbUrl, null)

    def exporter = new com.mysema.query.sql.codegen.MetaDataExporter()

    exporter.targetFolder = new File(qdslDestDir)
    exporter.packageName = qdslDestPackage
    // コード生成の実行
    exporter.export(con.metaData)

    con.close()
}

compileJava {
    dependsOn generate
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

ビルド結果は以下の通りです。

ビルド例
> gradle -b build2.gradle build

:generate
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

成果物は (a) のケースと同じです。

サンプルアプリケーション

最後に、簡単なサンプルアプリケーションを実装し実行してみます。

ビルド定義

(b) のビルド定義をベースに以下のようなビルド定義を用意しました。

build.gradle
apply plugin: 'application'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

def qdslDbUrl = 'jdbc:mysql://localhost:3306/sample1?user=root'
def qdslDestDir = 'src/main/qdsl-generated'
def qdslDestPackage = 'sample.model'

// 実行クラス
mainClassName = 'sample.App'

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.mysema.querydsl:querydsl-sql-codegen:3.6.3'
        classpath 'mysql:mysql-connector-java:5.1.35'
    }
}

repositories {
    jcenter()
}

dependencies {
    compile 'com.mysema.querydsl:querydsl-sql:3.6.3'
    // 実行用の依存モジュール定義
    runtime 'mysql:mysql-connector-java:5.1.35'
    runtime 'org.slf4j:slf4j-nop:1.7.12'
}

task generate << {
    def con = new com.mysql.jdbc.Driver().connect(qdslDbUrl, null)

    def exporter = new com.mysema.query.sql.codegen.MetaDataExporter()

    exporter.targetFolder = new File(qdslDestDir)
    exporter.packageName = qdslDestPackage

    exporter.export(con.metaData)

    con.close()
}

compileJava {
    dependsOn generate
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

アプリケーションクラス

product・variation テーブルへそれぞれデータを insert して select するだけの単純な処理を実装しました。

insert は set メソッドを使う方法と columnsvalues メソッドを使う方法を用いてみました。 (他にも Bean を populate する方法があります)

src/main/sample/App.java
package sample;

import com.mysema.query.sql.Configuration;
import com.mysema.query.sql.MySQLTemplates;
import com.mysema.query.sql.SQLQuery;
import com.mysema.query.sql.dml.SQLInsertClause;

import sample.model.QProduct;
import sample.model.QVariation;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class App {
    public static void main(String... args) throws SQLException {
        String dbUrl = "jdbc:mysql://localhost:3306/sample1?user=root";

        Configuration conf = new Configuration(new MySQLTemplates());

        QProduct p = QProduct.product;
        QVariation v = QVariation.variation;

        try (Connection con = DriverManager.getConnection(dbUrl)) {
            con.setAutoCommit(false);

            // product テーブルへレコードを insert
            Integer id = new SQLInsertClause(con, conf, p)
                    .set(p.name, "test" + System.currentTimeMillis())
                    .executeWithKey(p.id);

            // variation テーブルへレコードを insert
            new SQLInsertClause(con, conf, v)
                    .columns(v.productId, v.size, v.color)
                    .values(id, "L", "white")
                    .execute();

            // variation テーブルへレコードを insert
            new SQLInsertClause(con, conf, v)
                    .columns(v.productId, v.size, v.color)
                    .values(id, "S", "blue")
                    .execute();

            con.commit();

            // product と variation を join して select
            new SQLQuery(con, conf)
                    .from(p)
                    .join(v).on(v.productId.eq(p.id))
                    .where(p.name.startsWith("test"))
                    .list(p.id, p.name, v.color, v.size)
                    .forEach(System.out::println);
        }
    }
}

実行

実行結果は以下の通りです。

実行結果
> gradle run

・・・
:run
[1, test1428773665876, white, L]
[1, test1428773665876, blue, S]

BUILD SUCCESSFUL

Gradle を使った Querydsl MongoDB のコード生成

前回の Querydsl JPA に続き、今回は Querydsl MongoDB のコード生成を Gradle で実施してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150330/

Gradle を使ったコード生成

Querydsl MongoDB の場合は、前回 のビルド定義ファイルから依存モジュールとアノテーションプロセッサクラスを以下のように変えるだけです。 (querydsl-apt はそのまま使います)

  • querydsl-jpaquerydsl-mongodb へ変更
  • javaee-apimorphia へ変更
  • com.mysema.query.apt.jpa.JPAAnnotationProcessor を com.mysema.query.apt.morphia.MorphiaAnnotationProcessor へ変更

なお、エンティティクラスは Morphia のアノテーションを使って定義します。

(a) compileJava タスクでコード生成とコンパイルを実施

compileJava タスクの実行時に Querydsl のコード生成とコンパイルの両方を実施するタイプです。

build1.gradle
apply plugin: 'java'

repositories {
    jcenter()
}

configurations {
    apt
}

dependencies {
    apt 'com.mysema.querydsl:querydsl-apt:3.6.2'

    // Querydsl MongoDB 用の依存モジュール
    compile 'com.mysema.querydsl:querydsl-mongodb:3.6.2'
    compile 'org.mongodb.morphia:morphia:0.110'
}

compileJava {
    classpath += configurations.apt

    options.compilerArgs += [
        '-processor', 'com.mysema.query.apt.morphia.MorphiaAnnotationProcessor'
    ]
}

jar {
    excludes << '**/*.java'
}

ビルド結果は以下の通りです。

ビルド例
> gradle -b build1.gradle build

:compileJava
注意:Running MorphiaAnnotationProcessor
注意:Serializing Entity types
注意:Generating sample.model.QProduct for [sample.model.Product]
注意:Serializing Embeddable types
注意:Generating sample.model.QVariation for [sample.model.Variation]
注意:Running MorphiaAnnotationProcessor
注意:Running MorphiaAnnotationProcessor
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

自動生成されるソースの場所やクラス名の先頭に Q が付くのは前回と同じです。

(b) 別タスクでコード生成

Querydsl のコード生成を別タスク (下記の generate) で実施するタイプです。

Querydsl は IDE で利用すると思いますので、こちらの方が使い易いと思います。

build2.gradle
apply plugin: 'java'

def qdslDestDir = 'src/main/qdsl-generated'

repositories {
    jcenter()
}

configurations {
    apt
}

dependencies {
    apt 'com.mysema.querydsl:querydsl-apt:3.6.2'

    compile 'com.mysema.querydsl:querydsl-mongodb:3.6.2'
    compile 'org.mongodb.morphia:morphia:0.110'
}

task generate(type: JavaCompile) {

    source = sourceSets.main.java
    classpath = configurations.compile + configurations.apt

    destinationDir = new File(qdslDestDir)

    options.compilerArgs += [
        '-proc:only', 
        '-processor', 'com.mysema.query.apt.morphia.MorphiaAnnotationProcessor'
    ]
}

compileJava {
    dependsOn generate
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

ビルド結果は以下の通りです。

ビルド例
> gradle -b build2.gradle build

:generate
注意:Running MorphiaAnnotationProcessor
注意:Serializing Entity types
注意:Generating sample.model.QProduct for [sample.model.Product]
注意:Serializing Embeddable types
注意:Generating sample.model.QVariation for [sample.model.Variation]
注意:Running MorphiaAnnotationProcessor
注意:Running MorphiaAnnotationProcessor
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

サンプルアプリケーション

最後に、簡単なサンプルアプリケーションを実装し実行してみます。

ビルド定義

別タスクによるコード生成タイプ (b) のビルド定義をベースに以下のようなビルド定義を用意しました。

build.gradle
apply plugin: 'application'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

def qdslDestDir = 'src/main/qdsl-generated'

repositories {
    jcenter()
}

configurations {
    apt
}

dependencies {
    apt 'com.mysema.querydsl:querydsl-apt:3.6.2'

    compile 'com.mysema.querydsl:querydsl-mongodb:3.6.2'
    compile 'org.mongodb.morphia:morphia:0.110'
}

task generate(type: JavaCompile) {

    source = sourceSets.main.java
    classpath = configurations.compile + configurations.apt

    destinationDir = new File(qdslDestDir)

    options.compilerArgs += [
        '-proc:only',
        '-processor', 'com.mysema.query.apt.morphia.MorphiaAnnotationProcessor'
    ]
}

compileJava {
    dependsOn generate
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

mainClassName = 'sample.App'

エンティティクラス

Morphia 用のエンティティクラスとして以下を定義しました。

src/main/sample/model/Product.java
package sample.model;

import org.mongodb.morphia.annotations.*;
import org.bson.types.ObjectId;

import java.util.ArrayList;
import java.util.List;

@Entity
public class Product {
    @Id
    private ObjectId id;
    private String name;
    @Embedded
    private List<Variation> variationList = new ArrayList<>();

    public Product() {
    }

    public Product(String name) {
        setName(name);
    }

    public void setId(ObjectId id) {
        this.id = id;
    }

    public ObjectId getId() {
        return id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public List<Variation> getVariationList() {
        return variationList;
    }
}
src/main/sample/model/Variation.java
package sample.model;

import org.mongodb.morphia.annotations.Embedded;

@Embedded
public class Variation {
    private String size;
    private String color;

    public Variation() {
    }

    public Variation(String size, String color) {
        setSize(size);
        setColor(color);
    }

    public String getSize() {
        return size;
    }
    public void  setSize(String size) {
        this.size = size;
    }

    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
}

アプリケーションクラス

Product を保存した後に、Querydsl MongoDB の機能で簡単な検索を行う処理を実装しました。

src/main/sample/App.java
package sample;

import com.mongodb.MongoClient;
import com.mysema.query.mongodb.morphia.MorphiaQuery;
import org.mongodb.morphia.Datastore;
import org.mongodb.morphia.Morphia;
import sample.model.Product;
import sample.model.QProduct;
import sample.model.Variation;

import java.net.UnknownHostException;

public class App {
    public static void main(String... args) throws UnknownHostException {
        Morphia morphia = new Morphia();
        Datastore ds = morphia.createDatastore(new MongoClient(), "sample");

        Product p = new Product("test" + System.currentTimeMillis());

        p.getVariationList().add(new Variation("L", "black"));
        p.getVariationList().add(new Variation("M", "white"));

        ds.save(p);

        // Querydsl MongoDB を使った検索処理
        QProduct qp = QProduct.product;
        MorphiaQuery<Product> query = new MorphiaQuery<>(morphia, ds, qp);

        query.where(qp.name.like("test%")).list().forEach(App::printProduct);
    }

    private static void printProduct(Product p) {
        System.out.println("----------");

        System.out.println(p.getId() + ", " + p.getName());

        p.getVariationList().forEach(v ->
                System.out.println(v.getColor() + ", " + v.getSize()));

    }
}

実行

まず、MongoDB を起動しておきます。 今回は開発版の MongoDB 3.1.0 (SSL版) を使用しました。

MongoDB 起動
> mongod --dbpath data

・・・
2015-03-29T23:50:32.137+0900 I CONTROL  [initandlisten] ** NOTE: This is a development version (3.1.0) of MongoDB.
・・・
2015-03-29T23:50:32.685+0900 I NETWORK  [initandlisten] waiting for connections on port 27017

実行結果は以下の通りです。

実行結果
> gradle run

・・・
:run
3 29, 2015 11:52:47 午後 org.mongodb.morphia.logging.MorphiaLoggerFactory choose LoggerFactory
情報: LoggerImplFactory set to org.mongodb.morphia.logging.jdk.JDKLoggerFactory
----------
551811bfcc87cb6a0dcb43fd, test1427640767733
black, L
white, M

BUILD SUCCESSFUL

MongoDB shell を使って登録内容を確認してみます。

データ確認
> mongo
・・・
MongoDB shell version: 3.1.0
connecting to: test
Welcome to the MongoDB shell.
・・・

> use sample
switched to db sample

> db.Product.find()
{ "_id" : ObjectId("551811bfcc87cb6a0dcb43fd"), "className" : "sample.model.Product", "name" : "test1427640767733", "variationList" : [ { "size" : "L", "color"
: "black" }, { "size" : "M", "color" : "white" } ] }

Gradle を使った Querydsl JPA のコード生成

今回は Querydsl JPA のコード生成を Gradle で実施してみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150322/

はじめに

Querydsl JPA では JPA のエンティティクラスを元に Querydsl JPA 用のコードを自動生成して使います。

この場合のコード生成は querydsl-apt モジュールに含まれているアノテーションプロセッサ com.querydsl.apt.jpa.JPAAnnotationProcessor を javac のコンパイルオプションへ指定するだけです。

javac -processor com.mysema.query.apt.jpa.JPAAnnotationProcessor ・・・

Gradle の場合でも同様に上記コンパイルオプションを使用します。

(a) compileJava タスクでコード生成とコンパイルを実施

まずは、compileJava タスクの実行時に Querydsl のコード生成とコンパイルの両方を実施してみます。

下記 (1) のように compileJava の options.compilerArgsコンパイルオプション -processor com.mysema.query.apt.jpa.JPAAnnotationProcessor を追加します。

こうすることで、src/main/java 内の JPA エンティティクラスを元に destinationDir プロパティ値のディレクトリ (デフォルトでは build/classes/main) へ Querydsl JPA 用のソースファイルを自動生成し、コンパイルを実施します。

デフォルトでは destinationDir 内の全ファイルを JAR ファイルへ格納してしまいますので、下記の (2) では jar タスクの excludes を使って Querydsl が生成したソースファイルを除外するようにしています。

build.gradle
apply plugin: 'java'

repositories {
    jcenter()
}

configurations {
    apt
}

dependencies {
    // Querydsl JPA のコード生成モジュール
    apt 'com.mysema.querydsl:querydsl-apt:3.6.2'

    compile 'com.mysema.querydsl:querydsl-jpa:3.6.2'
    compile 'javax:javaee-api:7.0'
}

compileJava {
    classpath += configurations.apt

    // (1) アノテーションプロセッサの利用設定
    options.compilerArgs += [
        '-processor', 'com.mysema.query.apt.jpa.JPAAnnotationProcessor'
    ]
}

jar {
    // (2) 自動生成したソースファイルを jar ファイルから除外するための設定
    excludes << '**/*.java'
}

ビルド結果は以下の通りです。

ビルド例
> gradle build

:compileJava
注意:Running JPAAnnotationProcessor
注意:Serializing Entity types
注意:Generating sample.model.QProduct for [sample.model.Product]
注意:Running JPAAnnotationProcessor
注意:Running JPAAnnotationProcessor
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

JPA 用のエンティティクラス src/main/java/sample/model/Product.java を元に build/classes/main/sample/model/QProduct.java ファイルを生成し、ビルドを行っています。

ビルド結果の JAR ファイルは以下のような内容になります。

JAR ファイルの内容例
  • sample/model
    • Product.class
    • QProduct.class
  • META-INF
    • MANIFEST.MF

(b) 別タスクでコード生成

次は、Querydsl のコード生成を別タスク (下記の generate) で実施するようにしてみます。

javac のコンパイルオプションで -proc:only とすれば、コンパイルを実施せずアノテーションプロセッサだけを実施するようになりますので、これを利用します。

JavaCompile を type へ指定したタスクを定義して source・classpath・destinationDir (コード生成の出力先) を設定し、コンパイルオプションへ -proc:only を追加します。

あとは compileJava タスクのコンパイル対象ソースへ Querydsl コード生成先ディレクトリを追加するだけです。 (タスクの依存設定は必要に応じて実施)

build2.gradle
apply plugin: 'java'
// コード生成先
def qdslDestDir = 'src/main/qdsl-generated'

repositories {
    jcenter()
}

configurations {
    apt
}

dependencies {
    apt 'com.mysema.querydsl:querydsl-apt:3.6.2'

    compile 'com.mysema.querydsl:querydsl-jpa:3.6.2'
    compile 'javax:javaee-api:7.0'
}

// Querydsl JPA コード生成用のタスク定義
task generate(type: JavaCompile) {

    source = sourceSets.main.java
    classpath = configurations.compile + configurations.apt
    // コード生成の出力先を設定
    destinationDir = new File(qdslDestDir)

    options.compilerArgs += [
        '-proc:only', // アノテーションプロセッサのみ実施するための設定 (コンパイルしない)
        '-processor', 'com.mysema.query.apt.jpa.JPAAnnotationProcessor'
    ]
}

compileJava {
    dependsOn generate
    // コンパイル対象ソースへコード生成先ディレクトリを追加
    sourceSets.main.java.srcDir qdslDestDir
}

clean {
    delete qdslDestDir
}

ビルド結果は以下の通りです。

アノテーションプロセッサを compileJava タスクの前に実行している以外は先程の (a) と同じです。 (当然ながら JAR ファイルの内容も同じです)

ビルド例
> gradle -b build2.gradle build

:generate
注意:Running JPAAnnotationProcessor
注意:Serializing Entity types
注意:Generating sample.model.QProduct for [sample.model.Product]
注意:Running JPAAnnotationProcessor
注意:Running JPAAnnotationProcessor
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

Julia でロジスティック回帰 - glm

前回 に続き、今回も Julia で GLM を実施します。

今回は 「R でロジスティック回帰 - glm, MCMCpack」 のロジスティック回帰(GLM)を Julia で実装してみました。

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150309/

はじめに

GLM 等のパッケージは 前回 と同じものを使用します。

データは R で試した時のものをそのまま使います。(「R でロジスティック回帰 - glm, MCMCpack」 参照)

データ data4a.csv
N,y,x,f
8,1,9.76,C
8,6,10.48,C
8,5,10.83,C
・・・

データの内容は以下の通りで、個体 i それぞれにおいて 「 { N_i } 個の観察種子のうち生きていて発芽能力があるものは { y_i } 個」 となっています。

項目 内容
N 観察種子数
y 生存種子数
x 植物の体サイズ
f 施肥処理 (C: 肥料なし, T: 肥料あり)

体サイズ x と肥料による施肥処理 f が種子の生存する確率(ある個体 i から得られた種子が生存している確率)にどのように影響しているかをロジスティック回帰で解析します。

GLM によるロジスティック回帰

Julia の GLM パッケージを使う場合の注意点は下記の通りです。

  • (1) Binomial (二項分布) を使う場合は応答変数(下記の y に該当)の値が 0.0 ~ 1.0 内でなければならない
  • (2) readtable ではカテゴリ型変数 (R の因子型に該当) 化を実施しない

(1) への対応として、y を N で割った値 yn を応答変数(目的変数)として使いました。 今のところ d[:yn] = d[:y] / d[:N] とは書けないようなので map 関数を使っています。

R の read.csv 関数等では f のような項目は因子型になると思いますが、Julia の readtable では (2) のようにカテゴリ型変数とはならず単なる文字列の配列となります。 (d[:f] の型は DataArrays.DataArray{UTF8String,1} になる)

glm 関数で文字列の変数は使えないので、カテゴリ型変数への変換が必要となります。

DataFrames パッケージにはカテゴリ型への変換関数 pool が用意されているので、下記では f 項目の値を pool 関数で変換し ff へ設定しています。 (d[:ff] の型は DataArrays.PooledDataArray{UTF8String,UInt8,1} になる)

logisticGlm.jl
using DataFrames, GLM

d = readtable("data4a.csv")

# (1) 生存率 y / N の算出
d[:yn] = map(x -> d[:y][x] / d[:N][x], 1:nrow(d))

# (2) カテゴリ型へ変換 (DataArrays.PooledDataArray{UTF8String,UInt8,1} になる)
d[:ff] = pool(d[:f])

# 以下でも可 (ただし、DataArrays.PooledDataArray{UTF8String,UInt32,1} になる)
# d[:ff] = convert(PooledDataArray, d[:f])

res = glm(yn~x + ff, d, Binomial())

println(res)

実行結果は以下のようになりました。 (R で実施したときと同じ Estimate 値)

実行結果
> julia logisticGlm.jl

DataFrames.DataFrameRegressionModel{GLM.GeneralizedLinearModel{GLM.GlmResp{Array{Float64,1},Distributions.Binomial,GLM.LogitLink},GLM.DensePredChol{Float64}},Float64}:

Coefficients:
             Estimate Std.Error  z value Pr(>|z|)
(Intercept)  -19.5361   3.99861 -4.88572    <1e-5
x             1.95241  0.392777  4.97077    <1e-6
ff - T        2.02151  0.654152  3.09027   0.0020

ちなみに、d[:f] の内容と poolconvert 関数の適用結果は以下のようになります。

julia> d[:f]

100-element DataArrays.DataArray{UTF8String,1}:
 "C"
 "C"
 ・・・
 "T"
 "T"
julia> pool(d[:f])

100-element DataArrays.PooledDataArray{UTF8String,UInt8,1}:
 "C"
 "C"
 ・・・
 "T"
 "T"
julia> convert(PooledDataArray, d[:f])

100-element DataArrays.PooledDataArray{UTF8String,UInt32,1}:
 "C"
 "C"
 ・・・
 "T"
 "T"

PooledDataArray

PooledDataArray の水準名の一覧は levels 関数か pool フィールドを使って取得できます。

julia> levels(d[:ff])

2-element Array{UTF8String,1}:
 "C"
 "T"
julia> d[:ff].pool

2-element Array{UTF8String,1}:
 "C"
 "T"

水準名と数値のマッピングlevelsmap 関数で取得できます。

julia> levelsmap(d[:ff])

Dict{UTF8String,Int64} with 2 entries:
  "T" => 2
  "C" => 1

また、refs フィールドで各データに割り当てられた数値を取得できます。

julia> d[:ff].refs

100-element Array{UInt8,1}:
 0x01
 0x01
 ・・・
 0x02
 0x02

予測線の描画

次に glm の結果 (DataFrameRegressionModel) を使って予測腺を描画します。

glm の結果では、「肥料あり "T" => 1、肥料なし "C" => 0」 の扱いになっているので、predict する際に肥料ありは 1 を肥料なしは 0 の配列を渡します。

predict の結果は生存率 yn の予測値なので、グラフへ描画する際に観察種子数 N の最大値 (今回は全て 8 なので最大値とする必要はない) を乗算しています。

なお、rep 関数を使うと指定の配列を指定回数繰り返した配列を作成できます。

logisticGlm_draw.jl
using DataFrames, GLM, Gadfly
・・・
res = glm(yn~x + ff, d, Binomial())

# x の最小値 7.66 ~ 最大値 12.44 まで 0.1 刻みのデータを用意
xx = [minimum(d[:x]):0.1:maximum(d[:x])]

# 肥料あり "T" の予測値を算出
rt = predict(res, DataFrame(n = rep([1], length(xx)), x = xx, ff = rep([1], length(xx))))
# 肥料なし "C" の予測値を算出
rc = predict(res, DataFrame(n = rep([1], length(xx)), x = xx, ff = rep([0], length(xx))))

p = plot(
    layer(d, x = "x", y = "y", color = "f", Geom.point),
    # 肥料あり "T" の予測線を赤で描画
    layer(x = xx, y = maximum(d[:N]) * rt, Geom.line, Theme(default_color = color("red"))),
    # 肥料なし "C" の予測線を緑で描画
    layer(x = xx, y = maximum(d[:N]) * rc, Geom.line, Theme(default_color = color("green")))
)

draw(PNG("logisticGlm_draw.png", 500px, 400px), p)
実行結果

f:id:fits:20150309012215p:plain

なお、rt と rc の算出処理を PooledDataArray を使って実装すると以下のようになります。

PooledDataArray では "T" => 2, "C" => 1 となっているため、"T" => 1, "C" => 0 でそれぞれ predict するように <PooledDataArray変数>.refs - 1 としています。

・・・
xx = [minimum(d[:x]):0.1:maximum(d[:x])]

# 肥料あり "T" のカテゴリ型データを作成 (d[:ff] の水準を使用)
ft = PooledDataArray(rep([utf8("T")], length(xx)), d[:ff].pool)
# 肥料なし "C" のカテゴリ型データを作成 (d[:ff] の水準を使用)
fc = PooledDataArray(rep([utf8("C")], length(xx)), d[:ff].pool)

# 肥料あり "T" の予測値を算出
rt = predict(res, DataFrame(n = rep([1], length(xx)), x = xx, ff = ft.refs - 1))
# 肥料なし "C" の予測値を算出
rc = predict(res, DataFrame(n = rep([1], length(xx)), x = xx, ff = fc.refs - 1))
・・・