業務で JavaFX をちょっとだけ使ってみた

この記事は JavaFX Advent Calendar 2017 - Qiita の 21 日目です。昨日は @Yucchi_jp さんの ゆっちのBlog » JavaFX 9 で FontMetrics を取得する でした。明日はまだ空いてますのでどなたか・・・。

趣味では何年か前からちょくちょく JavaFX で遊んでたんですが、仕事では .NET メインなこともあり、 JavaFX の出番は全然ありませんでした。最近になって実案件で(ほんのちょっとだけ) JavaFX を投入する場面があったので、動機などを書いてみようと思います。

投入の動機

JavaFX の利点は「ランタイムのインストール不要で動かせる」という点です。

JavaFX で作ったのは DB アクセス含むちょっとしたデータ移行ツールだったのですが、クライアント・サーバー含め複数の環境で動かす必要がありました。

複数の環境とはいえ実は全部 Windows OS だったので .NET という手もあったのですが、 .NET だとどうしてもランタイムとして .NET Framework をインストールする必要があります。サーバーならある程度環境をコントロールできますが、クライアント環境って何が入っているかわからない*1ので、下手にランタイム入れて既存の環境が壊れてしまったりするのは避けたいというのがありました。また、 RDBMS によっては DB 接続用のコンポーネントもインストールする必要があったりして、 .NET は環境を汚しやすいのです。ということで「これは JavaFX の出番じゃね」ということで使ってみることにしました。

JavaFX ではネイティブパッケージングを使えばランタイムを同梱した状態でアプリを配布できるので、ランタイムのインストールが不要です。ビルドした実行ファイル一式を配布すればそのまま動かせます。すばらしいですね。

ネイティブパッケージング

ネイティブパッケージングについては手前味噌ですが以下にまとめてます。

ただ上記で紹介している JavaFX-Maven-Plugin は JavaFX-Maven-Plugin の現状について - notepad の通り Java9 環境はサポートしていないようですね。残念。 Java9 以降は今のところ javapackager コマンドを叩くのが良いのでしょうか?

導入してみて

実行ファイル群を配布(コピー)するだけなので実際導入は非常に楽でした。環境要因の問題も特に無く、順調に動いてくれました。ランタイムが同梱されてるので実行ファイル群のサイズが大きくなるのが難点ですかね*2

それなりの規模のアプリ開発に導入する場合はもっと色々考慮することがあると思いますが、ちょっとしたツールなんかに JavaFX を使うのは悪い選択肢ではないと思います。ランタイムの導入って結構お願いしづらかったりしますし。

JavaFX はどうにも影が薄くて、クロスプラットフォームなデスクトップアプリでは Electron やらの影に完全に隠れてる感じになっちゃってますが、もうちょっと広まってほしいですね。 Java で書けるのって、膨大な Java のライブラリも使えるし結構強みだと思うんですけどね。

*1:業務環境では特に変な制約があったりする

*2:Jigsaw でだいぶサイズを削減できるようですが: JDK9でのjavapackagerについて - AOEの日記

Thymeleaf3 + JavaScript で条件分岐

いつも忘れるのでメモ。

Thymeleaf3 では以下のように書くことで JavaScript 内で条件を指定して表示の切り替えができます。 JSP における c:if 的なやつ。

/*[# th:if="${hogehoge}"]*/
     alert('ほげ');
/*[/]*/

参考。

javafx-maven-plugin でネイティブ用アイコンを設定する

この記事は JavaFX Advent Calendar 2016 - Qiita の 13 日目です。

昨日は @boochnich さんの JavaFXのCanvasで拡大縮小と平行移動を行う(リベンジ編) - Qiita でした。明日は @y_q1m さんです。

はじめに

JavaFX の最大の魅力の一つはなんといってもクロスプラットフォームGUI アプリを作れることです。 jar 形式でなく、各プラットフォーム向けの実行ファイル形式にビルドする方法も提供されており、 Java がインストールされていない環境でも動作するアプリを作成することができます。

