読者です 読者をやめる 読者になる 読者になる

nginx でリバースプロキシする際は HTTP レスポンスヘッダーのサイズに注意

nginx で Web サーバーをリバースプロキシする際は以下に注意が必要です。 (nginx 1.8.0 と 1.9.4 で確認)

  • リバースプロキシ先からの HTTP レスポンスヘッダーのサイズが proxy_buffer_size の設定値を超えると 502 Bad Gateway エラーとなる

その場合のエラーログは次の通りです。

エラーログ例
2015/08/24 00:34:03 [error] 3672#4680: *6 upstream sent too big header while reading response header from upstream, ・・・

このエラーが発生した場合は、proxy_buffer_size の値をレスポンスヘッダーのサイズより大きくする必要があります。

proxy_buffer_size のデフォルト値は 4KB か 8KB に設定されているようですので、通常の Web アプリケーションでお目にかかる事はないかもしれません。

また、proxy_buffering の値 (on / off) に関わらず発生します。

nginx ソース確認

実際にどうなっているのか nginx 1.9.4 のソースを見てみました。

src/http/ngx_http_upstream.c の ngx_http_upstream_process_header 関数で upstream sent too big header のエラーログを出力しています。

受信したレスポンスヘッダーがバッファに収まりきらなかった ※ 場合に upstream sent too big header のログを出力しエラーとしているようです。

 ※ レスポンスヘッダーを受信し終わっていないのにバッファが終端に到達
src/http/ngx_http_upstream.c の該当ソース
static void
ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
    ・・・
    if (u->buffer.start == NULL) {
        u->buffer.start = ngx_palloc(r->pool, u->conf->buffer_size);
        ・・・

        u->buffer.pos = u->buffer.start;
        u->buffer.last = u->buffer.start;
        u->buffer.end = u->buffer.start + u->conf->buffer_size;
        ・・・
    }

    for ( ;; ) {

        n = c->recv(c, u->buffer.last, u->buffer.end - u->buffer.last);

        ・・・

        rc = u->process_header(r);

        if (rc == NGX_AGAIN) {

            if (u->buffer.last == u->buffer.end) {
                ngx_log_error(NGX_LOG_ERR, c->log, 0,
                              "upstream sent too big header");

                ngx_http_upstream_next(r, u,
                                       NGX_HTTP_UPSTREAM_FT_INVALID_HEADER);
                return;
            }

            continue;
        }

        break;
    }
    ・・・
}

また、上記で使用しているバッファサイズ u->conf->buffer_sizeproxy_buffer_size の設定値を使用していると思われます。

src/http/modules/ngx_http_proxy_module.c の該当ソース
・・・
{ ngx_string("proxy_buffer_size"),
  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
  ngx_conf_set_size_slot,
  NGX_HTTP_LOC_CONF_OFFSET,
  offsetof(ngx_http_proxy_loc_conf_t, upstream.buffer_size),
  NULL },
・・・

検証

最後に、簡単なサンプル Web アプリケーションを作って検証してみました。

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

nginx 設定ファイル

proxy_buffer_size を 1KB に減らし、http://127.0.0.1:8081 へリバースプロキシを行う設定 (以下) で nginx を起動しておきます。

nginx.conf
events {
}

http {
    upstream ap {
        server 127.0.0.1:8081;
    }

    server {
        listen 8080;

        location / {
            proxy_pass http://ap;
            proxy_buffer_size 1k;
        }
    }
}

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

