Windows上で Rust 1.3 を使用
以前、「Windows 上で Rust を使用」 では Rust 0.9 を使いましたが、今回は Rust 1.3 で試してみました。
環境構築
https://www.rust-lang.org/ から Windows installer をダウンロード (例 https://static.rust-lang.org/dist/rust-1.3.0-x86_64-pc-windows-gnu.msi) し、インストールするだけとなっています。
gcc 等もインストーラーに含まれているので (例 bin\rustlib\x86_64-pc-windows-gnu\bin\gcc.exe
)、以前のように MinGW を別途インストールしたりする必要はありません。
ビルドと実行
それではサンプルソースをビルドして実行してみます。
以前の Rust 0.9 用ソースをそのままでは使えなかったので書き換えました。
sample.rs
use std::fmt; fn main() { let d1 = Data { name: "data", value: 10 }; let d2 = Data { name: "data", value: 10 }; let d3 = Data { name: "data", value: 0 }; let d4 = Data { name: "etc", value: 5 }; println!("d1 == d2 : {}", d1 == d2); println!("d1 == d2 : {}", d1.eq(&d2)); println!("d1 == d3 : {}", d1 == d3); println!("-----"); println!("{:?}", d1); println!("{}", d1); println!("-----"); println!("times = {}", d1.times(3)); println!("-----"); d1.print_value(); d3.print_value(); println!("-----"); let res = calc(&[d1, d2, d3, d4]); println!("calc = {}", res); } fn calc(list: &[Data]) -> i32 { list.iter().fold(1, |acc, v| acc * match v { // name = "data" で value の値が 0 より大きい場合 &Data {name: "data", value: b} if b > 0 => b, // それ以外 _ => 1 }) } // PartialEq と Debug トレイトを自動導出 #[derive(PartialEq, Debug)] struct Data { name: &'static str, value: i32 } // メソッドの定義 impl Data { fn print_value(&self) { match self.value { 0 => println!("value: zero"), a @ _ => println!("value: {}", a) } } } // トレイトの定義 trait Sample { fn get_value(&self) -> i32; fn times(&self, n: i32) -> i32 { self.get_value() * n } } // トレイトの実装 impl Sample for Data { fn get_value(&self) -> i32 { self.value } } // {} で出力するため Display トレイトを実装 impl fmt::Display for Data { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "display (Data name={}, value={})", self.name, self.value) } }
以前との主な変更点は以下の通りです ※。
- deriving を
derive
へ変更 - int を
i32
へ変更 - ~str を
&'static str
へ変更 - Eq を PartialEq へ変更 (
trait Eq: PartialEq<Self>
となっている) {}
で出力するため Display トレイトを実装{:?}
で出力するため Debug トレイトを自動導出
※ Rust 1.0 で色々と変更されたようです
ビルド
rustc コマンドを使ってビルドを実施します。※
ビルドに成功すると .exe ファイルが作成されます。
※ インストール時に "Add to PATH" を実施しなかった場合は 環境変数 PATH へ Rust の bin ディレクトリを追加しておく必要があります
ビルド例
> rustc sample.rs
実行
ビルドで生成された .exe ファイルは普通に実行できます。
実行例
> sample.exe d1 == d2 : true d1 == d2 : true d1 == d3 : false ----- Data { name: "data", value: 10 } display (Data name=data, value=10) ----- times = 30 ----- value: 10 value: zero ----- calc = 100
今回使用したサンプルソースは http://github.com/fits/try_samples/tree/master/blog/20150920/
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_size
は proxy_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 におけるデフォルト設定の注意点
Spring Data Redis のデフォルト設定に関して、個人的に気になった点を挙げておきます。
- (1) キーと値に JdkSerializationRedisSerializer を適用
- (2) トランザクションサポートが無効化 (
enableTransactionSupport
= false)
今回使用したモジュールは以下。
- Spring Boot Starter Redis 1.2.5 ( Spring Data Redis 1.4.3 )
サンプルソースは 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) トランザクションサポートが無効化
デフォルトで RedisTemplate
の enableTransactionSupport
が false
となっています。
この場合、以下のように RedisTemplate
の multi
と exec
メソッドを使用しても Redis のトランザクションは適用されず、exec
の実行時に JedisDataException: ERR EXEC without MULTI
エラーが発生します。
// 楽観ロックの適用 redisTemplate.watch(key); ・・・ // Redis トランザクションの開始 redisTemplate.multi(); ・・・ // Redis トランザクションの実施。enableTransactionSupport = false の場合はエラー redisTemplate.exec();
エラーが発生する原因は、multi
と exec
を異なる Redis 接続に対して実施するためです。 (つまり、セッションが異なっている)
もう少し詳しく説明すると、multi
や exec
等で個別に 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 の場合でも SessionCallback
(RedisCallback
でも可) を 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 のせいで断念しました。
また、operations
(RedisOperations<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 を実施
RedisConnectionUtils
の bindConnection
と unbindConnection
メソッドを使って、自前でセッションを制御する方法です。
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-cli で set a1 1
(キー "a1" へ 1 という値をセット) を実行してみたところ、以下のような結果となりました。
実行結果 (sleep 中に a1 の値を更新)
> gradle run ・・・ res2 = null 1 res3 = null 1 ・・・
redisTemplate.exec()
の戻り値が null
となり、トランザクションの中止を確認できました。 (値も更新されていません)
Webブラウザ上で Excel ファイルを作成してダウンロード - Excel Builder (.js)
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.html
を Chrome や Firefox で直接開いて、download リンクをクリックすると sample.xlsx
をダウンロードできます。
ダウンロードした sample.xlsx
を開くと、セルの内容と計算式が正しく機能している事を確認できました。
Gradle と Querydsl Scala を使った Querydsl SQL のコード生成
前回 の JPA に続き、今回は Gradle と Querydsl Scala を使って Querydsl SQL のコード生成を試します。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150810/
はじめに
Querydsl SQL の場合は Scala も Java と同じ要領でコードを生成します。 (Java の場合は 「Gradle を使った Querydsl SQL のコード生成」 参照)
ただし、MetaDataExporter
へ Scala 用の serializerClass 等を設定する必要があります。
Gradle ビルド定義
Gradle 用のビルド定義ファイルは以下のようになります。
build.gradle
apply plugin: 'scala' // Querydsl のソース生成先パッケージ名 ext.modelPackage = 'sample.model' // Querydsl のソース生成先ディレクトリ ext.qdslDestDir = 'src/main/qdsl-generated' // DB接続 URL ext.dbUrl = 'jdbc:mysql://localhost:3306/jpa_sample?user=root' buildscript { repositories { jcenter() } dependencies { classpath 'com.querydsl:querydsl-sql-codegen:4.0.3' classpath 'com.querydsl:querydsl-scala:4.0.3' classpath 'org.scala-lang:scala-library:2.11.7' // MySQL へ接続してコード生成する場合 classpath 'mysql:mysql-connector-java:5.1.36' } } repositories { jcenter() } dependencies { compile 'com.querydsl:querydsl-scala:4.0.3' compile 'com.querydsl:querydsl-sql:4.0.3' compile 'org.scala-lang:scala-library:2.11.7' } // コード生成 task generate << { def con = new com.mysql.jdbc.Driver().connect(dbUrl, null) def exporter = new com.querydsl.sql.codegen.MetaDataExporter() exporter.packageName = modelPackage exporter.targetFolder = new File(qdslDestDir) exporter.serializerClass = com.querydsl.scala.sql.ScalaMetaDataSerializer exporter.typeMappings = com.querydsl.scala.ScalaTypeMappings.create() // Bean のコードも生成する場合は以下を有効化 //exporter.beanSerializerClass = com.querydsl.scala.ScalaBeanSerializer exporter.createScalaSources = true exporter.export(con.metaData) con.close() } compileScala { dependsOn generate sourceSets.main.scala.srcDir qdslDestDir } clean { delete qdslDestDir }
DB は MySQL を使用し、「JPA における一対多のリレーションシップ - EclipseLink」 で使ったものと同じテーブルを使用します。
使用する DB のテーブル定義 (DDL)
CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL, `price` decimal(10,0) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `product_variation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_id` bigint(20) NOT NULL DEFAULT 0, `color` varchar(10) NOT NULL, `size` varchar(10) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
サンプルアプリケーション1
それでは簡単なサンプルアプリケーションを作成し実行してみます。
ビルド定義
先程の build.gradle へ少しだけ手を加え、サンプルアプリケーション sample.SampleApp
を実行するようにしました。
build.gradle
apply plugin: 'scala' apply plugin: 'application' ext.modelPackage = 'sample.model' ext.qdslDestDir = 'src/main/qdsl-generated' ext.dbUrl = 'jdbc:mysql://localhost:3306/jpa_sample?user=root' mainClassName = 'sample.SampleApp' buildscript { ・・・ } ・・・ dependencies { compile 'com.querydsl:querydsl-scala:4.0.3' compile 'com.querydsl:querydsl-sql:4.0.3' compile 'org.scala-lang:scala-library:2.11.7' runtime 'mysql:mysql-connector-java:5.1.36' runtime 'org.slf4j:slf4j-nop:1.7.12' } task generate << { def con = new com.mysql.jdbc.Driver().connect(dbUrl, null) def exporter = new com.querydsl.sql.codegen.MetaDataExporter() exporter.packageName = modelPackage exporter.targetFolder = new File(qdslDestDir) exporter.serializerClass = com.querydsl.scala.sql.ScalaMetaDataSerializer exporter.typeMappings = com.querydsl.scala.ScalaTypeMappings.create() //exporter.beanSerializerClass = com.querydsl.scala.ScalaBeanSerializer exporter.createScalaSources = true exporter.export(con.metaData) con.close() } ・・・
generate タスクを実行すると以下のファイルが生成されます。
- src/main/qdsl-generated/sample/model/QProduct.scala
- src/main/qdsl-generated/sample/model/QProductVariation.scala
実行クラス
単純な insert・select 処理を実装しました。
src/main/scala/sample/SampleApp.scala
package sample import com.querydsl.sql.dml.SQLInsertClause import com.querydsl.sql.{SQLQuery, MySQLTemplates} import java.util.Properties import java.sql.DriverManager import scala.collection.JavaConversions._ import sample.model.{QProduct, QProductVariation} object SampleApp extends App { val conf = new Properties() conf.load(getClass.getClassLoader.getResourceAsStream("db.properties")) val con = DriverManager.getConnection(conf.getProperty("url"), conf) con.setAutoCommit(false) val templates = new MySQLTemplates() val p = QProduct as "p" val v = QProductVariation as "v" // product へ insert val pid: Long = new SQLInsertClause(con, templates, p) .set(p.name, s"sample${System.currentTimeMillis()}") .set(p.price, 1500L) .executeWithKey(p.id) // product_variation へ insert new SQLInsertClause(con, templates, v) .set(v.productId, pid).set(v.color, "Green").set(v.size, "L").addBatch() .set(v.productId, pid).set(v.color, "Blue").set(v.size, "S").addBatch() .execute() con.commit() val query = new SQLQuery(con, templates) // product と product_variation を join して select val res = query.from(p) .join(v).on(v.productId.eq(p.id)) .where(p.price.between(1300, 2500)) .select(p.id, p.name, p.price, v.color, v.size) .fetch() // id, name, price でグルーピング val groupedRes = res.groupBy(x => (x.get(p.id), x.get(p.name), x.get(p.price))) println(groupedRes) con.close() }
DB 接続設定ファイル
DB の接続設定に以下のプロパティファイルを使用します。
src/main/resources/db.properties
url=jdbc:mysql://localhost:3306/jpa_sample?characterEncoding=utf8 user=root password=
実行
実行結果は以下の通りです。
実行結果
> gradle run :compileJava UP-TO-DATE :generate :compileScala :processResources :classes :run Map((3,sample1439089472290,1500) -> ArrayBuffer([3, sample1439089472290, 1500, Green, L], [3, sample1439089472290, 1500, Blue, S]))
サンプルアプリケーション2
次は Bean を使ったサンプルです。
ビルド定義
exporter.beanSerializerClass = com.querydsl.scala.ScalaBeanSerializer
を有効化し、Bean のコード生成を行うようにしました。
build.gradle
apply plugin: 'scala' apply plugin: 'application' ext.modelPackage = 'sample.model' ext.qdslDestDir = 'src/main/qdsl-generated' ext.dbUrl = 'jdbc:mysql://localhost:3306/jpa_sample?user=root' mainClassName = 'sample.SampleApp2' buildscript { ・・・ } ・・・ dependencies { compile 'com.querydsl:querydsl-scala:4.0.3' compile 'com.querydsl:querydsl-sql:4.0.3' compile 'org.scala-lang:scala-library:2.11.7' compile 'org.apache.commons:commons-dbcp2:2.1.1' runtime 'mysql:mysql-connector-java:5.1.36' runtime 'org.slf4j:slf4j-nop:1.7.12' } task generate << { def con = new com.mysql.jdbc.Driver().connect(dbUrl, null) def exporter = new com.querydsl.sql.codegen.MetaDataExporter() exporter.packageName = modelPackage exporter.targetFolder = new File(qdslDestDir) exporter.serializerClass = com.querydsl.scala.sql.ScalaMetaDataSerializer exporter.typeMappings = com.querydsl.scala.ScalaTypeMappings.create() // Bean のコード生成を有効化 exporter.beanSerializerClass = com.querydsl.scala.ScalaBeanSerializer exporter.createScalaSources = true exporter.export(con.metaData) con.close() } ・・・
generate タスクを実行すると以下のファイルが生成されます。
- src/main/qdsl-generated/sample/model/Product.scala
- src/main/qdsl-generated/sample/model/ProductVariation.scala
- src/main/qdsl-generated/sample/model/QProduct.scala
- src/main/qdsl-generated/sample/model/QProductVariation.scala
実行クラス
処理内容は、サンプルアプリケーション1 と同じですが、com.querydsl.scala.sql.SQL
トレイトを使って Connection
を直接扱わなくても済むようにしています。
SQL
トレイトの tx
メソッドへ DB 処理 (insert や select 等) を渡します。
tx
では大まかに以下のような処理を実行するようです。
- (1)
DataSource
からConnection
取得 (setAutoCommit を false へ設定) - (2) 引数で渡した処理の実行
- (3) コミット or ロールバック
- (4)
Connection
のclose
なお、Bean を使って insert する場合は populate
メソッドを使います。
src/main/scala/sample/SampleApp2.scala
package sample import com.querydsl.scala.sql.SQL import com.querydsl.sql.{SQLTemplates, MySQLTemplates} import org.apache.commons.dbcp2.BasicDataSourceFactory import java.util.Properties import javax.sql.DataSource import scala.collection.JavaConversions._ import sample.model.{Product, ProductVariation, QProduct, QProductVariation} // com.querydsl.scala.sql.SQL トレイトの実装 case class QueryDSLHelper(dataSource: DataSource, templates: SQLTemplates) extends SQL object SampleApp2 extends App { val product = (name: String, price: Long) => { val res = new Product() res.name = name res.price = price res } val variation = (productId: Long, color: String, size: String) => { val res = new ProductVariation() res.productId = productId res.color = color res.size = size res } val conf = new Properties() conf.load(getClass.getClassLoader.getResourceAsStream("db.properties")) val dataSource = BasicDataSourceFactory.createDataSource(conf) val qdsl = QueryDSLHelper(dataSource, new MySQLTemplates()) val p = QProduct as "p" val v = QProductVariation as "v" qdsl.tx { // product へ insert val pid = qdsl.insert(p) .populate(product(s"test${System.currentTimeMillis()}", 2000L)) .executeWithKey(p.id) // product_variation へ insert qdsl.insert(v) .populate(variation(pid, "Red", "M")).addBatch() .populate(variation(pid, "Yellow", "F")).addBatch() .execute() } qdsl.tx { // product と product_variation を join して select val res = qdsl.from(p) .join(v).on(v.productId.eq(p.id)) .where(p.price.between(1300, 2500)) .select(p.id, p.name, p.price, v.color, v.size) .fetch() // id, name, price でグルーピング val groupedRes = res.groupBy(x => (x.get(p.id), x.get(p.name), x.get(p.price))) println(groupedRes) } }
DB 接続設定ファイル
commons-dbcp2 用のプロパティファイルを使いました。
src/main/resources/db.properties
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/jpa_sample?characterEncoding=utf8 username=root password=
実行
実行結果は以下の通りです。
実行結果
> gradle run :compileJava UP-TO-DATE :generate :compileScala :processResources :classes :run Map((3,sample1439089472290,1500) -> ArrayBuffer([3, sample1439089472290, 1500, Green, L], [3, sample1439089472290, 1500, Blue, S]), (4,test1439089637936,2000) -> ArrayBuffer([4, test1439089637936, 2000, Red, M], [4, test1439089637936, 2000, Yellow, F]))
Gradle と Querydsl Scala を使った Querydsl JPA のコード生成
Gradle と Querydsl Scala を使って Querydsl JPA の Scala 用コード生成を試してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150727/
はじめに
「Gradle を使った Querydsl JPA のコード生成」 ではアノテーションプロセッサを使って Querydsl JPA のコードを生成しましたが、Scala の場合は com.querydsl.codegen.GenericExporter
クラスを使うようです。
GenericExporter
でコード生成するには JPA のエンティティクラスをロードできなければなりません。 (つまり、エンティティクラスを事前にコンパイルしておく必要あり)
Gradle ビルド定義
エンティティクラスを Querydsl のコード生成前にコンパイルするため、今回は以下のようにエンティティクラスだけをコンパイルするタスク modelCompile
と Querydsl 用のコードを生成するタスク generate
を追加しました。
番号 | 概要 | タスク名 |
---|---|---|
(1) | エンティティクラスをコンパイル | modelCompile |
(2) | (1) のエンティティクラスを使って Querydsl JPA の Scala 用コードを生成 | generate |
(3) | (2) で生成したソースをビルド | compileScala |
(1) では src/main/scala-model
へ配置したエンティティクラスのソース (Scala) をビルドして build/classes/main
へ出力します。
(2) では com.querydsl.codegen.GenericExporter
を使って Scala 用の Querydsl JPA コードを src/main/qdsl-generated
へ生成します。
(3) で (2) の生成したソースをビルドできるように sourceSets.main.scala.srcDir
へ src/main/qdsl-generated
を追加しています。
なお、(2) で (1) のクラスをロードできるように buildscript
の classpath
へ build/classes/main
を追加しているのですが、これが原因で初回実行時や clean 直後は (1) と (2) を別々に実行する必要があります。
これは、build/classes/main
へクラスファイルが配置されていない状態 ((1) の実施前) で Gradle を実行すると given scan urls are empty. set urls in the configuration
とメッセージが出力され、以降のタスクで build/classes/main
をクラスパスとして認識しない事が原因です。
build.gradle
apply plugin: 'scala' // スキャン対象の JPA エンティティクラスのパッケージ名 ext.modelPackage = 'sample.model' // JPA エンティティクラスのソースディレクトリ ext.modelSourceDir = 'src/main/scala-model' // Querydsl のソース生成先ディレクトリ ext.qdslDestDir = 'src/main/qdsl-generated' buildscript { // JPA エンティティクラスのビルド結果の出力先ディレクトリ // buildscript の classpath へ設定する必要があるため、ここで定義している ext.destDir = "$buildDir/classes/main" repositories { jcenter() } dependencies { classpath 'com.querydsl:querydsl-codegen:4.0.2' classpath 'com.querydsl:querydsl-scala:4.0.2' classpath 'org.scala-lang:scala-library:2.11.7' classpath 'javax:javaee-api:7.0' // コード生成時に JPA エンティティクラスをロードさせるための設定 classpath files(destDir) } } repositories { jcenter() } dependencies { compile 'com.querydsl:querydsl-jpa:4.0.2' compile 'com.querydsl:querydsl-scala:4.0.2' compile 'org.scala-lang:scala-library:2.11.7' compile 'org.apache.commons:commons-dbcp2:2.1' compile 'javax:javaee-api:7.0' } // (1) JPA エンティティクラスをコンパイル task modelCompile(type: ScalaCompile) { // ソースディレクトリ source = modelSourceDir // クラスパスの設定 (buildscript のクラスパスを設定) classpath = buildscript.configurations.classpath // クラスファイルの出力先 destinationDir = file(destDir) // 以下が必須 (ファイル名やパスは何でも良さそう) scalaCompileOptions.incrementalOptions.analysisFile = file("${buildDir}/tmp/scala/compilerAnalysis/compileCustomScala.analysis") } // (2) Querydsl JPA の Scala 用コードを生成 task generate(dependsOn: 'modelCompile') { def exporter = new com.querydsl.codegen.GenericExporter() // コード生成先ディレクトリの設定 exporter.targetFolder = file(qdslDestDir) exporter.serializerClass = com.querydsl.scala.ScalaEntitySerializer exporter.typeMappingsClass = com.querydsl.scala.ScalaTypeMappings exporter.entityAnnotation = javax.persistence.Entity exporter.embeddableAnnotation = javax.persistence.Embeddable exporter.embeddedAnnotation = javax.persistence.Embedded exporter.skipAnnotation = javax.persistence.Transient exporter.supertypeAnnotation = javax.persistence.MappedSuperclass // Scala ソースの出力 exporter.createScalaSources = true // コード生成の実施 exporter.export(modelPackage) } // (3) ソースをビルド compileScala { // generate タスクとの依存設定 dependsOn generate // Querydsl のコード生成先ディレクトリを追加 sourceSets.main.scala.srcDir qdslDestDir } clean { delete qdslDestDir }
サンプルアプリケーション
それでは簡単なサンプルアプリケーションを作成し実行してみます。
ビルド定義
先程の build.gradle へ少しだけ手を加え、EclipseLink と MySQL を使ったサンプルアプリケーション sample.SampleApp
を実行するようにしました。
build.gradle
apply plugin: 'scala' apply plugin: 'application' ext.modelPackage = 'sample.model' ext.modelSourceDir = 'src/main/scala-model' ext.qdslDestDir = 'src/main/qdsl-generated' // 実行クラス mainClassName = 'sample.SampleApp' buildscript { ・・・ } ・・・ dependencies { compile 'com.querydsl:querydsl-scala:4.0.2' compile 'com.querydsl:querydsl-jpa:4.0.2' compile 'org.scala-lang:scala-library:2.11.7' compile 'org.apache.commons:commons-dbcp2:2.1' compile 'javax:javaee-api:7.0' // 実行用の依存ライブラリ runtime 'org.eclipse.persistence:eclipselink:2.6.1-RC1' runtime 'mysql:mysql-connector-java:5.1.36' runtime 'org.slf4j:slf4j-nop:1.7.12' } ・・・
JPA エンティティクラス
「JPA における一対多のリレーションシップ - EclipseLink」 で使った JPA エンティティクラスを Scala で実装し直しました。
src/main/scala-model/sample/model/Product.scala
package sample.model import javax.persistence._ import java.util.ArrayList import java.util.List import java.math.BigDecimal @Entity class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = _ var name: String = _ var price: BigDecimal = _ @OneToMany(fetch = FetchType.EAGER, cascade= Array(CascadeType.ALL)) @JoinColumn(name = "product_id") val variationList: List[ProductVariation] = new ArrayList() override def toString = s"Product(id: ${id}, name: ${name}, price: ${price}, variationList: ${variationList})" }
src/main/scala-model/sample/model/ProductVariation.scala
package sample.model import javax.persistence._ @Entity @Table(name = "product_variation") class ProductVariation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = _ var color: String = _ var size: String = _ override def toString = s"ProductVariation(id: ${id}, color: ${color}, size: ${size})" }
実行クラス
Querydsl JPA を使った単純な検索処理を行います。
src/main/scala/sample/SampleApp.scala
package sample import sample.model.Product import sample.model.ProductVariation import sample.model.QProduct import com.querydsl.jpa.impl.JPAQuery import javax.persistence.Persistence import java.math.BigDecimal import scala.collection.JavaConversions._ object SampleApp extends App{ def product(name: String, price: BigDecimal, variationList: ProductVariation*) = { val res = new Product() res.name = name res.price = price variationList.foreach(res.variationList.add) res } def variation(color: String, size: String) = { val res = new ProductVariation() res.color = color res.size = size res } val emf = Persistence.createEntityManagerFactory("jpa") val em = emf.createEntityManager() val tx = em.getTransaction() tx.begin() val p1 = product( "sample" + System.currentTimeMillis(), new BigDecimal(1250), variation("White", "L"), variation("Black", "M") ) em.persist(p1) tx.commit() val p = QProduct as "p" val query = new JPAQuery[Product](em) // Querydsl JPA による検索 val res = query.from(p).where(p.name.startsWith("sample")).fetch() // 結果の出力 res.foreach(println) em.close() }
実行
「JPA における一対多のリレーションシップ - EclipseLink」 で使った DB や JPA 設定ファイルを使って実行します。
src/main/resources/META-INF/persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="jpa"> <class>sample.model.Product</class> <class>sample.model.ProductVariation</class> <properties> <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa_sample" /> <property name="javax.persistence.jdbc.user" value="root" /> <property name="eclipselink.logging.level" value="FINE" /> </properties> </persistence-unit> </persistence>
初回実行時や clean 直後は、modelCompile
と generate
以降のタスクを分けて実行する必要があります。 (上の方でも書きましたが buildscript
の classpath
へ build/classes/main
を設定している事が原因です)
エンティティクラスのコンパイル (modelCompile タスクの実行)
> gradle modelCompile given scan urls are empty. set urls in the configuration :modelCompile
以下のファイルが生成されます。
- src/main/qdsl-generated/sample/model/QProduct.scala
- src/main/qdsl-generated/sample/model/QProductVariation.scala
実行結果 (run タスクの実行)
> gradle run :compileJava UP-TO-DATE :modelCompile :generate :compileScala :processResources :classes :run ・・・ Product(id: 3, name: sample1, price: 100, variationList: [ProductVariation(id: 4, color: Black, size: M), ProductVariation(id: 5, color: White, size: L)]) Product(id: 4, name: sample1437821487341, price: 1250, variationList: [ProductVariation(id: 6, color: White, size: L), ProductVariation(id: 7, color: Black, size: M)])
JPA における一対多のリレーションシップ - EclipseLink
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.level
へ FINE
を設定しています。
(a) 単方向: @OneToMany + @JoinColumn
まずは、@OneToMany
と @JoinColumn
を使った単方向の一対多の関連を実現します。
エンティティクラス
product テーブル用のエンティティクラス Product
内で @OneToMany
と @JoinColumn
を使います。
@JoinColumn
の name
を使って 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
で @OneToMany
、ProductVariation
で @ManyToOne
を用います。
@OneToMany
の mappedBy
を使って @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
によって適用されてしまう) Product
と ProductVariation
の toString
を交互に呼び出し続ける事になってしまうので、@ToString(exclude = "product")
としています。
実行クラス
基本的に (a) と同じですが、ProductVariation
へ Product
を設定しなければならない点が異なります。
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 も設定するようになっています。