Java 開発といえば Maven や Gradle がデファクトになっていますが、 javafx-maven-plugin を使うと、 JavaFX 開発で Maven を使いつつプラットフォームネイティブな JavaFX アプリをビルドすることができます。 NetBeansJavaFX + Maven アプリケーションを作ると javapackager を直で使うような構成の pom が出来上がりますが、 javafx-maven-plugin の方がビルドが速いしパッケージングも楽なので、自分は専らこれを使っています。

javafx-maven-plugin を導入する際は、以下のページからぽちぽちオプションを選んでいくと pom 設定の雛形が自動生成されるので便利です。

前置きが長くなりましたが、せっかくネイティブなアプリができるのなら、アプリケーションのアイコンをデフォルトのじゃばじゃばしいやつじゃなくて、独自のものを設定したくなるものです。 Oracle の公式とかだと javapackager を ant から呼ぶみたいなサンプルしかなくて、今更 build.xml もちょっと・・・ということで、 Maven でどうやってアイコンを設定するのか調べてみました。

環境

環境は以下のとおりです。

結論

まず結論から。最低限以下のファイルを用意して mvn jfx:native すればいいです。 Linux は環境がないので調べてませんが、後述のように調べれば同様にできると思います。

  • Mac の場合
    • src/main/deploy/package/macosx/アプリ名.icns を用意する
  • Windows の場合
    • src/main/deploy/package/windows/アプリ名.ico を用意する

アプリ名は pom の build -> finalName です。詳細は以下に述べます。

アイコンファイルをどこに置くか

とりあえず Oracle の公式は以下のような感じです。

To get more insight into what resources are being used, enable verbose mode by adding the verbose="true" attribute to , or pass the -v option to the javapackager -deploy command.

なんかよくわかりませんが、ビルド時に verbose="true" を指定すれば、どういうリソースを使うのかわかるよ!って感じでしょうか。しかしここの例では ant なので、 Maven における verbose オプションの指定方法がわかりません。 mvn コマンドの -X オプションを試してみたが、これではないようです。

ここで前述の javafx-maven-plugin の公式サイトを見てみましょう。

lag to turn on verbose logging. Set this to "true", if you are having problems and want more detailed information.

そのものずばり verbose オプションを指定するスイッチがありますね。これをオンにすると pom.xml に以下のように追加されます。

	<groupId>com.zenjava</groupId>
	<artifactId>javafx-maven-plugin</artifactId>
	<version>8.6.0</version>
	<configuration>
		<mainClass></mainClass>
		<verbose>true</verbose><!-- これ -->
	</configuration>

この状態で mvn jfx:native を叩くと以下のような出力が得られます(抜粋)。 Oracle 公式にあったような感じですね。

  Using default package resource [icon]  (add package/macosx/アプリ名.icns to the class path to customize)
(略)
  Using default package resource [dmg background]  (add package/macosx/アプリ名-background.png to the class path to customize)
  Using default package resource [volume icon]  (add package/macosx/アプリ名-volume.icns to the class path to customize)

上記は Mac で試したものですが、実行環境の OS により少しパスが異なります。

どうやら Mac の場合は package/macosx/アプリ名.icns というやつを用意してクラスパスに追加すればよさそうです。他にも アプリ名-background.pngアプリ名-volume.icns とかいうのもありますね。クラスパスということですがどこに置くのがよいのか、ここで javafx-maven-plugin の公式サイトをもう一度よく読みます。

Default: ${project.basedir}/src/main/deploy
(略)
The most common usage for this is to provide platform specific icons for native bundles. In this case you need to follow the convention of the JavaFX packaging tools to ensure your icons get picked up.

なるほど、 ${project.basedir}/src/main/deploy/package/macosx を用意すれば良いっぽいですね。

アイコンファイルの用意

というわけでアイコンファイルを用意しましょう。 icns という見慣れない拡張子(当社比)が出てきましたが、これはなんでしょうか。

Mac 用のアイコンファイルのようですね。ターミナルから作れるようなので作ってみましょう。元ネタの png はなんとかして用意する*1として、各サイズは Mac のプレビューアプリからリサイズすることで作れます。

icons_256x256.png とかいろんなサイズの画像を作り、 アプリ名.iconset というフォルダに突っ込みます。しかるのちにターミナルで以下のコマンドを叩くと、 アプリ名.icns が出来上がります。

$ iconutil -c icns アプリ名.iconset

