SpringBoot-JPA-Hibernateのハマりメモ


この記事について

この記事は自分がアプリケーションの開発中にハマった点と、発見できた場合は原因、解決方法をまとめたものです。
この記事で問題としているものの、そもそもの利用法が間違っているケースや、解決方法にもっと良いものがあるかもしれません。

環境

  • Spring Boot 2.2.4
  • Hibernate 5.4.10
  • MySQL 8.0.16

遭遇した問題たち

PK以外をキーにした関連を作るとエラーが出る

データベース上では普通にあり得る話なのですが、Hibernateの実装上問題が発生することがあります。

ユースケース

@Entity
class Parent {
    @Id
    Long id;
    @NatulalId
    Integer key;
    String firstName;
    String lastName;
}

@Entity
class Child {
   @Id
   Long id;
   @ManyToOne
   @JoinColumn(name = "parent_key",referencedColumnName = "key")
   Parent parent;
}

上記のような、Hibernate自身が推すPKをサロゲートキー、@NaturalIdで自然キーを指定するような利用法のとき、JPA仕様の範囲内だとkeyがUniqueを指定されており、idの代わりに外部キーとして機能させたい場合です。
自然キーで関連付けを行うとchild.getParent()などの実行時にキャスト絡みでエラーが発生します。1

原因

公式issue tracker HHH-7668

解決

参照されるエンティティ(上記だとParent)をSirializableにする。
これを鑑みるに、Hibernateで使用するエンティティはすべてSirializableである方が無難かもしれません。

issue trackerをざっと眺めたところ、PKではないUniqueカラムで関連付けするとこれ以外の不具合にも遭遇する可能性がある模様です。

2020/05/28追記 エンティティのSirializableについて

JPAの仕様上、エンティティがDBとだけやり取りされるときにSirializableは必要ありませんが、HTTPセッションにオブジェクトとして保存するなど、JavaEEの範囲で使用される場合にはエンティティのSirializableが必要になる、という事のようです。
思わぬところでエラーが出ることを考えると、Sirializableになっている方が望ましいようです。

FetchType.LazyのエンティティがJacksonによるシリアライズ時にエラーを出す

原因

Jacksonがシリアライズを行う時にアクセスしているLazy指定のプロパティの実体はHibernateのプロキシであるため

解決

Hibernateのバージョンに対応するJacksonのプラグインを追加する。
初期状態でSpringBootはHibernateとJacksonに依存しているのになぜこれは入っていないんだろう。2
Stack Overflow:No serializer found for... って?
Stack OverFlow:SpringのJacksonでLazy属性を抑制する設定
みんな苦労してます。

Spring JPAエンティティのJacksonシリアライズがLazyもトリガする問題の解決
上記が非常にわかりやすく説明と解決方法を示してくれます。

というわけで念のためにプラグインのGitHubです。内容はあまり親切じゃないです。
ついでに、上記GitHubで紹介されているJacksonのアノテーションによる出力制御解説(無限ループ回避策)

カラム名やテーブル名関連のエラーが出る(追記・修正2020-02-21)

これは仕様の話ですね。バグではありません。

原因

デフォルト命名戦略はJPAアノテーションよりも後に処理される

単に優先順位の問題で、JPA(≒Hibernate)のテーブル名やカラム名を指定するアノテーションが使用されている場合、フィールド名からのカラム名変換が処理されていないため、エラーになります。

仕様の理解が足りていませんでした。
Hibernateでは、2段階の変換を経てDBのカラム名が生成されます。

  1. エンティティ名、フィールド名から論理名を生成する
  2. 論理名を物理名に変換する

現在のSpringのデフォルトでは、

  1. 論理名の生成

    • クラス名、フィールド名を論理名として取得する
    • @Column@Tableで指定されていれば、その文字列を論理名にする
  2. 物理名の生成

    • ピリオドをアンダースコアに置き換える
    • キャメルケースをスネークケースに置き換える

という設定になっています。

@Column@Tableで指定された文字列を論理名にするため、フィールドやクラス名をキャメルケースで指定しているなら、アノテーションで指定する名前もキャメルケースにしなくてはなりません。ここで物理名を(つまりスネークケース)を指定していると、"Table [table name] contains physical column name [column name] referred to by multiple logical column names:"(テーブルの物理カラム名に対応する論理名2個あるんですけど)とエラーが出ます。

解決

JPAのアノテーションでカラム名等を指定している場合、フィールドに@Columnでnameを指定しておかなくてはなりません。思ったより手抜きができません。3
仕様とエラーメッセージはきちんと読みましょう。
物理名の生成ですべて小文字にしてしまうため、大文字のカラム名やテーブル名を使用するために使用するのが、

apprication.yaml

spring:
  jpa:
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

という設定(論理名をそのまま物理名にする)になります。
ただし、Springがデフォルトで使用しているSpringImplicitNamingStrategyはHibernateのデフォルトであるImplicitNamingStrategyJpaCompliantImplをほぼそのまま使用しているため、キャメルケースのプロパティ名を使用しているとそのままキャメルケースのカラム名になってしまいます。
stackoverfrow:Hibernateの命名戦略の実例を参考に最も近いものを選んでアノテーションで調整することになるかと思います。
Hibernateの戦略を流用するだけでは足りない場合には、戦略を自分で実装することになります。
参考:
PhysicalNamingStrategyで調整
ImplicitNamingStrategyJpaCompliantImplで調整

関連エンティティとキーの実値とをそれぞれ別プロパティに持ちたい

これは問題ではなくTipsですね。
キーの実値をプロパティに保持しつつ、必要になったときだけ関連をたどりたいという都合のいいエンティティの作り方です。

解決

@JoinColumnで関連エンティティをupdate,insert不可にする

@Entity
class Parent {
    @Id
    Long id;
    @NatulalId
    Integer key;
    String firstName;
    String lastName;
}

@Entity
class Child {
   @Id
   Long id;
   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "parent_key",referencedColumnName = "key",
        insertable = false, updatable = false)
   Parent parent;
   @Column(name="parent_key")
   Integer parentKey;
}

上記のようなクラスにすることでキーを直接保持しつつ親の参照が可能になり、子が親を更新してしまうのも防げます。
参照先を更新する際にはキーになっているフィールドの値を直接変更します(この例だとparentKey)。
マスターデータを参照するオブジェクトのとき、この形がとてもしっくりきます。


  1. 必ず発生するものでもないようで、そのあたりの疑問もたびたび投稿されているようです。 

  2. 記事を書いてる途中で気付きましたがSpring Data REST WebMVCには入ってますね…なんでや 

  3. Hibernate側のカスタマイズで解決できそうな気はします。