次のような単純な Web サーバー (Undertow を使用) を実行するスクリプトを用意しました。

  • レスポンスヘッダー TEST へ設定する t の文字数を実行時引数で指定 (例えば 't' * 5 の結果は ttttt
  • レスポンスボディに sample という文字列を返す
server_sample.groovy
@Grab('io.undertow:undertow-core:1.3.0.Beta9')
import io.undertow.Undertow
import io.undertow.server.HttpHandler
import io.undertow.util.Headers
import io.undertow.util.HttpString

// t の文字数
def size = args[0] as int

def server = Undertow.builder().addListener(8081, 'localhost').setHandler( { ex ->
    // レスポンスヘッダー
    ex.responseHeaders
        .put(Headers.CONTENT_TYPE, 'text/plain')
        .put(new HttpString('TEST'), 't' * size)

    // レスポンスボディ
    ex.responseSender.send('sample')

} as HttpHandler ).build()

server.start()

上記スクリプトが返すレスポンスヘッダーは以下のようになります。

レスポンスヘッダー例
$ curl -I http://localhost:8081/

HTTP/1.1 200 OK
TEST: ttttttttttttttttttttttt・・・
Connection: keep-alive
Content-Type: text/plain
Content-Length: 6
Date: Mon, 24 Aug 2015 06:04:08 GMT

動作検証1

まずは、t の数 800 で server_sample.groovy を実行してみます。

レスポンスヘッダーが 1KB を超えないはずなので正常に結果が返ってくるはずです。

> groovy server_sample.groovy 800
・・・

nginx へアクセスしてみると問題なく sample という文字列が返ってきました。

$ curl http://localhost:8080/

sample

動作検証2

次は 1100 で実行してみます。

1KB を超えるので 502 エラーとなるはずです

> groovy server_sample.groovy 1100
・・・

nginx へアクセスしてみると想定通り 502 Bad Gateway が返ってきました。

$ curl http://localhost:8080/

<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.9.4</center>
</body>
</html>

Spring Data Redis におけるデフォルト設定の注意点

Java Spring Redis

Spring Data Redis のデフォルト設定に関して、個人的に気になった点を挙げておきます。

  • (1) キーと値に JdkSerializationRedisSerializer を適用
  • (2) トランザクションサポートが無効化 (enableTransactionSupport = false)

今回使用したモジュールは以下。

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

はじめに

今回作成したサンプルの Gradle 用ビルド定義です。

spring-boot-gradle-plugin を使わず、gradle run で実行するようにしました。

build.gradle
apply plugin: 'application'

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

mainClassName = 'sample.App'

repositories {
    jcenter()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-redis:1.2.5.RELEASE'
}

クラス構成は以下の通りです。

実行クラス src/main/java/sample/App.java
package sample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import sample.repository.SampleRepository;

@ComponentScan
@EnableAutoConfiguration
public class App implements CommandLineRunner {
    @Autowired
    private SampleRepository sampleRepository;

    @Override
    public void run(String... args) {
        ・・・
    }

    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}
設定クラス src/main/java/sample/config/AppConfig.java
package sample.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class AppConfig {
    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public RedisTemplate<String, Integer> redisTemplate() {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        ・・・
        return template;
    }
}
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
package sample.repository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Repository;

import java.util.function.IntUnaryOperator;

@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    public Integer load(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    ・・・
}

(1) キーと値に JdkSerializationRedisSerializer を適用

デフォルト設定では、キーにも値にも JdkSerializationRedisSerializer を適用するため、Javaシリアライズしたキーと値を Redis へ保存します。

例えば、redisTemplate.opsForValue().set("a1", 10)redisTemplate.boundValueOps("a1").set(10) でも可) のように "a1" というキーに 10 という値をセットした後、Redis で実際の値を確認すると以下のようになりました。

redis-cli による確認結果1
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x02a1"

127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x02a1"
"\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\n"

"a1" が "\xac\xed\x00\x05t\x00\x02a1" に、 10 が "\xac\xed\・・・valuexr\x00\x10java.lang.Number・・・" になって保存されています。

文字列や数値をシリアライズせずに Redis へ保存したい場合は、RedisTemplate の個々の serializer を変更します。

今回は以下のように設定しました。

適用する serializer 設定先プロパティ
String StringRedisSerializer keySerializer
Integer GenericToStringSerializer valueSerializer

他にも、ハッシュを使う場合は hashKeySerializer や hashValueSerializer を変更し、同じ serializer を全てに適用する場合は defaultSerializer を変更します。

設定クラス src/main/java/sample/config/AppConfig.java
・・・
@Configuration
public class AppConfig {
    ・・・
    @Bean
    public RedisTemplate<String, Integer> redisTemplate() {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());

        // キーの serializer を変更
        template.setKeySerializer(new StringRedisSerializer());
        // 値の serializer を変更
        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));

        return template;
    }
}

serializer 変更後に redisTemplate.opsForValue().set("a1", 10) で保存した内容は以下のようになりました。

redis-cli による確認結果2 (selializer 変更後)
127.0.0.1:6379> keys *
1) "a1"

127.0.0.1:6379> get a1
"10"

(2) トランザクションサポートが無効化

デフォルトで RedisTemplateenableTransactionSupportfalse となっています。

この場合、以下のように RedisTemplatemultiexec メソッドを使用しても Redis のトランザクションは適用されず、exec の実行時に JedisDataException: ERR EXEC without MULTI エラーが発生します。

// 楽観ロックの適用
redisTemplate.watch(key);
・・・
// Redis トランザクションの開始
redisTemplate.multi();
・・・
// Redis トランザクションの実施。enableTransactionSupport = false の場合はエラー
redisTemplate.exec();

エラーが発生する原因は、multiexec を異なる Redis 接続に対して実施するためです。 (つまり、セッションが異なっている)

もう少し詳しく説明すると、multiexec 等で個別に RedisConnectionFactory から RedisConnection を取得し処理を実施します。

Redis トランザクションを使用するには、次の 3通りが考えられます。

  • (a) enableTransactionSupport を true へ変更 (トランザクションサポートを有効化)
  • (b) SessionCallback を execute
  • (c) 自前で bindConnection・unbindConnection を実施

(b) と (c) は enableTransactionSupport = false の設定でも適用できる方法です。 (enableTransactionSupport = true でも問題ありません)

(a) enableTransactionSupport を true へ変更

enableTransactionSupport を true へ変更するのが最も簡単だと思います。