今回はめんどくさいので 256x256 だけ用意しましたが特に問題なく作成できました。ファイル名で表されたサイズと実際のファイルサイズが異なっているとうまく icns ファイルが作成されないので気をつけましょう。

出来上がったアイコンファイルを ${project.basedir}/src/main/deploy/package/macosx に配置し、改めて mvn jfx:native を叩きます。

  Using custom package resource [icon]  (loaded from package/macosx/アプリ名.icns)
(略)
  Using default package resource [dmg background]  (add package/macosx/アプリ名-background.png to the class path to customize)
  Using default package resource [volume icon]  (add package/macosx/アプリ名-volume.icns to the class path to customize)

作成したアイコンファイルが読まれているのがわかります。これでめでたくアイコンが設定できました。

インストールすると、ちゃんと Finder や Dock に指定したアイコンが表示されます。

アプリ名-volume.icns はインストーラーがマウントされたときにデスクトップに出現するボリュームアイコン用のようです。このファイルを指定するとデスクトップに以下のアイコンが現れます。今回は同じアイコンファイルを使っています。

指定しない場合、デフォルトのジャバジャバしいやつです。

アプリ名-background.pngインストーラーの背景画像のようです。このファイルを指定すると以下のようになります。

なんかうっかりひどい色になってしまいました。すいません。とりあえず画像サイズは 512x256 にしてみたのですが、微妙に合ってないですね。

ウィンドウアイコンの設定

アプリのタイトルバーに表示するアイコンは以下のようにして設定できます。これ自体はネイティブアプリとは関係ないです。

  1. アイコンファイルを src/main/resources/images/icon.png あたりに配置する
    • Java コードから参照できる場所であればどこでも良いです
  2. JavaFX メインクラスの start メソッドでアイコンパスを指定する

アイコンパスの指定は以下のようなコードになります。

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("/fxml/Scene.fxml"));

        Scene scene = new Scene(root);
        scene.getStylesheets().add("/styles/Styles.css");

        // ↓この 1 行
        stage.getIcons().add(new Image(getClass().getResourceAsStream("/images/icon.png")));

        stage.setTitle("JavaFX Icon Sample");
        stage.setScene(scene);
        stage.show();
    }

これでこんな感じになります。もうちょいはっきりしたアイコンにしないと小さくてわかりづらいですね。

Mac の場合は未設定だとアイコン自体が表示されないのでなくても特に気になりませんが、 Windows だとデフォルトのウィンドウアイコンになってしまい、残念な感じになるのでこちらも設定しておいた方が良いでしょう。

Windows の場合

同様に Windowsmvn jfx:native を実行すると以下のような出力が得られます(抜粋)。

  Using default package resource [application icon]  (add package/windows/アプリ名.ico to the class path to customize)

Windows だと設定可能なアイコンが 1 種類しかないようですね。インストーラーを作成できるようにしてあるとまた違うのかもしれませんが、そのへんは未調査です。メッセージに従い、 src/main/deploy/package/windows/アプリ名.ico を用意して再度ビルドしてみると、 target/jfx/native/アプリ名 以下に実行ファイル類が一式作成され、アイコンもちゃんと設定されていることがわかります。

以上です。

*1:とりあえずなんでもいいのであれば Placehold.jp|ダミー画像生成 モック用画像作成 でダミー画像を作るのもありかと思います。 advanced オプションで CSS も使えるので、角丸やらグラデーションやら使えばそれなりにそれっぽくもなります。

Java の enum のコンストラクタで例外を投げてはいけない

Effective Java に書かれている通り、 Java でシングルトンを作りたい場合は単一要素の enum を作成するという方法があります。そんでまぁ結構気軽に enum を使っていたんですが、コンストラクタで例外が発生しうるような実装にしてしまうと辛いということに気づきました。

例えばこんな enum を作って、

public enum SampleEnum {

    INSTANCE;

    private SampleEnum() {
        throw new RuntimeException("enumコンストラクタで例外");
    }

    public void doHoge() {
        System.out.println("doHoge");
    }
}

こんな感じで使ってみるとします。

    public static void main(String[] args) {
        try {
            System.out.println("main");
            SampleEnum.INSTANCE.doHoge();
        } catch (Exception ex) {
            System.err.println("nice catch!");
        }
    }

実行すると以下の様な出力になります。

main
Exception in thread "main" java.lang.ExceptionInInitializerError
	at sample.App.main(App.java:40)
Caused by: java.lang.RuntimeException: enumコンストラクタで例外
	at sample.SampleEnum.<init>(SampleEnum.java:14)
	at sample.SampleEnum.<clinit>(SampleEnum.java:11)
	... 1 more

catch 節に入ってないですね。これは enum のコンストラクタでスローした例外が ExceptionInInitializerError に変換*1されており、 Exceptioncatch では捕捉できないためです。 java.lang.Error をキャッチすれば捕捉可能ですが、エラーは一般的に JVM がヤバい感じの時にスローされるものなので、キャッチすべきではなく*2、通常エラーのキャッチ節を書くことも無いと思います。

これの何が辛いって、通常のアプリケーション例外処理に乗らないことです。例えば上記のように例外をキャッチしてログ出力している場合、エラーなのでキャッチできず意図したログが出ません。また、 enum のコンストラクタ内で try/catch しても、コンストラクタ内では static なロガーを参照できないのでやはりログを出力できません。

そもそも enum にかぎらずコンストラクタから例外投げるのはどうなのか、という話もありますが、こと enum に関しては特に扱いが厄介になります。 enum のコンストラクタから例外が発生しうるような実装は避けたほうが良いです。

enum のコンストラクタで例外が発生する実装ってどんなんやねん、という話ですが、 enumfinal フィールドを生やしてコンストラクタで外部ファイルから値を読み込んで設定する、みたいな作りとかですかね。アプリケーションスコープな値オブジェクトとして使えるかなと思ったんですが、読み込みに失敗した場合辛いということがわかりました。こういう場合はアプリケーションの初期化処理とかで明示的に読み込み処理を呼ぶような作りにしたほうが無難かなと思います。

*1:通常の class であればそのまま例外が飛んでくるので、 enum 特有の動作

*2:エラー発生後に JVM がまともに動作する保証はないため

Spring Security で起動時に AlreadyBuiltException: This object has already been built が発生する場合

ちょっとハマったのでメモ。

現象

Spring Boot + Spring Security で、 JavaConfig で url ベースのアクセス制御を書き換えて起動すると以下のようなエラーが発生。

2015-12-25 18:18:46.835 ERROR 5944 --- [           main] o.s.boot.SpringApplication               : Application startup failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springSecurityFilterChain' defined in class path resource [org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.servlet.Filter]: Factory method 'springSecurityFilterChain' threw exception; nested exception is org.springframework.security.config.annotation.AlreadyBuiltException: This object has already been built
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:599) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1123) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1018) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:296) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772) ~[spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:838) ~[spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:537) ~[spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118) ~[spring-boot-1.3.0.RELEASE.jar:1.3.0.RELEASE]

原因

ロール名に "ROLE_" を付けているのが原因。 JavaConfig で設定する場合、不要です。

	.antMatchers("/foo/bar/**")
	.hasRole("ROLE_HOGE") // "HOGE" だけで良い

Spring Boot で複数のログイン画面を使う

会員用のログイン画面と管理画面用のログイン画面を分けるみたいな場合に、 Spring Boot ( Spring Security )でどうやればいいのか手探りで調べたのでメモ。よくありそうな話ではありますが、意外と情報がなかった。

注意

remember me を有効にするとこの方法ではまともに動きません。 HttpSecurity を複数用意するとだめっぽいです(そもそも formLogin を複数用意するのは若干イレギュラー感もあり・・・)。 remember me を有効にしたい場合、ログイン成功時・失敗時の遷移先 URL を動的に切り替えるために以下のような手順が必要と思われます。

  • UsernamePasswordAuthenticationFilter のカスタマイズ
  • AuthenticationEntryPoint のカスタマイズ

・・・など。機会があればまたまとめるかもしれません。

ざっくりした要件

  • 会員画面と管理画面は別々のログイン画面を用意する
  • 未ログイン状態で会員画面にアクセスされた場合は会員用ログイン画面に、管理画面にアクセスされた場合は管理用ログイン画面にそれぞれ遷移する
  • 会員はログイン状態でも管理画面にはアクセスできないし、逆も然り