また、true へ変更すると @Transactional アノテーションも使えるみたいです。

設定クラス src/main/java/sample/config/AppConfig.java
・・・
@Configuration
public class AppConfig {
    ・・・
    @Bean
    public RedisTemplate<String, Integer> redisTemplate() {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
        // トランザクションサポートを有効化
        template.setEnableTransactionSupport(true);

        return template;
    }
}
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・
@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    ・・・

    public Object updateWithCas1(String key, IntUnaryOperator func) {
        try {
            // 楽観ロック
            redisTemplate.watch(key);

            BoundValueOperations<String, Integer> valueOps = redisTemplate.boundValueOps(key);
            // 現在の値を取得
            Integer value = valueOps.get();

            redisTemplate.multi();

            // 値の更新。enableTransactionSupport = true の場合はキューイングされる
            valueOps.set(func.applyAsInt(value));

            // enableTransactionSupport = true の場合はキューイングした処理の実行、false の場合はエラー
            return redisTemplate.exec();
        } catch (Exception e) {
            // InvalidDataAccessApiUsageException: ERR EXEC without MULTI
            System.out.println(e);
        }

        return null;
    }
    ・・・
}
実行クラス src/main/java/sample/App.java
・・・
@ComponentScan
@EnableAutoConfiguration
public class App implements CommandLineRunner {
    @Autowired
    private SampleRepository sampleRepository;

    @Override
    public void run(String... args) {
        String key = "a1";

        sampleRepository.save(key, 10);
        Object res1 = sampleRepository.updateWithCas1(key, v -> v + 5);
        System.out.println("res1 = " + res1);
        System.out.println(sampleRepository.load(key));
        ・・・
    }
    ・・・
}

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

実行結果1
> groovy run

・・・
res1 = []
15
・・・

enableTransactionSupport = false の場合の実行結果は以下の通りです。
トランザクションを使用せず set a1 15 を単独で実行しています。

実行結果2 (enableTransactionSupport = false の場合)
> groovy run

・・・
org.springframework.dao.InvalidDataAccessApiUsageException: ERR EXEC without MULTI; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR EXEC without MULTI
res1 = null
15
・・・

(b) SessionCallback を execute

enableTransactionSupport = false の場合でも SessionCallbackRedisCallback でも可) を execute すればトランザクションを使用できます。

SessionCallback インターフェースの execute メソッド内へ実装した処理は同一セッション内で実行されます。

ただし、SessionCallbackメソッドへ仮型引数 K・V が付いており、API 的に微妙な気がします。 (SessionCallback<T, K, V> の方がよかったのでは)

org.springframework.data.redis.core.SessionCallback
・・・
public interface SessionCallback<T> {
    <K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;
}

SessionCallback をラムダで代用したいところですが、仮型引数 K・V のせいで断念しました。

また、operationsRedisOperations<K, V> 型) を強引にキャストしていますが、実体は redisTemplate なので一応は問題無いはずです。

Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・
@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    ・・・

    public Object updateWithCas2(String key, IntUnaryOperator func) {
        // SessionCallback の実行
        return redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                // 扱い難いのでキャスト
                @SuppressWarnings("unchecked")
                RedisOperations<String, Integer> ops = (RedisOperations<String, Integer>)operations;

                ops.watch(key);

                BoundValueOperations<String, Integer> valueOps = ops.boundValueOps(key);
                // 現在の値を取得。
                // multi 実行前に実施する必要あり。multi 後に実行するとキューイングされて戻り値が null になる
                Integer value = valueOps.get();

                ops.multi();

                valueOps.set(func.applyAsInt(value));

                return ops.exec();
            }
        });
    }
    ・・・
}
実行クラス src/main/java/sample/App.java
・・・
@ComponentScan
@EnableAutoConfiguration
public class App implements CommandLineRunner {
    @Autowired
    private SampleRepository sampleRepository;

    @Override
    public void run(String... args) {
        String key = "a1";
        ・・・
        sampleRepository.save(key, 10);
        Object res2 = sampleRepository.updateWithCas2(key, v -> v + 10);
        System.out.println("res2 = " + res2);
        System.out.println(sampleRepository.load(key));
        ・・・
    }
    ・・・
}
設定クラス src/main/java/sample/config/AppConfig.java
・・・
@Configuration
public class AppConfig {
    ・・・
    @Bean
    public RedisTemplate<String, Integer> redisTemplate() {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));

        //template.setEnableTransactionSupport(true);

        return template;
    }
}

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

実行結果
> groovy run

・・・
res2 = []
20
・・・

(c) 自前で bindConnection・unbindConnection を実施