URL 構成

こんな感じを目標とします。ちょっと微妙ですが・・・。

種別 ログイン画面 URL ログイン実行 URL ログイン後 URL
会員用 / または /index /member/login /member/top
管理画面用 /admin/index /admin/login /admin/top

環境

  • Spring Boot 1.3.0
  • JDK 8u66

実装

Spring Boot の一般的な構成でプロジェクトを作ります。今回は STS メニューの Spring Starter Project から作成しました。 Thymeleaf と Security を使うようにしておきます。

application.yml

Basic 認証は要らないのでオフっておきます。

security:
  basic:
    enabled: false
Controller

ログイン画面表示用・ログイン後画面表示用のメソッドを定義しておきます。

@Controller
public class IndexController {

	@RequestMapping("admin/index")
	String indexForAdmin() {
		return "admin/index";
	}

	@RequestMapping("admin/top")
	String topForAdmin() {
		return "admin/top";
	}
	
	@RequestMapping(value = {"", "index"})
	String index() {
		return "index";
	}
	
	@RequestMapping("member/top")
	String top() {
		return "member/top";
	}
}
画面

src/main/resources/templates 以下に、 Controller で指定した html を配置しておきます。

  • admin/index.html
  • admin/top.html
  • member/top.html
  • index.html

会員用ログイン画面の html はこんな感じです。管理画面用も th:action の値が違うぐらいです。ここの値は後述の JavaConfig で指定します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>ログイン</title>
</head>
<body>
	<p>会員用ログイン</p>
	<form method="post" th:action="@{/member/login}">
		<p th:if="${param.error}">失敗</p>
		<input type="text" name="loginId" required="required" />
		<input type="password" name="password" required="required" />
		<input type="submit" value="ろぐいん" />
	</form>
</body>
</html>

ログイン後トップ画面はなんでもいいです。

Security 用 JavaConfig クラス

configureGlobal メソッドでインメモリのユーザーを用意しています。 /member/** には USER 権限のユーザーのみ、 /admin/** には ADMIN 権限のユーザーのみアクセスを許可します。

@EnableWebSecurity
public class SecurityConfig {

	// 会員用設定
	@Configuration
	@Order(1)
	public static class MemberConfigurerAdapter extends WebSecurityConfigurerAdapter {
		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.antMatcher("/member/**") // ポイント?
				.authorizeRequests()
					.antMatchers("/member/**")
					.hasRole("USER") // USER 権限のみアクセス可
					.anyRequest()
					.authenticated()
				.and()
					.formLogin()
					.loginProcessingUrl("/member/login")
					.loginPage("/index")
					.failureUrl("/index?error")
					.defaultSuccessUrl("/member/top")
					.usernameParameter("loginId")
					.passwordParameter("password")
				.and()
					.logout()
					.logoutUrl("/**/logout")
					.logoutSuccessUrl("/index")
					.deleteCookies("JSESSIONID")
				.and()
					.csrf()
					.disable();
		}		
	}
	
	// 管理画面用設定
	@Configuration
	@Order(2)
	public static class AdminConfigurerAdapter extends WebSecurityConfigurerAdapter {
		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.antMatcher("/admin/**") // ポイント?
				.authorizeRequests()
					.antMatchers("/admin/index") // ログイン画面は未ログインでもアクセス可
					.permitAll()
					.antMatchers("/admin/**")
					.hasRole("ADMIN") // ADMIN 権限のみアクセス可
					.anyRequest()
					.authenticated()
				.and()
					.formLogin()
					.loginProcessingUrl("/admin/login")
					.loginPage("/admin/index")
					.failureUrl("/admin/index?error")
					.defaultSuccessUrl("/admin/top")
					.usernameParameter("loginId")
					.passwordParameter("password")
				.and()
					.logout()
					.logoutUrl("/**/logout")
					.logoutSuccessUrl("/admin/index")
					.deleteCookies("JSESSIONID")
				.and()
					.csrf()
					.disable();
		}
	}

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		// インメモリのユーザーを用意
		auth
			.inMemoryAuthentication()
			.withUser("user").password("pass").roles("USER");
		auth
			.inMemoryAuthentication()
			.withUser("admin").password("pass").roles("ADMIN");
	}
}

とりあえずはこれで意図した動きになりました。が、いまいち信頼できる資料がなかったので、これでいいのかはちょっと自信ないです・・。まずそうな部分があったら指摘していただけると幸いです。

ポイントは以下でしょうか。

  • 会員用・管理画面用でそれぞれ WebSecurityConfigurerAdapter の実装クラスを用意する
    • 一つの実装でやろうとするとうまく動かない(なぜかはよくわからない・・・)
  • WebSecurityConfigurerAdapter の実装クラスに、 @Order でそれぞれ異なる順番を指定する
    • これをしないと起動時にはねられる
    • 指定するのはどっちか片方だけでもよい
  • configure メソッドの最初に HttpSecurity.antMatcher(String) を必ず呼び出す
    • これをしないと、最初に読まれる WebSecurityConfigurerAdapter 実装だけが使われるっぽい

JPA + PostgreSQL でエンティティからテーブル生成 + ID 発番

PostgreSQL に対し、 JPA のエンティティからテーブルを自動生成して ID 列を自動採番する方法を試してみました。

今回は Spring Boot 1.3.0 を使っています。 JPA 実装は Hibernate 4.3.11.Final のようです*1

JPA のエンティティがこんな感じ。

@Entity
@Table(name = "account")
public class Account {
	
	@Id
	@SequenceGenerator(name = "account_id_gen", sequenceName = "account_id_seq")
	@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "account_id_gen")
	private Long id;
	
	@Column(nullable = false)
	private String loginId;

        // getter/setter 略
}

GeneratedValue に加え、SequenceGenerator を指定するのがポイントっぽいです。オプションなしの GeneratedValue のみの場合、 hibernate_sequence というシーケンスが作られるようですが、さすがに明示的に指定したほうが良さそうです。

Spring Boot を使っているので、 application.yml にデータベース接続定義を書きます。ユーザーやデータベース自体は予め作成しておくものとします。

spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop
  datasource:
    url: jdbc:postgresql://localhost:5432/sampledb
    driverClassName: org.postgresql.Driver
    username: testuser
    password: password

これでアプリケーションを起動し、 psql でデータベースの状態を確認します。以下のような感じでテーブルとシーケンスが作成されることが確認できます。

sampledb=> \d
                リレーションの一覧
 スキーマ |      名前      |     型     | 所有者
----------+----------------+------------+---------
 public   | account        | テーブル   | testuser
 public   | account_id_seq | シーケンス | testuser
(2 行)


sampledb=> \d account
                                テーブル "public.account"
    列    |           型           |                        修飾語
----------+------------------------+------------------------------------------------------
 id       | bigint                 | not null default nextval('account_id_seq'::regclass)
 login_id | character varying(255) | not null
インデックス:
    "account_pkey" PRIMARY KEY, btree (id)

シーケンスが作成され、 id 列のデフォルト値として設定されていることがわかります。この状態で JPA でレコード登録を行うと、以下のような SQL が出力されます。デフォルト値が使われていますね。

Hibernate: insert into account (login_id) values (?)

なお、 GenerationType.SEQUENCE を指定した場合、 id 列のデフォルト値が設定されませんでした。

sampledb=> \d account
          テーブル "public.account"
    列    |           型           |  修飾語
----------+------------------------+----------
 id       | bigint                 | not null
 login_id | character varying(255) | not null
インデックス:
    "account_pkey" PRIMARY KEY, btree (id)

しかしこの状態で JPA で登録を行ってもちゃんと登録されます。 SQL のログを見ると以下のようになっていました。一度シーケンスから値を取得してから insert するようです。検索すると SEQUENCE を使ってる例が結構出てきますが、どっちがいいんだろう?

Hibernate: select nextval ('account_id_seq')
Hibernate: insert into account (login_id, id) values (?, ?)

自動採番される id は RDBMS によって JPA のエンティティの定義方法が微妙に変わってくるところがちょっとめんどうですね。

*1:Spring Boot の場合とそうでない場合で挙動が違うかどうかは調べていません