RedisConnectionUtilsbindConnectionunbindConnection メソッドを使って、自前でセッションを制御する方法です。

Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・
@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    ・・・

    public Object updateWithCas3(String key, IntUnaryOperator func) {
        // Redis 接続を bind
        RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory());
        try {
            redisTemplate.watch(key);

            BoundValueOperations<String, Integer> valueOps = redisTemplate.boundValueOps(key);
            Integer value = valueOps.get();

            redisTemplate.multi();

            valueOps.set(func.applyAsInt(value));

            return redisTemplate.exec();

        } finally {
            // Redis 接続を unbind
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
    }
}
実行クラス src/main/java/sample/App.java
・・・
@ComponentScan
@EnableAutoConfiguration
public class App implements CommandLineRunner {
    @Autowired
    private SampleRepository sampleRepository;

    @Override
    public void run(String... args) {
        String key = "a1";
        ・・・
        sampleRepository.save(key, 10);
        Object res3 = sampleRepository.updateWithCas3(key, v -> v + 15);
        System.out.println("res3 = " + res3);
        System.out.println(sampleRepository.load(key));
    }
}

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

実行結果
> groovy run

・・・
res3 = []
25
・・・

備考 (Redis のトランザクション

Redis のトランザクションは、MULTI ~ EXEC 間の処理(コマンド)をキューイングし、EXEC 時に直列化して処理します。(他の処理が途中で入り込む事はない)

ロールバックはできず、途中でコマンドが失敗(文法エラーなど)しても次のコマンドを引き続き実行する点に注意が必要です。

なお、EXEC の代わりに DISCARD を実行するとキューが破棄されトランザクションはキャンセルされます。

WATCH は check-and-set (CAS) による楽観ロックをトランザクションへ適用します。

WATCH したキーが、WATCH 後に更新されていれば MULTI ~ EXEC によるトランザクションを EXEC 時に中止します。

また、EXEC すると WATCH が解除されて UNWATCH になります。

WATCH の動作検証

watch の後に sleep 処理を差し込んで、楽観ロックとトランザクションの動作を確認してみます。

Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・
@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    ・・・

    public Object updateWithCas2(String key, IntUnaryOperator func) {
        return redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                @SuppressWarnings("unchecked")
                RedisOperations<String, Integer> ops = (RedisOperations<String, Integer>)operations;

                ops.watch(key);

                // sleep
                try {
                    Thread.sleep(10000);
                } catch(InterruptedException e) {}

                ・・・
                ops.multi();

                valueOps.set(func.applyAsInt(value));

                return ops.exec();
            }
        });
    }

    public Object updateWithCas3(String key, IntUnaryOperator func) {
        RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory());
        try {
            redisTemplate.watch(key);
            ・・・
            redisTemplate.multi();

            // sleep
            try {
                Thread.sleep(10000);
            } catch(InterruptedException e) {}

            valueOps.set(func.applyAsInt(value));

            return redisTemplate.exec();

        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
    }
}

sleep している間に redis-cliset a1 1 (キー "a1" へ 1 という値をセット) を実行してみたところ、以下のような結果となりました。

実行結果 (sleep 中に a1 の値を更新)
> gradle run

・・・
res2 = null
1
res3 = null
1
・・・

redisTemplate.exec() の戻り値が null となり、トランザクションの中止を確認できました。 (値も更新されていません)

Webブラウザ上で Excel ファイルを作成してダウンロード - Excel Builder (.js)

JavaScript

Excel Builder (.js) を使って、Web ブラウザ上で動的に Excel ファイル (.xlsx) を作成し、ダウンロードする方法をご紹介します。

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

サンプル作成

まずは、HTML を用意します。

今回は、download というリンク (a タグ) をクリックすると Excel ファイル (.xlsx) をダウンロードするようにしてみます。

Excel Builder (.js)RequireJS に依存しているため、RequireJS を読み込むようにして data-main 属性へ実行する js ファイルを指定します。

index.html
<!DOCTYPE html>
<html>
<head>
    <script data-main="app.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.20/require.js"></script>
</head>
<body>

<a id="dw" href="#">download</a>

</body>
</html>

次に、Excel を生成する処理を実装します。

app.js
require(['excel-builder'], function(EB) {
    var wb = EB.createWorkbook();
    var sh = wb.createWorksheet();
    // セルへ値を設定
    sh.setData([
        ['aaa', 10],
        ['サンプル', 2],
        ['てすと', 3],
        ['計', {value: 'sum(B1:B3)', metadata: {type: 'formula'}}]
    ]);

    wb.addWorksheet(sh);

    var trg = document.getElementById("dw");

    // href 属性へ Excel ファイル内容を設定
    trg.href = 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,' + EB.createFile(wb);
    // ダウンロードファイル名の設定
    trg.download = 'sample.xlsx';
});

API がシンプルなので特に説明の必要は無いと思いますが、 計算式は {value: <計算式>, metadata: {type: 'formula'}} で設定できます。

また、EB.createFile(<ワークブック>) により Excel ファイルの内容を Base64 形式で取得できるので、データ形式 data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64, を先頭に付けて a タグの href 属性へ設定すれば Excel ファイルをダウンロードできるようになります。

excel-builder.js の配置

最後に、http://excelbuilderjs.com/ の Download からアーカイブファイルをダウンロード、適当なディレクトリへ解凍し、dist ディレクトリ内のファイルのどれか (例えば dist/excel-builder.compiled.min.js) を excel-builder.js という名称で app.js と同じディレクトリへ配置すれば完成です。

ファイル構成
  • index.html
  • app.js
  • excel-builder.js

動作確認

作成した index.htmlChromeFirefox で直接開いて、download リンクをクリックすると sample.xlsx をダウンロードできます。

f:id:fits:20150822020436p:plain

ダウンロードした sample.xlsx を開くと、セルの内容と計算式が正しく機能している事を確認できました。

f:id:fits:20150822020449p:plain

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

Java Scala

前回JPA に続き、今回は Gradle と Querydsl Scala を使って Querydsl SQL のコード生成を試します。

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

はじめに

Querydsl SQL の場合は ScalaJava と同じ要領でコードを生成します。 (Java の場合は 「Gradle を使った Querydsl SQL のコード生成」 参照)

ただし、MetaDataExporterScala 用の 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) Connectionclose

なお、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 のコード生成

Java Scala

Gradle と Querydsl Scala を使って Querydsl JPAScala 用コード生成を試してみました。

ソースは 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 JPAScala 用コードを生成 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.srcDirsrc/main/qdsl-generated を追加しています。

なお、(2) で (1) のクラスをロードできるように buildscriptclasspathbuild/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 直後は、modelCompilegenerate 以降のタスクを分けて実行する必要があります。 (上の方でも書きましたが buildscriptclasspathbuild/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)])

JPA における一対多のリレーションシップ - EclipseLink

Java

EclipseLink 2.6.1 RC1 を使って JPA の一対多リレーションシップを下記 2通りで試し、SQL の実行内容などを調査してみました。

  • (a) 単方向: @OneToMany + @JoinColumn
  • (b) 双方向: @OneToMany + @ManyToOne

簡単にまとめると次の通りです。

タイプ 使用したアノテーション one側のデータ登録SQL many側のデータ登録SQL
(a) 単方向 @OneToMany, @JoinColumn insert insert, update
(b) 双方向 @OneToMany, @ManyToOne insert insert

(a) の場合に insert だけでは無く update も実施していました。

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

はじめに

テーブル構成

今回使用したテーブル構成は以下の通りです。

product と product_variation が一対多の関係になっています。 (product_variation.product_id で product を参照)

product テーブル
id name price
1 test1 100
2 test2 200
product_variation テーブル
id product_id color size
1 1 Green F
2 1 Blue S
3 2 Red S

DDL 文は以下の通りです。

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;

(a) の単方向の場合、データ登録時に下記のような挙動となるため、product_id へ外部キー制約を付けたりすると不都合が生じます。 (product_id のデフォルト値を 0 としているのもそのためです)

  • (1) product_id を指定せずに product_variation へ insert 文を実行
  • (2) update 文で product_id を設定

ビルド定義ファイル

Gradle 用のビルド定義ファイルを以下のようにしました。 動作確認に SampleApp クラスを実行するようになっています。

また、コードを簡素化するため lombok を使っています。

build.gradle
apply plugin: 'application'

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

mainClassName = 'sample.SampleApp'

repositories {
    jcenter()
}

dependencies {
    compile 'javax:javaee-api:7.0'
    compile 'org.projectlombok:lombok:1.16.4'

    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 の設定ファイルは以下のようにしました。

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" />

            <!-- SQL の内容をログ出力するための設定 -->
            <property name="eclipselink.logging.level" value="FINE" />
        </properties>
    </persistence-unit>
</persistence>

SQL の内容をログ出力 (標準出力) するため eclipselink.logging.levelFINE を設定しています。

(a) 単方向: @OneToMany + @JoinColumn

まずは、@OneToMany@JoinColumn を使った単方向の一対多の関連を実現します。

エンティティクラス

product テーブル用のエンティティクラス Product 内で @OneToMany@JoinColumn を使います。

@JoinColumnname を使って join するテーブル (product_variation) の外部キー項目 (product_id) を指定します。

ちなみに、FetchType の指定は必須ではありません。 (今回は FetchType.EAGER 時の SQL 内容を確認するため指定しました)

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

import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private BigDecimal price;

    // setter メソッドの未定義化
    @Setter(AccessLevel.NONE)
    // 一対多の関連
    @OneToMany(fetch = FetchType.EAGER, cascade= CascadeType.ALL)
    @JoinColumn(name = "product_id")
    private List<ProductVariation> variationList = new ArrayList<>();
}

lombok の @Data を使うと各フィールドの getter・setter メソッドを自動的に定義してくれますが、variationList の setter メソッドは不要なので @Setter(AccessLevel.NONE) を使って無効化しています。

また、product_variation テーブルのエンティティクラス ProductVariation に対する特別な設定は不要です。

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

import lombok.Data;
import javax.persistence.*;

@Data
@Entity
@Table(name = "product_variation")
public class ProductVariation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String color;
    private String size;
}

実行クラス

動作確認のための実行クラスです。

2つのバリエーションを追加した商品データを永続化した後、全件検索するようにしてみました。

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

import lombok.val;
import sample.model.Product;
import sample.model.ProductVariation;
import javax.persistence.Persistence;
import java.math.BigDecimal;
import java.util.List;

public class SampleApp {
    public static void main(String... args) throws Exception {
        val emf = Persistence.createEntityManagerFactory("jpa");
        val em = emf.createEntityManager();

        val tx = em.getTransaction();
        tx.begin();

        val p1 = product(
            "sample1", "50", 
            variation("White", "L"), 
            variation("Black", "M")
        );

        // 永続化
        em.persist(p1);

        tx.commit();

        val cq = em.getCriteriaBuilder().createQuery(Product.class);
        // 全件検索
        List<Product> res = em.createQuery(cq).getResultList();
        // 結果出力
        res.forEach(System.out::println);

        em.close();
    }
    // Product の作成
    private static Product product(String name, String price, ProductVariation... variations) {
        val res = new Product();

        res.setName(name);
        res.setPrice(new BigDecimal(price));

        for (val v : variations) {
            // バリエーションの追加
            res.getVariationList().add(v);
        }

        return res;
    }
    // ProductVariation の作成
    private static ProductVariation variation(String color, String size) {
        val res = new ProductVariation();

        res.setColor(color);
        res.setSize(size);

        return res;
    }
}

実行

Gradle による実行結果です。

実行結果
> gradle run
・・・
Product(id=1, name=test1, price=100, variationList=[ProductVariation(id=1, color=Green, size=F), ProductVariation(id=2, color=Blue, size=S)])
Product(id=2, name=test2, price=200, variationList=[ProductVariation(id=3, color=Red, size=S)])
Product(id=3, name=sample1, price=50, variationList=[ProductVariation(id=5, color=White, size=L), ProductVariation(id=4, color=Black, size=M)])

SQL の実行内容です。

SQL の実行内容
INSERT INTO PRODUCT (NAME, PRICE) VALUES (?, ?)
    bind => [sample1, 50]

SELECT LAST_INSERT_ID()

INSERT INTO product_variation (COLOR, SIZE) VALUES (?, ?)
    bind => [Black, M]

SELECT LAST_INSERT_ID()

INSERT INTO product_variation (COLOR, SIZE) VALUES (?, ?)
    bind => [White, L]

SELECT LAST_INSERT_ID()

UPDATE product_variation SET product_id = ? WHERE (ID = ?)
    bind => [3, 5]

UPDATE product_variation SET product_id = ? WHERE (ID = ?)
    bind => [3, 4]

----------

SELECT ID, NAME, PRICE FROM PRODUCT

SELECT ID, COLOR, SIZE FROM product_variation WHERE (product_id = ?)
    bind => [1]

SELECT ID, COLOR, SIZE FROM product_variation WHERE (product_id = ?)
    bind => [2]

特徴は次の通りです。

  • product_variation を insert してから update している (update 時に product_id を設定)
  • product_variation 1件毎に 1回 update 文を実行している
  • product_variation を product_id 毎に検索している
  • 永続化した p1 に対しては SQL を実行していない

(b) 双方向: @OneToMany + @ManyToOne

次に@OneToMany@ManyToOne を使った双方向の一対多の関連を実現します。

エンティティクラス

Product@OneToManyProductVariation@ManyToOne を用います。

@OneToManymappedBy を使って @ManyToOne を付与したフィールド名を指定します。

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

import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private BigDecimal price;

    @Setter(AccessLevel.NONE)
    // 一対多の関連
    @OneToMany(mappedBy = "product", 
        fetch = FetchType.EAGER, cascade= CascadeType.ALL)
    private List<ProductVariation> variationList = new ArrayList<>();
}
src/main/java/sample/model/ProductVariation.java
package sample.model;

import lombok.Data;
import lombok.ToString;
import javax.persistence.*;

@Data
@ToString(exclude = "product") // Product.toString() を呼び出すのを防止
@Entity
@Table(name = "product_variation")
public class ProductVariation {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String color;
    private String size;

    // 多対一の関連
    @ManyToOne
    private Product product;
}

ここで、toString の内容に product フィールドが含まれると (@Data によって適用されてしまう) ProductProductVariationtoString を交互に呼び出し続ける事になってしまうので、@ToString(exclude = "product") としています。

実行クラス

基本的に (a) と同じですが、ProductVariationProduct を設定しなければならない点が異なります。

mappedBy で指定しているのだから、自動で設定してくれても良さそうな気がするのですが。 (もしかすると何か方法があるのかもしれません)

src/main/java/sample/SampleApp.java
・・・
public class SampleApp {
    public static void main(String... args) throws Exception {
        ・・・
    }

    private static Product product(String name, String price, ProductVariation... variations) {
        val res = new Product();

        res.setName(name);
        res.setPrice(new BigDecimal(price));

        for (val v : variations) {
            res.getVariationList().add(v);

            // 下記の設定が必要な点が (a) との違い
            v.setProduct(res);
        }

        return res;
    }
    ・・・
}

実行

Gradle による実行結果です。

実行結果
> gradle run
・・・
Product(id=1, name=test1, price=100, variationList=[ProductVariation(id=1, color=Green, size=F), ProductVariation(id=2, color=Blue, size=S)])
Product(id=2, name=test2, price=200, variationList=[ProductVariation(id=3, color=Red, size=S)])
Product(id=3, name=sample1, price=50, variationList=[ProductVariation(id=5, color=White, size=L), ProductVariation(id=4, color=Black, size=M)])

SQL の実行内容です。

SQL の実行内容
INSERT INTO PRODUCT (NAME, PRICE) VALUES (?, ?)
    bind => [sample1, 50]

SELECT LAST_INSERT_ID()

INSERT INTO product_variation (COLOR, SIZE, PRODUCT_ID) VALUES (?, ?, ?)
    bind => [Black, M, 3]

SELECT LAST_INSERT_ID()

INSERT INTO product_variation (COLOR, SIZE, PRODUCT_ID) VALUES (?, ?, ?)
    bind => [White, L, 3]

SELECT LAST_INSERT_ID()

-----

SELECT ID, NAME, PRICE FROM PRODUCT

SELECT ID, COLOR, SIZE, PRODUCT_ID FROM product_variation WHERE (PRODUCT_ID = ?)
    bind => [1]

SELECT ID, COLOR, SIZE, PRODUCT_ID FROM product_variation WHERE (PRODUCT_ID = ?)
    bind => [2]

(a) とは違って product_variation の insert 時に product_id も設定するようになっています。

Google スプレッドシートを REST API で操作

Google

Google スプレッドシートREST API で操作します。

API の利用には 前回 と同様にリフレッシュトークンを使います。

はじめに

Google スプレッドシートAPI を使うには、Google アカウントで API の利用を承認する際に scopehttps://spreadsheets.google.com/feeds/ と指定します。 (手順は前回を参照)

アクセストークンの取得

リフレッシュトークンからアクセストークンを REST API で取得するには、 https://www.googleapis.com/oauth2/v3/tokenclient_id=<クライアントID>&client_secret=<クライアントシークレット>&grant_type=refresh_token&refresh_token=<リフレッシュトークン> を POST します。

実行例(cURL
$ curl -d "client_id=xxxxx.apps.googleusercontent.com&client_secret=SarzR・・・&grant_type=refresh_token&refresh_token=1/iiM・・・" https://www.googleapis.com/oauth2/v3/token

{
 "access_token": "ya26.pw・・・",
 "token_type": "Bearer",
 "expires_in": 3600
}

取得したアクセストークンは HTTP ヘッダーで指定します。

アクセストークンの指定例 (HTTP ヘッダー)
Authorization: Bearer ya26.pw・・・

(1) スプレッドシートの一覧を取得

スプレッドシートの一覧を取得するには https://spreadsheets.google.com/feeds/spreadsheets/private/full へ GET します。

スプレッドシート取得例(cURL
$ curl -H "Authorization: Bearer ya26.pw・・・" https://spreadsheets.google.com/feeds/spreadsheets/private/full

<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom'xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'><id>https://spreadsheets.google.com/feeds/spreadsheets/private/full</id>・・・

処理結果(XML)の内容は以下の通りです。

XML 結果例

<feed>
  ・・・
  <entry>
    <id>https://spreadsheets.google.com/feeds/spreadsheets/private/full/1E0R・・・</id>
    <updated>2015-07-01T16:30:20.027Z</updated>
    <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#spreadsheet"/>
    <title type="text">sample</title>
    <content type="text">sample</content>
    <link rel="http://schemas.google.com/spreadsheets/2006#worksheetsfeed" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full"/>
    <link rel="alternate" type="text/html" href="https://docs.google.com/spreadsheets/d/1E0R・・・/edit"/>
    <link rel="self" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/spreadsheets/private/full/1E0R・・・"/>
    <author>
      ・・・
    </author>
  </entry>
  ・・・
</feed>

(2) スプレッドシート内のシートの一覧を取得

シートの一覧を取得するには https://spreadsheets.google.com/feeds/worksheets/${key}/private/full へ GET します。

${key} の値は (1) で取得した XML の entry/id 要素から取得できます。 (https://spreadsheets.google.com/feeds/spreadsheets/private/full/${key}

シート取得例(cURL
$ curl -H "Authorization: Bearer ya26.pw・・・" https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full

<?xml version='1.0' encoding='UTF-8'?><feed xmlns='http://www.w3.org/2005/Atom'
xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gs='http://schemas.google.com/spreadsheets/2006'><id>https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full</id>・・・

処理結果(XML)の内容は以下の通りです。

XML 結果例

<feed>
  ・・・
  <entry>
    <id>https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full/od6</id>
    <updated>2015-07-01T16:30:20.010Z</updated>
    <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#worksheet"/>
    <title type="text">シート1</title>
    <content type="text">シート1</content>
    <link rel="http://schemas.google.com/spreadsheets/2006#listfeed" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/list/1E0R・・・/od6/private/full"/>
    <link rel="http://schemas.google.com/spreadsheets/2006#cellsfeed" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full"/>
    <link rel="http://schemas.google.com/visualization/2008#visualizationApi" type="application/atom+xml" href="https://docs.google.com/spreadsheets/d/1E0R・・・/gviz/tq?gid=0"/>
    <link rel="http://schemas.google.com/spreadsheets/2006#exportcsv" type="text/csv" href="https://docs.google.com/spreadsheets/d/1E0R・・・/export?gid=0&format=csv"/>
    <link rel="self" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full/od6"/>
    <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/worksheets/1E0R・・・/private/full/od6/6cl・・・"/>
    <gs:colCount>26</gs:colCount>
    <gs:rowCount>1000</gs:rowCount>
  </entry>
  ・・・
</feed>

なお、シートの一覧を取得する URL は、(1) の XML から以下のような XPath 式で取り出す事も可能です。

XPath
//entry[title = '<スプレッドシートのタイトル>']/link[@rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']/@href

(3) シート内のセルを取得

セルを取得するには https://spreadsheets.google.com/feeds/cells/${key}/${worksheetId}/private/full へ GET します。

${worksheetId} は (2) で取得した XML の entry/id 要素から取得できます。 (https://spreadsheets.google.com/feeds/worksheets/${key}/private/full/${worksheetId}

セル取得例(cURL
$ curl -H "Authorization: Bearer ya26.pw・・・" https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full

<?xml version='1.0' encoding='UTF-8'?><feed xmlns='http://www.w3.org/2005/Atom'
xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:batch='http://schemas.google.com/gdata/batch' xmlns:gs='http://schemas.google.com/spreadsheets/2006'><id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full</id>・・・

処理結果(XML)の内容は以下の通りです。

XML 結果例

<feed>
  ・・・
  <entry>
    <id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C1</id>
    <updated>2015-07-01T16:30:20.010Z</updated>
    <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#cell"/>
    <title type="text">A1</title>
    <content type="text">aaa</content>
    <link rel="self" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C1"/>
    <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C1/kgy・・・"/>
    <gs:cell row="1" col="1" inputValue="aaa">aaa</gs:cell>
  </entry>
  ・・・
</feed>

なお、セルの一覧を取得する URL は、(2) の XML から以下のような XPath 式で取り出す事も可能です。

XPath
//entry[title = '<シートのタイトル>']/link[@rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']/@href

(4) セルを登録・更新

https://spreadsheets.google.com/feeds/cells/${key}/${worksheetId}/private/full へ以下のようなフォーマットの XML を POST する事でセルを登録 (更新も可) できます。

注意点として、Content-Type を application/atom+xml と指定する必要があります。

セル登録 XML フォーマット例
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gs="http://schemas.google.com/spreadsheets/2006">
  <id>https://spreadsheets.google.com/feeds/cells/${key}/${worksheetId}/private/full/R${行}C${列}</id>
  <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/${key}/${worksheetId}/private/full/R${行}C${列}"/>
  <gs:cell row="${行}" col="${列}" inputValue="${値}"/>
</entry>

今回は以下の XML ファイルを使って E1 セルへ sample123 という文字を登録してみます。

e1.xml
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gs="http://schemas.google.com/spreadsheets/2006">
  <id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5</id>
  <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5" />
  <gs:cell row="1" col="5" inputValue="sample123" />
</entry>
セル登録例(cURL
$ curl -X POST -H "Authorization: Bearer ya26.pw・・・" -H "Content-Type: application/atom+xml" -d @e1.xml https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full

<?xml version='1.0' encoding='UTF-8'?><entry xmlns='http://www.w3.org/2005/Atom' xmlns:batch='http://schemas.google.com/gdata/batch' xmlns:gs='http://schemas.google.com/spreadsheets/2006'><id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5</id>・・・

処理結果(XML)の内容は以下の通りです。

XML 結果例

<entry>
  <id>https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5</id>
  <updated>2015-07-01T16:30:20.010Z</updated>
  <category scheme="http://schemas.google.com/spreadsheets/2006" term="http://schemas.google.com/spreadsheets/2006#cell"/>
  <title type="text">E1</title>
  <content type="text">sample123</content>
  <link rel="self" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5"/>
  <link rel="edit" type="application/atom+xml" href="https://spreadsheets.google.com/feeds/cells/1E0R・・・/od6/private/full/R1C5/jikzt4"/>
  <gs:cell row="1" col="5" inputValue="sample123">sample123</gs:cell>
</entry>

link[@rel='edit']/@hrefjikzt4 はそのセルのバージョン番号です。