6.3. データベースアクセス(JPA編)

Todo

TBD

本章では以下の内容について、現在精査中である。

  • persistence.xml の設定項目について。
  • QueryDSLを使用した動的Queryの実装方法について。
  • 関連エンティティのfetch方法を指定する設定値をデフォルト値から変更した方がよいケースの具体例について。
  • 複数のPersistenceUnitを使用する方法について。
  • Nativeクエリの使用方法について。

6.3.1. Overview

本節では、JPAを使ってデータベースにアクセスする方法について、説明する。
本ガイドラインでは、JPAプロバイダとしてHibernateを、JPAのラッパーとしてSpring Data JPAを使用することを前提としている。
Target of description

Picture - Target of description

Warning

本節で説明した内容がEclipseLinkなど、Hibernate以外のJPAプロバイダでも動作することは保証しない。


6.3.1.1. JPAについて

JPA(Jakarta Persistence API)は、リレーショナルデータベースで管理されているレコードを、Javaオブジェクトにマッピングする方法と、マッピングされたJavaオブジェクトに対して行われた操作を、リレーショナルデータベースのレコードに反映するための仕組みをJavaのAPI仕様として定義したものである。

JPAは、仕様の定義をしているだけで、実装は提供していない。
JPAの実装は、HibernateのようなO/R Mapperを開発しているベンダーによって、参照実装として提供されている。
このように、O/R Mapperを開発しているベンダーによって実装された参照実装のことを、JPAプロバイダと呼ぶ。

6.3.1.1.1. JPAのO/R Mapping

JPAを使用した際の、リレーショナルデータベースで管理されているレコードとJavaオブジェクトは、以下のようなイメージでマッピングされる。

Image of O/R Mapping

Picture - Image of O/R Mapping

JPAでは、「管理状態」と呼ばれる状態のEntityが保持している値を、変更(setterメソッドを呼び出して値を変更)した場合、変更内容が、リレーショナルデータベースに反映される仕組みになっている。
これは、編集機能をもつテーブルビューアなどのクライアントソフトウェアに、よく似ている仕組みである。
テーブルビューアなどのクライアントソフトウェアでは、ビューア上の値を変更すると、データベースに反映されるが、JPAでは、Entityと呼ばれるJavaオブジェクト(JavaBean)の値を変更すると、データベースに反映されることになる。

6.3.1.1.2. JPAの基本用語

以下に、JPAを使う上で、最低限知っていてほしい用語について、簡単に説明する。

項番

用語

説明

Entityクラス
リレーショナルデータベースで管理されているレコードを表現するJavaクラス。
@jakarta.persistence.Entityアノテーションを付与されたクラスが、Entityクラスとなる。
EntityManager
Entityのライフサイクルを管理するために、必要なAPIを提供するインタフェース。
アプリケーションは、jakarta.persistence.EntityManagerのメソッドを使用して、リレーショナルデータベースで管理されているレコードを、Javaオブジェクトとして操作する。
Spring Data JPAを使う場合は、直接、使用することはないが、Spring Data JPAの仕組みでは表現できないような、Queryを発行する必要がある場合は、このインタフェースを経由してEntityを取得することになる。
TypedQuery
Entityを検索するためのAPIを提供するインタフェース。
アプリケーションは、jakarta.persistence.TypedQueryのメソッドを使用して、ID以外の条件に一致するEntityを検索する。
Spring Data JPAを使う場合は、直接使用することはないが、Spring Data JPAの仕組みでは、表現できないようなQueryを発行する必要がある場合は、このインタフェースを使用してEntityを検索することになる。
条件に一致する永続層(DB)のEntityを直接操作(更新または削除)するためのメソッドもこのインタフェースに用意されている。
PersistenceContext
Entityを管理するための領域。
EntityManagerを経由して取得または作成されたEntityは、この領域に格納されてライフサイクル管理される。この領域で管理されているEntityの事を「管理状態のEntity」と呼ぶ。
この領域は、アプリケーションから直接アクセスすることはできない。
Entityの状態は「管理状態」以外に、「作成状態」「削除状態」「分離状態」が存在する。
findメソッド
管理状態のEntityを取得するためのメソッド。
IDに対応するEntityがPersistenceContextに存在しない場合は、リレーショナルデータベースに格納されているレコードを取得(SELECT)し、管理状態のEntityを生成する。
persistメソッド
アプリケーションで作成した作成状態のEntityを、管理状態のEntityにするためのメソッド。
EntityManagerのメソッドとして提供されており、リレーショナルデータベースにレコードのINSERTを実行するための操作がPersistenceContextに蓄積される。
mergeメソッド
PersistenceContextで管理されていない分離状態のEntityを、管理状態のEntityにするためのメソッド。
EntityManagerのメソッドとして提供されており、基本的にはリレーショナルデータベースに格納されているレコードに対して、UPDATEを実行するための操作がPersistenceContextに蓄積される。
ただし、リレーショナルデータベースにIDに一致するレコードが存在しない場合は、UPDATEではなくINSERTが実行される。
removeメソッド
管理状態のEntityを、削除状態のEntityにするためのメソッド。
EntityManagerのメソッドとして提供されており、リレーショナルデータベースに格納されているレコードに対して、DELETEを実行するための操作がPersistenceContextに蓄積される。
flushメソッド
PersistenceContextで管理されているEntityに対して行われた操作を、リレーショナルデータベースに強制的に反映するためのメソッド。
EntityManagerのメソッドとして提供されており、蓄積された未反映の操作をリレーショナルデータベースに対して実行する。
通常、リレーショナルデータベースへの反映は、トランザクションコミット時に行われるが、コミットより前に反映する必要がある場合は、メソッドを使用する。

6.3.1.1.3. Entityのライフサイクル管理

Entityのライフサイクル管理イメージは、以下の通りである。

Life cycle of entity

Picture - Life cycle of entity

項番

説明

(1)
EntityManagerのpersistメソッドを呼び出すと、引数に渡したEntity(作成状態のEntity)が、管理状態のEntityとして、PersistenceContextに格納される。
(2)
EntityManagerのfindメソッドを呼び出すと、引数に渡したIDをもつ管理状態の、Entityが返却される。
PersistenceContextに存在しない場合は、Queryを発行して、リレーショナルデータベースよりマッピング対象のレコードを取得し、管理状態のEntityとして格納する。
(3)
EntityManagerのmergeメソッドを呼び出すと、引数に渡したEntity(分離状態のEntity)の状態が、管理状態のEntityにマージされる。
PersistenceContextに存在しない場合は、Queryを発行してリレーショナルデータベースよりマッピング対象のレコードを取得し、管理状態のEntityを格納した後に、引数に渡されたEntityの状態がマージされる。
このメソッドを呼び出した場合、persistメソッドとは異なり、引数に渡したEntityが管理状態のEntityとして格納されるわけではないという点に注意すること。
(4)
EntityManagerのremoveメソッドを呼び出すと、引数に渡した管理状態のEntityが削除状態のEntityとなる。
このメソッドを呼び出した場合、削除状態となったEntityを取得することはできなくなる。
(5)
EntityManagerのflushメソッドを呼び出すと、persist、merge、removeメソッドによって、蓄積されたEntityへの操作が、リレーショナルデータベースに反映される。
このメソッドを呼び出すことで、Entityに対して行った変更内容が、リレーショナルデータベースのレコードに同期される。
ただし、リレーショナルデータベース側のレコードに対してのみ行われた変更は、Entityには同期されない

EntityManagerのfindメソッドを使わずにQueryを発行してEntityを検索すると、検索処理を行う前に、EntityManager内部の処理でflushメソッドと同等の処理が実行され、蓄積されていたEntityへの操作がリレーショナルデータベースに反映される仕組みになっている。
Spring Data JPAを使用した際の永続操作の反映タイミングについては、
を参照されたい。

Note

他のライフサイクル管理用のメソッドについて

EntityManagerには、Entityのライフサイクルを管理するためのメソッドとして、detachメソッド、refreshメソッド、clearメソッドなどがあるが、Spring Data JPAを使用する場合、デフォルトの機能で、これらのメソッドを呼び出す仕組みが存在しないため、メソッドの役割のみ説明しておく。

  • detachメソッドは、管理状態のEntityを分離状態のEntityにするためのメソッド。

  • refreshメソッドは、管理状態のEntityをリレーショナルデータベースの状態で最新化するためのメソッド。

  • clearメソッドは、PersistenceContextで、管理されているEntityおよび蓄積された操作をメモリ上から破棄するためのメソッド。

clearメソッドについては、Spring Data JPAより提供されている@ModifyingアノテーションのclearAutomatically属性を、trueに設定することで、呼び出すことができる。

詳細については、永続層のEntityを直接操作するを参照されたい。

Note

作成状態と分離状態のEntityへの操作について

作成状態のEntityや、分離状態のEntityに行った操作は、persistメソッドまたはmergeメソッドを呼び出さないと、リレーショナルデータベースには反映されない。


6.3.1.2. Spring Data JPAについて

Spring Data JPAは、JPAを使ってRepositoryを作成するための、ライブラリを提供している。

Spring Data JPAを使用すると、Queryメソッドと呼ばれるメソッドを、Repositoryインタフェースに定義するだけで、
指定した条件に一致するEntityを取得することが出来るため、Entityの操作を行うための実装を減らすことができる。
ただし、Queryメソッドで定義できるのはアノテーションで表現できる静的なQueryのみなので、
動的Queryなどのアノテーションで表現できないQueryについては、カスタムRepositoryクラスの実装が必要となる。

Spring Data JPAを使ってデータベースにアクセスする際の基本フローを以下に示す。

Basic flow of Spring Data JPA

Picture - Basic flow of Spring Data JPA

項番

説明

(1)
Serviceから、Repositoryインタフェースのメソッドを呼び出す。
メソッドの呼び出しパラメータとして、Entityオブジェクト、EntityのIDなどが渡される。上記例ではEntityを渡しているが、プリミティブな値となることもある。
(2)
Repositoryインタフェースを動的に実装したProxyクラスは、org.springframework.data.jpa.repository.support.SimpleJpaRepositoryや、 カスタムRepositoryクラスに処理を委譲する。
Serviceから指定されたパラメータが渡される。
(3)
Repositoryの実装クラスは、JPAのAPIを呼び出す。
Serviceから指定されたパラメータや、Repositoryの実装クラスで生成したパラメータなどが渡される。
(4)
HibernateのJPA参照実装は、 HibernateのコアAPIの処理を呼び出す。
Repositoryの実装クラスから指定されたパラメータやHibernateのJPA参照実装のクラスで生成したパラメータなどが渡される。
(5)
HibernateのコアAPIは、指定されたパラメータからSQLとバインド値を生成しJDBCドライバに渡す。
(実際の値のバインドは、java.sql.PreparedStatement のAPIが使われている)
(6)
JDBCドライバは、渡されたSQLとバインド値をデータベースに送信することで、SQLを実行する。
Spring Data JPAを使用してRepositoryを作成する場合、JPAのAPIを直接呼び出す必要はないが、Spring Data JPAのRepositoryインタフェースのメソッドが、
JPAのどのメソッドを呼び出しているのかは、意識しておいた方がよい。
以下に、Spring Data JPAの、Repositoryインタフェースの代表的なメソッドが、JPAのどのメソッドを呼び出しているのかを示す。
API Mapping of Spring Data JPA and JPA

Picture - API Mapping of Spring Data JPA and JPA


6.3.2. How to use

6.3.2.1. pom.xmlの設定

インフラストラクチャ層にJPA(Spring Data JPA)を使用する場合、以下のdependencyを、pom.xmlに追加する。

<!-- (1) -->
<dependency>
    <groupId>org.terasoluna.gfw</groupId>
    <artifactId>terasoluna-gfw-jpa-dependencies</artifactId>
    <type>pom</type>
</dependency>

項番

説明

(1)
JPAに関連するライブラリ群が定義してあるterasoluna-gfw-jpa-dependenciesを、dependencyに追加する。

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。


6.3.2.2. アプリケーションの設定

6.3.2.2.1. データソースの設定

データベースの接続情報をデータソースに設定する。
データソースの設定については、共通編のデータソースの設定を参照されたい。

6.3.2.2.2. EntityManagerの設定

EntityManagerを使用するための設定を行う。

ここではJPA用のブランクプロジェクトの設定をもとに解説を行う。

  • XxxInfraConfig.java

    @Configuration
    public class XxxInfraConfig {
    
        // (1)
        @Bean("jpaVendorAdapter")
        public HibernateJpaVendorAdapter jpaVendorAdapter() {
            HibernateJpaVendorAdapter bean = new HibernateJpaVendorAdapter();
            bean.setDatabase(Database.POSTGRESQL); // (2)
            return bean;
        }
    
        // (3)
        @Bean("entityManagerFactory")
        public LocalContainerEntityManagerFactoryBean entityManagerFactory(
                @Qualifier("dataSource") DataSource dataSource) {
    
            LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
            bean.setPackagesToScan("xxx.yyy.zzz.domain.model"); // (4)
            bean.setDataSource(dataSource); // (5)
            bean.setJpaVendorAdapter(jpaVendorAdapter()); // (6)
    
            Map<String, Object> param = new LinkedHashMap<String, Object>();
            param.put("hibernate.connection.charSet", "UTF-8");
            param.put("hibernate.format_sql", false);
            param.put("hibernate.use_sql_comments", true);
            param.put("hibernate.jdbc.batch_size", 30);
            param.put("hibernate.jdbc.fetch_size", 100);
            bean.getJpaPropertyMap().putAll(param); // (7)
    
            return bean;
        }
    

    項番

    説明

    (1)
    JPAプロバイダが提供する実装クラスとのアダプタクラスを指定する。
    JPAプロバイダとしてHibernateを使用するため、org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapterを指定する。
    (2)
    使用するRDBMSに対応する値を設定する。org.springframework.orm.jpa.vendor.Database列挙型に定義されている値を指定することができる。
    設定例では「PostgreSQL」を指定している。
    【プロジェクトで使用するデータベースに対応する値に変更が必要】
    環境によって使用するデータベースが変わる場合は、プロパティファイルに値を定義すること。
    (3)
    jakarta.persistence.EntityManagerFactoryのインスタンスを作成するFactoryBeanのクラスを指定する。
    ここでは、org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBeanを指定している。
    (4)
    Entityクラスが格納されているパッケージを指定する。
    指定したパッケージに格納されているEntityクラスが、jakarta.persistence.EntityManagerで管理することができるEntityクラスとなる。
    【プロジェクトのパッケージに変更が必要】
    (5)
    永続層(DB)にアクセスする際に使用するデータソースを指定する。
    設定済みのデータソースのbeanを指定する。
    (6)
    JpaVendorAdapterのbeanを指定する。
    (1)で設定済みのbeanを指定する。
    (7)
    Hibernateから提供されているEntityManagerの動作設定を指定する。
    この設定例はJPA用のブランクプロジェクトで設定されているものであるため、プロジェクトの要件に合わせて設定を変更されたい。
    詳細については「Developing Hibernate Applications」を参照されたい。
  • EntityManagerの動作設定について

    ここでは上記の設定例(7)で指定しているプロパティについて説明する。

    プロパティ名

    説明

    hibernate.connection.charSet

    JDBC接続時の文字エンコーディングを指定する設定。

    hibernate.format_sql

    発行されたSQLを出力する際に、読みやすい形にフォーマットする設定。

    hibernate.use_sql_comments

    発行されたSQL内にコメントを生成するデバック用の設定。
    実行されたSQLからJPQLを特定する際などは有効にしておくと効率的である。
    本番環境では、コメントの生成によるオーバーヘッドを考慮した上で必要に応じて設定されたい。

    hibernate.jdbc.batch_size

    JDBCのバッチサイズを指定する設定。

    hibernate.jdbc.fetch_size

    JDBCのフェッチサイズを指定する設定。

    Note

    EntityManagerの動作設定でhibernate.use_sql_commentstrueを指定する場合、利用しているHibernateのバージョンによってはCVE-2020-25638で報告されているセキュリティの脆弱性が発生する可能性がある。

    なお、TERASOLUNA Server Framework for Java (5.x)の本バージョンで利用しているHibernateでは脆弱性の対策が行われているためhibernate.use_sql_commentsを利用しても問題は発生しない。

    Tip

    データベースにOracleを使う際に、テーブル結合を行うSQLにANSI標準のJOINを使用したい場合は、(7)のjpaPropertyMapに、以下の設定を指定することで実現できる。

    @Bean("entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            @Qualifier("dataSource") DataSource dataSource) {
    
        Map<String, Object> param = new LinkedHashMap<String, Object>();
    
        // omitted
    
        param.put("hibernate.dialect", "org.hibernate.dialect.OracleDialect"); // (8)
        bean.getJpaPropertyMap().putAll(param);
    
        return bean;
    }
    

    項番

    説明

    (8)
    hibernate.dialectorg.hibernate.dialect.OracleDialectを指定する。
    OracleDialectを指定することで、テーブル結合を行うSQLにANSI標準のJOIN句が使用される。

6.3.2.2.3. PlatformTransactionManagerの設定

ローカルトランザクションを使用する場合は、以下の設定を行う。

  • XxxEnvConfig.java

    // (1)
    @Bean("jpaTransactionManager")
    public TransactionManager jpaTransactionManager(
            @Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager bean = new JpaTransactionManager();
        bean.setEntityManagerFactory(entityManagerFactory); // (2)
        return bean;
    }
    

    項番

    説明

    (1)

    org.springframework.orm.jpa.JpaTransactionManagerを指定する。 このクラスは、JPAのAPIを呼び出してトランザクション制御を行う。

    (2)
    トランザクション内で使用するEntityManagerの、Factoryを指定する。
    設定済みのEntityManagerFactoryのbeanを指定する。

6.3.2.2.4. persistence.xmlの設定

LocalContainerEntityManagerFactoryBeanを使用する場合は、persistence.xmlに必ず設定しなくてはいけない設定項目はない。

Todo

TBD

現時点では、persistence.xmlに必ず設定しなくてはいけない設定項目はないが、今後増える可能性はある。

また、Jakarta EEのアプリケーションサーバー上のEntityManagerFactoryを使用する場合は、persistence.xmlに、設定が必要となると思われるので、Jakarta EEのアプリケーションサーバー上のEntityManagerFactoryを使用する場合の設定については、今後整備する予定である。


6.3.2.2.5. Spring Data JPAを有効化するための設定

  • XxxInfraConfig.java

    @Configuration
    @EnableJpaRepositories(basePackages = "xxx.yyy.zzz.domain.repository") // (2)
    public class XxxInfraConfig {
    
        // omitted
    

    項番

    説明

    (2)
    RepositoryインタフェースおよびカスタムRepositoryクラスが格納されているベースパッケージを指定する。
    org.springframework.data.repository.Repositoryを継承しているインタフェースと、org.springframework.data.repository.RepositoryDefinitionアノテーションが付与されているインタフェースが、 Spring Data JPAのRepositoryクラスとして自動的にbean定義される。
  • @EnableJpaRepositoriesアノテーションの属性について

    属性として、entityManagerFactoryRef、transactionManagerRef、namedQueriesLocation、queryLookupStrategy、repositoryFactoryBeanClass、repositoryImplementationPostfixが存在する。

    項番

    要素

    説明

    entityManagerFactoryRef

    Repositoryで使用するEntityManagerを生成するためのFactoryを指定する。
    通常指定する必要はないが、EntityManagerのFactoryを複数用意する場合は、使用するbeanを指定する必要がある。

    transactionManagerRef

    Repositoryのメソッドが呼び出された際に使用するPlatformTransactionManagerを指定する。
    デフォルトはtransactionManagerというbean名で登録されているbeanが使用される。
    使用するPlatformTransactionManagerのbean名がtransactionManagerでない場合は指定が必要である。

    namedQueriesLocation

    Named Queryが指定されているSpring Data JPAのプロパティファイルのロケーションを指定する。
    デフォルトは「classpath:META-INF/jpa-named-queries.properties」が使用される。

    queryLookupStrategy

    Queryメソッドが呼び出された特に実行するQueryをLookupする方法を指定する。
    デフォルトはCREATE_IF_NOT_FOUNDとなっている。詳細は、Spring Data Commons - Reference Documentationの “Query lookup strategies”を参照されたい。 特に理由がない場合は、デフォルトのままでよい。

    repositoryFactoryBeanClass

    Repositoryインタフェースのメソッドが呼び出された際の処理を実装するクラスを生成するためのFactoryを指定する。
    デフォルトでは、org.springframework.data.jpa.repository.support.JpaRepositoryFactoryが使用される。Spring Data JPAのデフォルト実装を変更する場合や、新しいメソッドを追加する場合に作成したFactoryを指定する。
    新しいメソッドを追加する方法については、すべてのRepositoryインタフェースに一括で追加するを参照されたい。

    repositoryImplementationPostfix

    カスタムRepositoryの実装クラスであることを表す接尾辞を指定する。
    デフォルトはImplとなっている。例えば、Repositoryインタフェースの名前がOrderRepositoryの場合は、OrderRepositoryImplがカスタムRepositoryの実装クラスとなる。特に理由がない場合は、デフォルトのままでよい。
    カスタムRepositoryについては、「Entity毎のRepositoryインタフェースに個別に追加する」を参照されたい。

6.3.2.2.6. JPAのアノテーションを使用するための設定

JPAから提供されているアノテーション(jakarta.persistence.PersistenceContextと、jakarta.persistence.PersistenceUnit)を使用して、 jakarta.persistence.EntityManagerFactoryjakarta.persistence.EntityManagerをInjectするためには、org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessorをbean定義する必要がある。

@EnableJpaRepositoriesアノテーションまたは<jpa:repositories>要素を指定した場合、デフォルトでbeanが定義されるため、bean定義の必要はない。

6.3.2.2.7. JPAの例外を DataAccessExceptionに変換するための設定

JPAの例外をSpring Frameworkから提供されているDataAccessExceptionに変換するためには、org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessorをbean定義する必要がある。

@EnableJpaRepositoriesアノテーションまたは<jpa:repositories>要素を指定した場合、デフォルトでbeanが定義されるため、bean定義の必要はない。

6.3.2.2.8. OpenEntityManagerInViewInterceptorの設定

ControllerやJSP等のアプリケーション層でEntityのLazy Fetchを行うためには、org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptorを使用して、EntityManagerの生存期間をアプリケーション層まで伸ばす必要がある。

Lifetime of EntityManager on OpenEntityManagerInViewInterceptor

Picture - Lifetime of EntityManager on OpenEntityManagerInViewInterceptor

OpenEntityManagerInViewInterceptorを使用しない場合は、EntityManagerの生存期間はトランザクションと同じになるため、アプリケーション層で必要となるデータをServiceクラスの処理としてFetchするか、Lazy Fetchを使わずにEager Fetchを使用する必要がある。

Default lifetime of EntityManager

Picture - Default Life time of EntityManager

下記の点から、基本的にはFetch方法はLazy Fetchとして、OpenEntityManagerInViewInterceptorを使用することを推奨する。

  • Serviceクラスの処理としてFetchした場合、getterメソッドを呼び出すだけの処理やgetterメソッドへアクセスしたコレクションへのアクセスなど、一見意味のない処理を実装することになってしまう。

  • Eager Fetchにした場合、アプリケーション層で使用しないデータへのFetchも行われる可能性があるため、性能に影響を与える可能性がある。

以下に、OpenEntityManagerInViewInterceptorの設定例を示す。

  • SpringMvcConfig.java

    @EnableAspectJAutoProxy
    @EnableWebMvc
    @Configuration
    public class SpringMvcConfig implements WebMvcConfigurer {
    
        // omitted
    
        @Override
        protected void addInterceptors(InterceptorRegistry registry) {
            registry.addWebRequestInterceptor(openEntityManagerInViewInterceptor()) // (2)
                .addPathPatterns("/**") // (1)
                .excludePathPatterns("/resources/**") // (1)
        }
    
        // (2)
        @Bean
        public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
            return new OpenEntityManagerInViewInterceptor();
        }
    

    項番

    説明

    (1)
    Interceptorを適用するパスと、除外パスを指定する。
    例では、リソースファイル(js、css、imageなど)のパス以外のリクエストに対してInterceptorを適用している。
    (2)
    org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptorを指定する。

Note

静的リソースへのパスを適用対象外とする

静的リソース(js、css、image、htmlなど)のパスについては、データアクセスが発生しないため、Interceptorの適用外とすることを推奨する。

適用対象にしてしまうと、EntityManagerに対して無駄な処理(インスタンス生成とクローズ処理)が実行されることになる。


Servlet FilterでLazy Fetchが必要な場合は、org.springframework.orm.jpa.support.OpenEntityManagerInViewFilterを使用して、EntityManagerの生存期間をServlet Filter層まで伸ばす必要がある。
例えば、 SpringSecurityのorg.springframework.security.core.userdetails.UserDetailsServiceを拡張実装し、拡張した処理の中でEntityオブジェクトにアクセスする場合、このケースにあてはまる。
ただし、Lazy Fetchの必要がないのであれば、EntityManagerの生存期間をServlet Filter層まで伸ばす必要はない。

Note

Servlet Filter層でのLazy Fetchについて

Servlet Filter層でLazy Fetchが発生しないように設計および実装することを推奨する。

OpenEntityManagerInViewInterceptorを使用した方が、 適用パターンと除外パターンを柔軟に指定できるため、EntityManagerの生存期間をアプリケーション層まで伸ばす対象のパスを指定しやすくなる。

Servlet Filterで必要となるデータへのアクセスについては、Serviceクラスの処理として事前にFetchしておくか、またはEager Fetchを使用して事前にロードしておくことで、Lazy Fetchが発生しないようにする。

Lifetime of EntityManager on OpenEntityManagerInViewFilter

Picture - Lifetime of EntityManager on OpenEntityManagerInViewFilter


以下にOpenEntityManagerInViewFilterの設定例を示す。

  • web.xml

    <!-- (1) -->
    <filter>
        <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
        <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
    </filter>
    <!-- (2) -->
    <filter-mapping>
        <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    

    項番

    説明

    (1)
    org.springframework.orm.jpa.support.OpenEntityManagerInViewFilterを指定する。
    このServlet Filterは、Lazy Fetchが発生するServlet Filterより前に定義する必要がある。
    (2)
    フィルタを適用するURLのパターンを指定する。可能な限り必要なパスのみに適用することを推奨するが、設定が煩雑になってしまうのであれば、「/*」(すべてのリクエスト)としてもよい。

    Note

    OpenEntityManagerInViewFilterを適用するURLのパターンに「/*」(すべてのリクエスト)を指定した場合は、OpenEntityManagerInViewInterceptorの設定は不要となる。

    Warning

    意図せぬLazyInitializationExceptionの発生を防止するために

    OpenEntityManagerInViewInterceptorOpenEntityManagerInViewFilterを利用するかどうかに関わらず、Lazy Fetchが行われないままのEntityをEntityManagerの生存期間外まで引き継いでしまうと、意図せぬところでorg.hibernate.LazyInitializationExceptionが発生する可能性がある。

    例として、SessionやFlash scopeに登録後、別リクエストやリダイレクト先で取得する場合が挙げられる。

    上記のような場合には、あらかじめEntityManagerの生存期間内でFetchを行い、取得したデータを別オブジェクトに入れ替えて引き継ぐように実装することを推奨する。


6.3.2.3. Repositoryインタフェースの作成

Spring DataではEntity毎のRepositoryインタフェースを作成する方法として、以下3つの方法を提供している。

項番

作成方法

説明

Spring Data提供のインタフェースを継承する

Spring Dataから提供されているインタフェースを継承することで、Entity毎のRepositoryインタフェースを作成する。 特に理由がない場合は、この方法で、Entity毎のRepositoryインタフェースを作成することを推奨する。

必要なメソッドのみ定義したインタフェースを継承する

Spring Dataから提供されているRepositoryインタフェースのメソッドの中から、必要なメソッドのみ定義したプロジェクト用の共通インタフェースを作成し、作成した共通インタフェースを継承することでEntity毎のRepositoryインタフェースを作成する。

インタフェースの継承は行わない

Spring Dataから提供されているインタフェースやプロジェクト用の共通インタフェースの継承は行わずに、Entity毎にRepositoryインタフェースを作成する。


6.3.2.3.1. Spring Data提供のインタフェースを継承する

Spring Dataから提供されているインタフェースを継承してEntity毎のRepositoryインタフェースを作成する方法について説明する。

継承することができるインタフェースは以下の通り。

項番

インタフェース

説明

org.springframework.data.repository
CrudRepository

汎用的なCRUD操作を行うメソッドを提供しているRepositoryインタフェース。

org.springframework.data.repository
PagingAndSortingRepository

CrudRepositoryのfindAllメソッドにページネーション機能とソート機能を追加したRepositoryインタフェース。

org.springframework.data.repository.query
QueryByExampleExecutor
例示による問い合わせ(Query by Example)の実行を可能にするRepositoryインタフェース。
本ガイドラインではQuery by Exampleについては取り扱わない。

org.springframework.data.jpa.repository JpaRepository

JPAの仕様に依存するメソッドを提供しているRepositoryインタフェース。
PagingAndSortingRepositoryQueryByExampleExecutorを継承しているため、PagingAndSortingRepositoryCrudRepositoryQueryByExampleExecutorのメソッドも使用する事ができる。
特に理由がない場合は、本インタフェースを継承してEntity毎のRepositoryインタフェースを作成することを推奨する。

Note

Spring Data提供のRepositoryインタフェースのデフォルト実装について

上記インタフェースで定義されているメソッドの実装は、Spring Data JPAより提供されている org.springframework.data.jpa.repository.support.SimpleJpaRepositoryで行われている。

Note

例示による問い合わせ(Query by Example)について

Query by Exampleでは動的なクエリの作成が可能になり、フィールド名を含むクエリを作成する必要がない。

Query by ExampleについてはSpring Data JPA - Reference Documentation の “Query by Example”を参照されたい。


以下に、作成例を示す。

public interface OrderRepository extends JpaRepository<Order, Integer> { // (1)

}

項番

説明

(1)
JpaRepositoryを継承し、ジェネリック型<T>にEntityの型、ジェネリック型<ID extends Serializable>にEntityのIDの型を指定する。
上記例では、EntityにOrder型、EntityのIDにInteger型を指定している。

JpaRepositoryを継承してEntity毎のRepositoryインタフェースを作成すると、以下のメソッドに対する実装を得ることが出来る。なお、QueryByExampleExecutorのメソッドおよび非推奨メソッドは記載していない。

項番

メソッド

説明

<S extends T> S save(S entity)

指定されたEntityに対する永続操作(INSERT/UPDATE)をjakarta.persistence.EntityManagerに蓄積するためのメソッド。
IDプロパティ(@jakarta.persistence.Idアノテーションまたは@jakarta.persistence.EmbeddedIdアノテーションが付与されているプロパティ)に値が設定されていない場合はEntityManagerpersistメソッドが呼ばれ、値が設定されている場合はmergeメソッドが呼び出される。
mergeメソッドが呼び出された場合、返却されるEntityオブジェクトは、引数で渡されたEntityとは別のオブジェクトとなるので注意すること。

<S extends T> List<S> saveAll(Iterable<S> entities)

指定された複数のEntityに対する永続操作をEntityManagerに蓄積するためのメソッド。
<S extends T> S save(S entity)メソッドを繰り返し呼び出す事で実現している。

<S extends T> saveAndFlush(T entity)

指定されたEntityに対する永続操作をEntityManagerに蓄積した後に、蓄積されている永続操作(INSERT/UPDATE/DELETE)を永続層(DB)に反映するためのメソッド。

<S extends T> List<S> saveAllAndFlush(Iterable<S> entities)

指定された複数のEntityに対する永続操作をEntityManagerに蓄積した後に、蓄積されている永続操作(INSERT/UPDATE/DELETE)を永続層(DB)に反映するためのメソッド

void flush()

EntityManagerに蓄積されたEntityへの永続操作(INSERT/UPDATE/DELETE)を永続層(DB)に実行するためのメソッド。

void deleteById(ID id)

指定されたIDのEntityに対する削除操作をEntityManagerに蓄積するためのメソッド。
このメソッドはfindById(ID)メソッドを呼び出してEntityオブジェクトをEntityManagerの管理下にしてから削除している。
findById(ID)メソッドの呼び出し時にEntityが存在しない場合は、org.springframework.dao.EmptyResultDataAccessExceptionが発生する。

void delete(T entity)

指定されたEntityに対する削除操作をEntityManagerに蓄積するためのメソッド。

void deleteAll(Iterable<? extends T> entities)

指定された複数のEntityに対する削除操作をEntityManagerに蓄積するためのメソッド。
void delete(T entity) メソッドを繰り返し呼び出す事で実現している。削除対象のEntityが大量になる場合は、void deleteAllInBatch(Iterable<T> entities)メソッドを使用した方が効率的に削除することができる。

void deleteAll()

すべてのEntityに対する削除操作をEntityManagerに蓄積するためのメソッド。
List<T> findAll()メソッドで取得したEntityに対してvoid delete(T entity)メソッドを繰り返し呼び出す事で実現している。
削除対象のEntityが大量になる場合は、void deleteAllInBatch()メソッドを使用すること。このメソッドは削除するEntityをすべてアプリケーション上に読み込むので、メモリ枯渇の原因になる。

void deleteAllInBatch(Iterable<T> entities)

指定された複数のEntityを直接永続層(DB)から削除するためのメソッド。
このメソッドを使って削除した場合、EntityManager上で管理(キャッシュ)されているEntityは削除されないため、本メソッドで削除した後にfindById(ID id)メソッドを呼び出すとEntityManager上で管理されているEntityが返却されるので注意が必要。
後続処理で削除したEntityに対してfindById(ID id)メソッドなどのEntityManager上で管理(キャッシュ)されているEntityオブジェクトを返却するメソッドを呼び出す可能性がある場合は、void deleteAll(Iterable<? extends T> entities)メソッドを使用して削除すること。

void deleteAllInBatch()

すべてのEntityを直接永続層(DB)から削除するためのメソッド。
void deleteAllInBatch(Iterable<T> entities)メソッドと同様、EntityManager上で管理(キャッシュ)されているEntityは削除されない点に注意すること。

void deleteAllByIdInBatch(Iterable<ID> ids)

指定されたIDの複数のEntityを直接永続層(DB)から削除するためのメソッド。
このメソッドを使って削除した場合、EntityManager上で管理(キャッシュ)されているEntityは削除されないため、本メソッドで削除した後にfindById(ID id)メソッドを呼び出すとEntityManager上で管理されているEntityが返却されるので注意が必要。
後続処理で削除したEntityに対してfindById(ID id)メソッドなどのEntityManager上で管理(キャッシュ)されているEntityオブジェクトを返却するメソッドを呼び出す可能性がある場合は、void deleteAll(Iterable<? extends T> entities)メソッドを使用して削除すること。

void deleteAllById(Iterable<? extends ID> ids)

指定されたIDの複数のEntityに対する削除操作をEntityManagerに蓄積するためのメソッド。
void deleteById(ID id)メソッドを繰り返し呼び出す事で実現している。削除対象のEntityが大量になる場合は、void deleteAllByIdInBatch(Iterable<ID> ids)メソッドを使用した方が効率的に削除することができる。

Optional<T> findById(ID id)

指定されたIDのEntityを永続層(DB)から取得するためのメソッド。
永続層から取得されたEntityはEntityManagerによって管理(キャッシュ)されるため、2回目以降のアクセスでは永続層へのアクセスは発生せず、キャッシュされているEntityが返却される。

T getById(ID id)

指定されたIDのEntityを永続層(DB)から取得するためのメソッド。
永続層から取得されたEntityはEntityManagerによって管理(キャッシュ)されるため、2回目以降のアクセスでは永続層へのアクセスは発生せず、キャッシュされているEntityが返却される。
findByIdメソッドとは異なりEntityはLazy Fetchされ、データが存在しない場合はEntityのプロパティへの最初のアクセス時にEntityNotFoundExceptionがスローされる。

Warning

Entityの取得とプロパティへのアクセスを別々の箇所で行うよう実装した場合、実装者がLazy Fetchを意識できずに例外ハンドリングが漏れる恐れがあるため、意図的にLazy Fetchするとき以外はfindByIdメソッドを使用することを推奨する。

List<T> findAll()

すべてのEntityを永続層(DB)から取得するためのメソッド。
永続層から取得されたEntityはEntityManagerによって管理(キャッシュ)される。

List<T> findAllById(Iterable<ID> ids)

指定された複数のIDのEntityを永続層(DB)から取得するためのメソッド。
永続層から取得されたEntityはEntityManagerによって管理(キャッシュ)される。 指定されたIDはIN句を使って検索されるため、OracleなどIN句に指定できる値の数に制限があるDBを使う場合は注意すること。

List<T> findAll(Sort sort)

指定された並び順ですべてのEntityを永続層(DB)から取得するためのメソッド。
永続層から取得されたEntityはEntityManagerによって管理(キャッシュ)される。

Page<T> findAll(Pageable pageable)

指定されたページ(並び順、ページ数、ページ内に表示する件数)に一致するEntityを永続層(DB)から取得するためのメソッド。
永続層から取得されたEntityはEntityManagerによって管理(キャッシュ)される。

boolean existsById(ID id)

指定されたIDのEntityが存在するかチェックするためのメソッド。

long count()

永続化対象のEntityの件数を取得するためのメソッド。

Warning

JPAの楽観ロック(@jakarta.persistence.Version)使用時の動作について

JPAの楽観ロック(@Version)使用時に更新対象のEntityが更新または削除された場合は、org.springframework.dao.OptimisticLockingFailureExceptionが発生する。

OptimisticLockingFailureExceptionが発生する可能性があるメソッドは、以下の通りである。

  • <S extends T> S save(S entity)

  • <S extends T> List<S> saveAll(Iterable<S> entities)

  • T saveAndFlush(T entity)

  • <S extends T> List<S> saveAllAndFlush(Iterable<S> entities)

  • void deleteById(ID id)

  • void delete(T entity)

  • void deleteAll(Iterable<? extends T> entities)

  • void deleteAll()

  • void deleteAllById(Iterable<? extends ID> ids)

  • void flush()

JPAの楽観ロックの詳細については排他制御を参照されたい。

Note

永続操作の反映タイミングについて(その1)

EntityManagerに蓄積されたEntityへの永続操作は、トランザクションをコミットする直前に実行され永続層(DB)に反映される。そのため、一意制約違反などのエラーをトランザクション管理内の処理(Serviceの処理)でハンドリングしたい場合は、saveAndFlushメソッドまたはflushメソッドを呼び出してEntityManager内に蓄積されているEntityへの永続操作を強制的に実行する必要がある。単にエラーをクライアントに通知するだけでよければ、Controllerで例外ハンドリングを行い適切なメッセージを設定すればよい。

saveAndFlushメソッドおよびflushメソッドはJPA依存のメソッドなので、意図なく使用しないように注意すること。

  • 通常のフロー

    Normal sequence of persistence processing

    Picture - Normal sequence of persistence processing

  • flush時のフロー

    Sequence of persistence processing when using flush method

    Picture - Sequence of persistence processing when using flush method

Note

永続操作の反映タイミングについて(その2)

以下のメソッドを呼び出した場合、EntityManagerと、永続層(DB)で管理しているデータの不整合が発生しないようにするために、メインの処理が行われる前にEntityManagerに蓄積されているEntityへの永続操作が永続層(DB)に反映される。

  • findAll系メソッド

  • boolean existsById(ID id)

  • long count()

上記のメソッドは、永続層(DB)に直接Queryを発行するため、 メインの処理が行われる前に永続層(DB)に反映されないとデータの不整合が発生することになる。

後述するQueryメソッドを呼び出した際も、EntityManagerに蓄積されていたEntityへの永続操作が、永続層(DB)に反映されるトリガーとなる。

  • Query発行時のフロー

    Sequence of persistence processing when using query method

    Picture - Sequence of persistence processing when using query method


6.3.2.3.2. 必要なメソッドのみ定義したインタフェースを継承する

Spring Dataから提供されているインタフェースに定義されているメソッドの中から、必要なメソッドのみ定義した共通インタフェースを 作成し継承する事でEntity毎のRepositoryインタフェースを作成する方法について説明する。

メソッドのシグネチャをSpring Dataから提供されているRepositoryインタフェースのメソッドと一致させる必要があるが、Spring Data提供の Repositoryインタフェースを継承して作成した際と同様、メソッドの実装は不要である。

Note

想定される適用ケース

Spring Dataから提供されているRepositoryインタフェースのメソッドの中には、実際のアプリケーションでは使用しないまたは使用しない方がよいメソッドもある。そのようなメソッドをRepositoryインタフェースから削除したい場合は、この方法で作成する。

インタフェースに定義したメソッドの実装は、Spring Data JPAより提供されているorg.springframework.data.jpa.repository.support.SimpleJpaRepositoryで行われている。

以下に、作成例を示す。

@NoRepositoryBean // (1)
public interface MyProjectRepository<T, ID extends Serializable> extends
        Repository<T, ID> { // (2)

    Optional<T> findById(ID id); // (3)

    T save(T entity); // (3)

    // omitted

}
public interface OrderRepository extends MyProjectRepository<Order, Integer> { // (4)

}

項番

説明

(1)
@NoRepositoryBeanを指定し、Spring DataによるRepositoryインターフェースのインスタンス化対象から外す。
(2)
org.springframework.data.repository.Repositoryを継承しプロジェクト用の汎用インタフェースを作成する。
汎用インタフェースなので、ジェネリック型を使用する。
(3)

Spring Dataから提供されているRepositoryインタフェースのメソッドの中から必要なメソッドを選んで定義する。

(4)

プロジェクト用の汎用インタフェースを継承し、ジェネリック型<T>にEntityの型、ジェネリック型<ID extends Serializable>にEntityのIDの型を指定する。例では、EntityにOrder型、EntityのIDにInteger型を指定している。


6.3.2.3.3. インタフェースの継承は行わない

Spring Dataより提供されているインタフェースや共通インタフェースを継承しないで、Entity毎のRepositoryインタフェースを作成する方法について、説明する。

クラスアノテーションとして@org.springframework.data.repository.RepositoryDefinitionアノテーションを指定し、 domainClass属性にEntityの型を、idClass属性にEntityのIDの型を指定する。
Spring Dataから提供されているRepositoryインタフェースに定義されているメソッドと同じシグネチャのメソッドについては、Spring Data提供のRepositoryインタフェースを継承して作成した際と同様、メソッドの実装は不要である。

Note

想定される適用ケース

共通的なEntityの操作が必要ない場合は、この方法で作成してもよい。

Spring Dataから提供されているRepositoryインタフェースに定義されているメソッドと同じシグネチャのメソッドの実装は、Spring Data JPAより提供されているorg.springframework.data.jpa.repository.support.SimpleJpaRepositoryで行われている。

以下に、作成例を示す。

@RepositoryDefinition(domainClass = Order.class, idClass = Integer.class) // (1)
public interface OrderRepository { //(2)

    Optional<Order> findById(Integer id); // (3)

    Order save(Order entity); // (3)

    // omitted
}

項番

説明

(1)
@RepositoryDefinitionアノテーションを指定する。
例では、domainClass属性(Entityの型)にOrder型、idClass属性(EntityのIDの型)にInteger型を指定している。
(2)

Spring Dataから提供されているインタフェース(org.springframework.data.repository.Repository)の継承は不要である。

(3)

Entity毎に必要なメソッドを定義する。


6.3.2.4. Queryメソッドの追加

Spring Dataより提供されている汎用的なCRUD操作を行うためのインタフェースだけでは、実際のアプリケーションを構築する事は難しい。
そのためSpring Dataでは、Entity毎のRepositoryインタフェースに対して任意の永続操作(SELECT/UPDATE/DELETE)を行うためのQueryメソッドを追加できる仕組みを提供している。
追加したQueryメソッドでは、Query言語(JPQLまたはNativeなSQL)を使用してEntityの操作を行う。

Note

JPQLとは

JPQLとは”Java Persistence Query Language”の略で、永続層(DB)のレコードに対応するEntityを操作(SELECT/UPDATE/DELETE)するためのQuery言語である。

文法はSQLに似ているが、永続層(DB)のレコードを直接操作するのではなく、永続層のレコードにマッピングされているEntityを操作することになる。Entityに対して行った操作の永続層(DB)への反映は、JPAプロバイダ(Hibernate)によって行われる。

JPQLの詳細については、jakarta Persistence APIのSpecification「4. Query Language」を参照されたい。


6.3.2.4.1. Queryメソッドを定義する

Queryメソッドは、Entity毎のRepositoryインタフェースのメソッドとして定義する。

public interface OrderRepository extends JpaRepository<Order, Integer> {
    List<Order> findByStatusCode(String statusCode);
}

6.3.2.4.2. 実行するQueryを指定する

Queryメソッド呼び出し時に実行するQueryを指定する必要がある。
指定方法は以下の通り。詳細は、QueryメソッドのQuery指定を参照されたい。

項番

Queryの指定方法

説明

(Spring Dataの機能)
Entity毎のRepositoryインタフェースに追加したメソッドに@org.springframework.data.jpa.repository.Queryアノテーションを指定し、実行するQueryを指定する。
特に理由がない場合は、この方法で指定することを推奨する。
Spring Dataが定めた命名規約に則りメソッド名を付与することで実行するQueryを指定する。
Spring Data JPAの機能によってメソッド名から実行するQuery(JPQL)が生成される。生成できるJPQLはSELECTのみとなっている。
条件が少なくシンプルなQueryの場合は、 @Query アノテーションを使わずにこの方法を使ってもよい。
ただし、条件が多く複雑なQueryの場合は、メソッド名は振る舞いを表すシンプルな名前にして@QueryアノテーションでQueryを指定すること。
Spring Data JPAから提供されているプロパティファイルに実行するQueryを指定する。
メソッド定義とQuery指定を行う箇所が分離してしまうので、基本的にはこの方法での指定は推奨しない。
ただし、QueryとしてNativeなSQLを使用する場合は、データベースに依存するSQLをプロパティファイルに定義する必要があるか確認すること。
使用するデータベースを任意に選択できるアプリケーションや、実行環境によって用意できるデータベースが変わる(変わる可能性がある)場合には、この方法でQueryを指定し、プロパティファイルを環境依存資材として管理する必要がある。

Note

Query指定方法の併用について

複数のQueryの指定方法を併用することに対して、特に制限は設けない。プロジェクトで使用する指定方法や併用の制限については、プロジェクト毎に判断すること。

Note

QueryのLookup方法について

Spring Dataデフォルトの動作は、CREATE_IF_NOT_FOUNDに設定されているため、以下の動作となる。

#.@Queryアノテーションに指定されているQueryを取得し、指定があればそのQueryを使用する。 #. Named queryから対応するQueryを取得し、対応するQueryが見つかった場合はそのQueryを使用する。 #. メソッド名からQuery(JPQL)を作成して使用する。 #. メソッド名からQuery(JPQL)が作成できない場合は、エラーとなる。

QueryのLookup方法の詳細については、Spring Data Commons - Reference Documentation「Defining query methods」の「Query lookup strategies」を参照されたい。


6.3.2.4.3. Entityのロックを取得する

Entityのロックを取得する必要がある場合は、Queryメソッドに@org.springframework.data.jpa.repository.Lockアノテーションを追加し、ロックモードを指定する。
詳細については、排他制御を参照されたい。
@Query(value = "SELECT o FROM Order o WHERE o.status.code = :statusCode ORDER BY o.id DESC")
@Lock(LockModeType.PESSIMISTIC_WRITE) // (1)
List<Order> findByStatusCode(@Param("statusCode") String statusCode);
-- (2) statusCode='accepted'
SELECT
        order0_.id AS id1_5_
        ,order0_.status_code AS status2_5_
    FROM
        t_order order0_
    WHERE
        order0_.status_code = 'accepted'
    ORDER BY
        order0_.id DESC
    FOR UPDATE

項番

説明

(1)
@Lockアノテーションのvalue属性にロックモードを指定する。
指定可能なロックモードについては、LockModeTypeのJavaDocを参照されたい。
(2)
JPQLから変換されたNativeなSQL。(使用DBはPostgreSQL)
例では、LockModeType.PESSIMISTIC_WRITEを指定しているので、SQLに”FOR UPDATE”句が追加される。

6.3.2.4.4. 永続層のEntityを直接操作する

Entityの更新および削除の操作は、原則EntityManager上で管理されているEntityオブジェクトに対して行うことを推奨する。
ただし、Entityを一括で更新または削除する必要がある場合は、Queryメソッドを使って永続層(DB)のEntityを直接操作することを検討すること。

Note

性能劣化の要因軽減

永続層のEntityを直接操作することで、Entityの操作を行うためのSQLの発行回数を減らすことができる。そのため、高い性能要件があるアプリケーションの場合は、この方法でEntityの一括操作を行うことで、性能劣化の要因を減らす事ができる。

減らすことが出来るSQLは以下の通り。

  • EntityオブジェクトをEntityManager上に読み込むためのSQL。発行が不要となる。

  • Entityを更新および削除するためのSQL。n回の発行が必要だったものが1回の発行で済む。

Note

永続層のEntityを直接操作するかの判断基準について

永続層のEntityを直接操作する場合、機能的な注意点がいくつかあるため、性能要件が高くないアプリケーションの場合は、一括操作についても EntityManager 上で管理されているEntityオブジェクトに対して行うことを推奨する。

具体的な注意点については、実装例を参照されたい。

以下に、Queryメソッドを使って、永続層のEntityを直接操作する実装例を示す。

@Modifying // (1)
@Query("UPDATE OrderItem oi SET oi.logicalDelete = true WHERE oi.id.orderId = :orderId ") // (2)
int updateToLogicalDelete(@Param("orderId") Integer orderId); // (3)

項番

説明

(1)
更新系のQueryメソッドであることを示す@org.springframework.data.jpa.repository.Modifyingアノテーションを指定する。
指定しないと実行時にエラーとなる。
(2)
更新系(UPDATEまたはDELETE)用のQueryを指定する。
(3)
更新件数や削除件数が必要な場合は、intまたはjava.lang.Integerを戻り値の型として指定し、件数が必要ない場合は、voidを指定する。

Warning

EntityManager上で管理しているEntityとの整合性について

Queryメソッドを使って永続層のEntityを直接操作した場合、Spring Data JPAのデフォルト動作ではEntityManager上で管理されているEntityに反映されない。そのため、直後にJpaRepository#findById(ID)メソッドを呼び出して取得されるEntityオブジェクトは、操作前の状態である点に注意すること。

この動作を回避する方法として、@ModifyingアノテーションのclearAutomatically属性をtrueに指定する方法がある。

clearAutomatically属性にtrueを指定した場合、永続層のEntityを直接操作した後に、EntityManagerclear()メソッドが呼び出され、 EntityManager上で管理されていたEntityオブジェクトと蓄積されていた永続操作がEntityManager上から破棄される。そのため、直後にJpaRepository#findById(ID)メソッドを呼び出した場合、永続層から最新状態のEntityが取得され、永続層とEntityManagerの状態が同期される仕組みになっている。

Warning

@Modifying(clearAutomatically = true) 使用時の注意点

@Modifying(clearAutomatically = true)とすることで、蓄積されていた永続操作(INSERT/UPDATE/DELETE)もEntityManager上から破棄されてしまうという点に注意が必要となる。これは、必要な永続操作が永続層に反映されない可能性がある事を意味するため、バグを引き起こす要因となりうる。

この問題を回避するためには、 永続層のEntityを直接操作する前にJpaRepository#saveAndFlush(T entity)またはJpaRepository#flush()メソッドを呼び出し、蓄積されている永続操作を永続層に反映しておく必要がある。


6.3.2.4.5. Queryヒントを設定する

Queryにヒントを設定する必要がある場合は、Queryメソッドに@org.springframework.data.jpa.repository.QueryHintsアノテーションを追加し、value属性にQueryヒント(@jakarta.persistence.QueryHint)を指定する。

@Query(value = "SELECT o FROM Order o WHERE o.status.code = :statusCode ORDER BY o.id DESC")
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints(value = { @QueryHint(name = "jakarta.persistence.lock.timeout", value = "0") }) // (1)
List<Order> findByStatusCode(@Param("statusCode") String statusCode);

項番

説明

(1)
@QueryHintアノテーションのname属性にヒント名、value属性にヒント値を設定する。
指定できるヒントは、JPAの仕様で決められているものに加え、プロバイダ固有のものがある。
上記例では、ロックタイムアウトを”0“に設定している(使用DBはOracle)。SQLに”FOR UPDATE NOWAIT”句が追加される。

Note

Hibernateで指定できるQueryヒントについて

JPA仕様で決められているQueryヒントは以下の通り。 詳細は、Jakarta Persistence APIのSpecification「3.10.10. Query Hints」を参照されたい。

  • jakarta.persistence.query.timeout

  • jakarta.persistence.lock.timeout

  • jakarta.persistence.cache.retrieveMode

  • jakarta.persistence.cache.storeMode

Hibernate固有のQueryヒントについては、QueryHintsクラスのJavaDocを参照されたい。


6.3.2.5. QueryメソッドのQuery指定

Queryメソッド呼び出し時に実行するQueryの指定方法について説明する。


6.3.2.5.1. @Query アノテーションで指定する

@Queryアノテーションのvalue属性に実行するQuery(JPQL)を指定する。

@Query(value = "SELECT o FROM Order o WHERE o.status.code = :statusCode ORDER BY o.id DESC") // (1)
List<Order> findByStatusCode(@Param("statusCode") String statusCode);
-- (2) statusCode='accepted'
SELECT
        order0_.id AS id1_5_
        ,order0_.status_code AS status2_5_
    FROM
        t_order order0_
    WHERE
        order0_.status_code = 'accepted'
    ORDER BY
        order0_.id DESC

項番

説明

(1)
@Queryアノテーションのvalue属性に実行するQuery(JPQL)を指定する。
上記例では、Orderオブジェクトで保持しているstatusプロパティ(OrderStatus型)のcodeプロパティ(String型) の値が指定したパラメータ値(statusCode)と一致するOrderオブジェクトをidプロパティの降順に並べて取得するためのQueryを指定している。
(2)

JPQLから変換されたNativeなSQL。@Queryアノテーションのvalue属性に指定したQuery(JPQL)は、使用するデータベースのNativeなSQLに変換され実行される。

Note

JPQLではなくNativeなSQLを直接指定する方法

QueryとしてJPQLではなくNativeなSQLを直接指定したい場合は、 nativeQuery属性をtrueに設定することで指定可能となる。

基本的にはJPQLを使用する事を推奨するが、JPQLで表現できないQueryを発行する必要がある場合はNativeなSQLを直接指定してもよい。

データベースに依存するSQLを指定する場合は、SQLをプロパティファイルに定義することを検討すること。

SQLをプロパティファイルに定義する方法については、「プロパティファイルにNamed queryとして指定する」を参照されたい。

Note

Named Parametersについて

Queryにバインドするパラメータに対して名前を付与し、Query内からはパラメータ名を指定することで値をバインドすることができる。

Named Parameterを使用する場合は、@org.springframework.data.repository.query.Paramアノテーションを対象とする引数に追加し、value属性にパラメータ名を指定する。

Queryでは、バインドしたい位置に「:パラメータ名」の形式で指定する。

特に理由がない場合は、 Queryのメンテナンス性と可読性を考慮し、Named Parametersを使用することを推奨する。


LIKE検索の一致方法(前方一致、後方一致、部分一致)が固定の場合は、JPQL内に “%“ を指定することが出来る。
ただし、これはJPQLの標準形式ではなくSpring Data JPAの拡張形式になるので、@Queryアノテーションで指定するJPQLでのみ指定することが出来る。
Named queryとして指定するJPQL内に “%“ を指定するとエラーになるので注意すること。

項番

一致方法

形式

具体例

前方一致

:parameterName%
or
?n%
SELECT a FROM Account WHERE a.firstName LIKE :firstName%
SELECT a FROM Account WHERE a.firstName LIKE ?1%

後方一致

%:parameterName
or
%?n
SELECT a FROM Account WHERE a.firstName LIKE %:firstName
SELECT a FROM Account WHERE a.firstName LIKE %?1

部分一致

%:parameterName%
or
%?n%
SELECT a FROM Account WHERE a.firstName LIKE %:firstName%
SELECT a FROM Account WHERE a.firstName LIKE %?1%

Note

LIKE検索時のエスケープについて

LIKE検索を行う場合は、検索条件となる値をLIKE検索用にエスケープする必要がある。

org.terasoluna.gfw.common.query.QueryEscapeUtilsクラスにエスケープするためのメソッドが用意されているため、要件を充たせる場合は使用を検討すること。

QueryEscapeUtilsクラスの詳細については、「データベースアクセス(共通編)」の「LIKE検索時のエスケープについて」を参照されたい。

Note

一致方法を動的に変化させる必要がある場合

一致方法(前方一致、後方一致、部分一致)を動的に変化させる必要がある場合は、JPQL内に “%“ を指定するのではなく、従来通りバインドするパラメータ値の前後に “%“ を追加すること。

org.terasoluna.gfw.common.query.QueryEscapeUtilsクラスに一致方法に対応する検索条件値に変換するメソッドが用意されているため、要件を充たせる場合は、使用を検討すること。

QueryEscapeUtilsクラスの詳細については、「データベースアクセス(共通編)」の「LIKE検索時のエスケープについて」を参照されたい。

ソート条件は、Query内に直接指定することができる。
以下に、実装例を示す。
// (1)
@Query(value = "SELECT o FROM Order o WHERE o.status.code = :statusCode ORDER BY o.id DESC")
Page<Order> findByStatusCode(@Param("statusCode") String statusCode, Pageable pageable);

項番

説明

(1)

QueryにORDER BYを指定する。降順にする場合はDESCを、昇順にする場合はASCを指定する。DESC/ASCを省略した場合は、ASCが適用される。


ソート条件はQuery内に直接指定する以外に、Pageableオブジェクト内に保持しているorg.springframework.data.domain.Sortオブジェクトに指定することが出来る。
この方法でソート条件を指定する場合は、countQuery属性の指定は不要。
以下に、Pageableオブジェクト内に保持しているSortオブジェクトを使用してソートする実装例を示す。
  • Controller

    @GetMapping("list")
    public String list(@PageableDefault(
                            size=5,
                            sort = "id", // (1)
                            direction = Direction.DESC // (1)
                            ) Pageable pageable,
                              Model model) {
        Page<Order> orderPage = orderService.getOrders(pageable); // (2)
        model.addAttribute("orderPage", orderPage);
        return "order/list";
    }
    

    項番

    説明

    (1)
    ソート条件を指定する。Pageable#getSort()メソッドで取得できるSortオブジェクトにソート条件が設定される。
    上記例では、 idフィールドの降順をソート条件として指定している。
    (2)

    Pageableオブジェクトを指定してServiceのメソッドを呼び出す。


  • Service (Caller)

    public String getOrders(Pageable pageable){
        return orderRepository.findByStatusCode("accepted", pageable); // (3)
    }
    

    項番

    説明

    (3)

    Controllerから渡されたPageableオブジェクトを指定してRepositoryのメソッドを呼び出す。


  • Repositoryインタフェース

    @Query(value = "SELECT o FROM Order o WHERE o.status.code = :statusCode") // (4)
    Page<Order> findByStatusCode(@Param("statusCode") String statusCode, Pageable pageable);
    
    -- (5) statusCode='accepted'
    SELECT
            COUNT(order0_.id) AS col_0_0_
        FROM
            t_order order0_
        WHERE
            order0_.status_code = 'accepted'
    
    -- (6) statusCode='accepted'
    SELECT
            order0_.id AS id1_5_
            ,order0_.status_code AS status2_5_
        FROM
            t_order order0_
        WHERE
            order0_.status_code = 'accepted'
        ORDER BY
            order0_.id DESC
        LIMIT 5
    

    項番

    説明

    (4)

    Queryに”ORDER BY”句の指定は行わない。countQuery属性の指定も不要。

    (5)

    JPQLから変換された件数カウント用のNativeなSQL。

    (6)
    JPQLから変換された「指定されたページ位置のEntityを取得する」ためのNativeなSQL。
    Queryに指定はしていないが、Pageableオブジェクト内に保持しているSortオブジェクトに指定した条件で”ORDER BY”句が追加される。例では、PostgreSQL用のSQLになっている。

6.3.2.5.2. 命名規約ベースのメソッド名で指定する

Spring Dataが定めた命名規約に則ったメソッド名にすることで実行するQuery(JPQL)を指定する。
Spring Data JPAの機能によってメソッド名からJPQLが生成される。
ただし、メソッド名からJPQLを作成できるのはSELECTのみで、UPDATEおよびDELETEのJPQLは生成できない。

メソッド名からJPQLを生成するための命名規約などのルールについては、以下のページを参照されたい。

項番

参照ページ

説明

Spring Data Commons - Reference Documentation「Defining query methods」の「Query creation」

Distinct、ORDER BY、Case insensitiveの指定方法などが記載されている。

Spring Data Commons - Reference Documentation「Defining query methods」の「Property expressions」

ネストされたEntityのプロパティを条件に指定する方法などが記載されている。

Spring Data Commons - Reference Documentation「Defining query methods」の「Special parameter handling」

特別なメソッド引数(PageableSort)についての説明が記載されている。

Spring Data JPA - Reference Documentation「Query methods」の「Query creation」

JPQLを組み立てるための命名規約(キーワード)に関する説明が記載されている。

Spring Data Commons - Reference Documentation「Appendix C. Repository query keywords」

JPQLを組み立てるための命名規約(キーワード)に関する説明が記載されている。

以下に、実装例を示す。

  • OrderRepositry.java

    Page<Order> findByStatusCode(String statusCode, Pageable pageable); // (1)
    

    項番

    説明

    (1)
    メソッド名が^(find|read|get).*By(.+)のパターンに一致する場合、メソッド名からJPQLを生成する対象のメソッドとなる。
    (.+)の部分に条件となるEntityのプロパティや操作を示すキーワードを指定する。
    例では、Orderオブジェクトで保持しているstatusプロパティ(OrderStatus型)のcodeプロパティ(String型) の値が指定したパラメータ値(statusCode)と一致するOrderオブジェクトをページ形式で取得している。
  • 件数カウント用Query

    -- (2) JPQL
    SELECT
            COUNT(*)
        FROM
            ORDER AS generatedAlias0
                LEFT JOIN generatedAlias0.status AS generatedAlias1
            WHERE
                generatedAlias1.code = ?1
    
    -- (3) SQL statusCode='accepted'
    SELECT
            COUNT(*) AS col_0_0_
        FROM
            t_order order0_
                LEFT OUTER JOIN c_order_status orderstatu1_
                    ON order0_.status_code = orderstatu1_.code
        WHERE
            orderstatu1_.code = 'accepted'
    

    項番

    説明

    (2)

    メソッド名から生成された件数カウント用のJPQLのQuery。

    (3)

    (2)のJPQLから変換された件数カウント用のNativeなSQL。

  • Entity取得用Query

    -- (4) JPQL
    SELECT
            generatedAlias0
        FROM
            ORDER AS generatedAlias0
                LEFT JOIN generatedAlias0.status AS generatedAlias1
            WHERE
                generatedAlias1.code = ?1
            ORDER BY
                generatedAlias0.id DESC;
    
    -- (5) statusCode='accepted'
    SELECT
            order0_.id AS id1_5_
            ,order0_.status_code AS status2_5_
        FROM
            t_order order0_
                LEFT OUTER JOIN c_order_status orderstatu1_
                    ON order0_.status_code = orderstatu1_.code
        WHERE
            orderstatu1_.code = 'accepted'
        ORDER BY
            order0_.id DESC
        LIMIT 5
    

    項番

    説明

    (4)

    メソッド名から生成されたEntity取得用のJPQLのQuery。

    (5)

    (4)のJPQLから変換されたEntity取得用のNativeなSQL。


6.3.2.5.3. プロパティファイルにNamed queryとして指定する

Spring Data JPAから提供されているプロパティファイル(classpath:META-INF/jpa-named-queries.properties)に、実行するQueryを指定する。

この方法は、QueryとしてNativeQueryを使用する際に、データベース固有のSQLを記載する必要が出た場合に使用するか検討すること。
データベース固有のSQLであっても、実行環境に依存しないのであれば @Queryアノテーションに直接指定する方法を推奨する。
  • OrderRepositry.java

    @Query(nativeQuery = true)
    List<Order> findAllByStatusCode(@Param("statusCode") String statusCode); // (1)
    

    項番

    説明

    (1)
    Named queryのLookup名は、Entityのクラス名とメソッド名を “.“ (dot) で連結したものが使用される。
    上記例だと、Order.findAllByStatusCodeがLookup名となる。

    Tip

    Named queryのLookup名を指定する方法

    デフォルトの動作では、Entityのクラス名とメソッド名を “.“ (dot) で連結したものがLookup名として使用されるが、任意のクエリ名を指定することも出来る。

    • Entity取得用のLookup名に任意のクエリ名を指定したい場合は、@Queryアノテーションのname属性にクエリ名を指定する。

    • ページ検索時の件数カウント用のLookup名に任意のクエリ名を指定したい場合は、@QueryアノテーションのcountName属性にクエリ名を指定する。

      @Query(name = "OrderRepository.findAllByStatusCode", nativeQuery = true) // (2)
      List<Order> findAllByStatusCode(@Param("statusCode") String statusCode);
      

      項番

      説明

      (2)

      上記例では、OrderRepository.findAllByStatusCodeをLookup用のクエリ名として指定している。


  • jpa-named-queries.properties

    # (3)
    Order.findAllByStatusCode=SELECT * FROM order WHERE status_code = :statusCode
    

    項番

    説明

    (3)
    クエリー名をキーとして、実行するSQLを指定する。
    上記例では、Order.findAllByStatusCodeをキーに、実行するSQLを指定している。

    Tip

    Spring Data JPAから提供されているプロパティファイルではなく、任意のプロパティファイルにNamed Queryを指定する方法を以下に紹介する。

    • XxxInfraConfig.java

      @Configuration
      @EnableJpaRepositories(basePackages = "xxx.yyy.zzz.domain.repository",
                             namedQueriesLocation = "classpath:META-INF/jpa/jpa-named-queries.properties") // (4)
      public class XxxInfraConfig {
      
          // omitted
      

      項番

      説明

      (4)
      @EnableJpaRepositoriesアノテーションのnamedQueriesLocation属性に、任意のプロパティファイルを指定する。
      上記例では、クラスパス上にあるMETA-INF/jpa/jpa-named-queries.propertiesが使用される。

6.3.2.6. Entityの検索処理の実装

Entityの検索方法について、目的別に説明する。


6.3.2.6.1. 条件に一致するEntityを全件検索

条件に一致するEntityを全件取得するQueryメソッドを呼び出す。

  • Repositroyインタフェース

    public interface AccountRepository extends JpaRepository<Account, String> {
    
        // (1)
        @Query("SELECT a FROM Account a WHERE :createdDateFrom <= a.createdDate AND a.createdDate < :createdDateTo ORDER BY a.createdDate DESC")
        List<Account> findByCreatedDate(
                @Param("createdDateFrom") Date createdDateFrom,
                @Param("createdDateTo") Date createdDateTo);
    
    }
    

    項番

    説明

    (1)
    java.util.Listインタフェースを返却するQueryメソッドを定義する。
  • Service

    public List<Account> getAccounts(Date targetDate) {
        LocalDate targetLocalDate = new LocalDate(targetDate);
        Date fromDate = targetLocalDate.toDate();
        Date toDate = targetLocalDate.dayOfYear().addToCopy(1).toDate();
    
        // (2)
        List<Account> accounts = accountRepository.findByCreatedDate(fromDate,
                toDate);
        if (accounts.isEmpty()) { // (3)
            // omitted
        }
        return accounts;
    }
    

    項番

    説明

    (2)
    Repositryインタフェースに実装したQueryメソッドを呼び出す。
    (3)
    検索結果が0件の場合は、空のリストが返却される。nullは返却されないのでnullチェックは不要。
    必要に応じて、検索結果が0件の場合の処理を実装する。

6.3.2.6.2. 条件に一致するEntityのページ検索

条件に一致するEntityの中から指定ページに該当するEntityを取得するQueryメソッドを呼び出す。

  • Repositroyインタフェース

    public interface AccountRepository extends JpaRepository<Account, String> {
    
        // (1)
        @Query("SELECT a FROM Account a WHERE :createdDateFrom <= a.createdDate AND a.createdDate < :createdDateTo")
        Page<Account> findByCreatedDate(
                @Param("createdDateFrom") Date createdDateFrom,
                @Param("createdDateTo") Date createdDateTo, Pageable pageable);
    
    }
    

    項番

    説明

    (1)
    引数としてorg.springframework.data.domain.Pageableインタフェースを受け取り、org.springframework.data.domain.Pageインタフェースを返却するQueryメソッドを定義する。
  • Controller

    @GetMapping("list")
    public String list(@RequestParam("targetDate") Date targetDate,
                       @PageableDefault(
                           page = 0,
                           value = 5,
                           sort = { "createdDate" },
                           direction = Direction.DESC)
                           Pageable pageable, // (2)
                       Model model) {
        Page<Order> accountPage = accountService.getAccounts(targetDate, pageable);
        model.addAttribute("accountPage", accountPage);
        return "account/list";
    }
    

    項番

    説明

    (2)
    Spring Dataより提供されているページング検索用のオブジェクト( org.springframework.data.domain.Pageable)を生成する。
    詳細は「ページネーション」を参照されたい。
  • Service

    public Page<Account> getAccounts(Date targetDate ,Pageable pageable) {
    
        LocalDate targetLocalDate = new LocalDate(targetDate);
        Date fromDate = targetLocalDate.toDate();
        Date toDate = targetLocalDate.dayOfYear().addToCopy(1).toDate();
    
        // (3)
        Page<Account> page = accountRepository.findByCreatedDate(fromDate,
                toDate, pageable);
        if (!page.hasContent()) { // (4)
            // ...
        }
        return page;
    }
    

    項番

    説明

    (3)
    Repositryインタフェースに実装したQueryメソッドを呼び出す。
    (4)
    検索結果が0件の場合は、Pageオブジェクトに空のリストが設定され、Page#hasContent()メソッドの返り値がfalseになる。
    必要に応じて、検索結果が0件の場合の処理を実装する。

6.3.2.7. Entityの動的条件による検索処理の実装

動的条件によるEntityの検索を行うQueryメソッドをRepositoryメソッドに追加する場合は、 Entity毎のRepositoryインタフェースに対して、カスタムRepositoryインタフェースとカスタムRepositoryインタフェースの実装クラスを用意する方法で実装する。
カスタムRepositoryインタフェースとカスタムRepositoryクラスの作成方法については、「Entity毎のRepositoryインタフェースに個別に追加する」を参照されたい。

以降では、動的条件を適用してEntityを検索する方法について、目的別に説明する。

Todo

TBD

今後、以下の内容を追加する予定である。

  • QueryDSLを使用した動的Queryの実装例。


6.3.2.7.1. 動的条件に一致するEntityを全件検索

動的条件に一致するEntityを全件取得するQueryメソッドを実装し、呼び出す。

以下の実装例を示す。

実装例では、動的条件として、

  • 注文ID

  • 商品名

  • 注文状態(複数指定可能)

を指定可能とし、指定された条件に一致する注文をAND条件で絞り込む検索とする。
なお、条件の指定がない場合は、検索は行わず空のリストを返却する。
  • Criteria (JavaBean)

    public class OrderCriteria implements Serializable { // (1)
    
        private Integer id;
    
        private String itemName;
    
        private List<String> statusCodes;
    
        // omitted
    
    }
    

    項番

    説明

    (1)
    検索条件を保持するCriteriaオブジェクト(JavaBean)を作成する。
  • カスタムRepositoryインタフェース

    public interface OrderRepositoryCustom {
    
        Page<Order> findAllByCriteria(OrderCriteria criteria); // (2)
    
    }
    

    項番

    説明

    (2)
    カスタムRepositoryインタフェースに、Criteriaオブジェクトを引数にとり、Listを返却するメソッドを定義する。
  • カスタムRepositoryクラス

    public class OrderRepositoryImpl implements OrderRepositoryCustom { // (3)
    
        @PersistenceContext
        EntityManager entityManager; // (4)
    
        public List<Order> findAllByCriteria(OrderCriteria criteria) { // (5)
    
            // Collect dynamic conditions.
            // (6)
            final List<String> andConditions = new ArrayList<String>();
            final List<String> joinConditions = new ArrayList<String>();
            final Map<String, Object> bindParameters = new HashMap<String, Object>();
    
            // (7)
            if (criteria.getId() != null) {
                andConditions.add("o.id = :id");
                bindParameters.put("id", criteria.getId());
            }
            if (!CollectionUtils.isEmpty(criteria.getStatusCodes())) {
                andConditions.add("o.status.code IN :statusCodes");
                bindParameters.put("statusCodes", criteria.getStatusCodes());
            }
            if (StringUtils.hasLength(criteria.getItemName())) {
                joinConditions.add("o.orderItems oi");
                joinConditions.add("oi.item i");
                andConditions.add("i.name LIKE :itemName ESCAPE '~'");
                bindParameters.put("itemName", QueryEscapeUtils
                        .toLikeCondition(criteria.getItemName()));
            }
    
            // (8)
            if (andConditions.isEmpty()) {
                return Collections.emptyList();
            }
    
            // (9)
            // Create dynamic query.
            final StringBuilder queryString = new StringBuilder();
    
            // (10)
            queryString.append("SELECT o FROM Order o");
    
            // (11)
            // add join conditions.
            for (String joinCondition : joinConditions) {
                queryString.append(" LEFT JOIN ").append(joinCondition);
            }
            // add conditions.
            Iterator<String> andConditionsIt = andConditions.iterator();
            if (andConditionsIt.hasNext()) {
                queryString.append(" WHERE ").append(andConditionsIt.next());
            }
            while (andConditionsIt.hasNext()) {
                queryString.append(" AND ").append(andConditionsIt.next());
            }
    
            // (12)
            // add order by condition.
            queryString.append(" ORDER BY o.id");
    
            // (13)
            // Create typed query.
            final TypedQuery<Order> findQuery = entityManager.createQuery(
                    queryString.toString(), Order.class);
            // Bind parameters.
            for (Map.Entry<String, Object> bindParameter : bindParameters
                    .entrySet()) {
                findQuery.setParameter(bindParameter.getKey(), bindParameter
                        .getValue());
            }
    
            // (14)
            // Execute query.
            return findQuery.getResultList();
    
        }
    
    }
    

    項番

    説明

    (3)
    カスタムRepositoryインタフェースの実装クラスを作成する。
    (4)
    EntityManagerをInjectする。
    @jakarta.persistence.PersistenceContextアノテーションを使用してInjectすること。
    (5)
    動的条件に一致するEntityを全件取得するQueryメソッドを実装する。
    上記実装例では説明のためにメソッド分割はしていないが、必要に応じてメソッド分割すること。
    (6)
    動的クエリを組み立てるための変数(AND条件用リスト、結合条件用リスト、バインドパラメータ用マップ)を定義している。
    OrderCriteriaオブジェクトに条件の指定があるものについて、これらの変数に必要な情報を設定する。
    (7)
    OrderCriteriaオブジェクトに条件の指定があるか判定し、動的クエリを組み立てるために必要な情報を設定していく。
    上記例では、idは指定した値と完全一致するもの、statusCodesは指定したリストに含まれるもの、itemNameは指定した値と前方一致するものを取得対象とするための情報を設定している。
    itemNameについては、比較対象の値を保持する関連Entityが複雑なネスト関係になっているため、関連EntityをJOINする必要がある。
    (8)
    上記実装例では条件が指定されていない場合は、検索する必要がないため、空のリストを返却する。
    (9)
    条件が指定されている場合は、Entityを検索するためのQueryを組み立てる。
    上記例では実行するQueryをjava.lang.StringBuilderクラスを使って組み立てる実装例となっている。
    (10)
    静的なQuery要素を組み立てる。
    上記例では、SELECT句とFROM句は静的なQueryの構成要素として組み立てている。
    (11)
    動的なQuery要素を組み立てる。
    上記例では、結合条件用リスト(JOIN句)とAND条件用リスト(WHERE句)に設定する条件を動的なQuery要素として組み立てている。
    (12)
    静的なQuery要素を組み立てる。
    上記例では、ORDER BY句は静的なQuery要素として組み立てている。
    (13)
    動的に組み立てたQuery文字列をjakarta.persistence.TypedQueryに変換し、Query実行に必要なバインド用のパラメータを設定する。
    (14)
    動的に組み立てたQueryを実行し、条件に一致するEntityを全件取得する。
  • Entity毎のRepositoryインタフェース

    public interface OrderRepository extends JpaRepository<Order, Integer>,
                                    OrderRepositoryCustom { // (15)
        // omitted
    }
    

    項番

    説明

    (15)
    Entity毎のRepositoryインタフェースに、カスタムRepositoryインタフェースを継承する。
  • Service (Caller)

    // condition values for sample.
    Integer conditionValueOfId = 4;
    List<String> conditionValueOfStatusCodes = Arrays.asList("accepted");
    String conditionValueOfItemName = "Wat";
    
    // implementation of sample.
    // (16)
    OrderCriteria criteria = new OrderCriteria();
    criteria.setId(conditionValueOfId);
    criteria.setStatusCodes(conditionValueOfStatusCodes);
    criteria.setItemName(conditionValueOfItemName);
    List<Order> orders = orderRepository.findAllByCriteria(criteria); // (17)
    if (orders.isEmpty()) { // (18)
        // omitted
    }
    

    項番

    説明

    (16)
    OrderCriteriaオブジェクトに検索条件を指定する。
    (17)
    OrderCriteriaオブジェクトを引数として、動的条件に一致するEntityを全件取得するQueryメソッドを呼び出す。
    (18)
    必要に応じて検索結果を判定し、0件の場合の処理を行う。
  • 実行されるJPQL(SQL)

    -- (19)
    --   conditionValueOfId=4
    --   conditionValueOfStatusCodes = ["accepted"]
    --   conditionValueOfItemName = "Wat"
    
    -- JPQL
    SELECT
            o
        FROM
            ORDER o
                JOIN o.orderItems oi
                    JOIN oi.item i
                WHERE
                    o.id = :id
                    AND o.status.code IN :statusCodes
                    AND i.name LIKE :itemName ESCAPE '~'
                ORDER BY
                    o.id
    
    -- SQL
    SELECT
            order0_.id AS id1_6_
            ,order0_.created_by AS created2_6_
            ,order0_.created_date AS created3_6_
            ,order0_.last_modified_by AS last4_6_
            ,order0_.last_modified_date AS last5_6_
            ,order0_.status_code AS status6_6_
        FROM
            t_order order0_ INNER JOIN t_order_item orderitems1_
                ON order0_.id = orderitems1_.order_id INNER JOIN m_item item2_
                ON orderitems1_.item_code = item2_.code
    WHERE
        order0_.id = 4
        AND (
            order0_.status_code IN ('accepted')
        )
        AND (
            item2_.name LIKE 'Wat%' ESCAPE '~'
        )
    ORDER BY
        order0_.id
    

    項番

    説明

    (19)
    すべての条件を指定した場合に発行されるJPQLとSQLの例。
    -- (20)
    --   conditionValueOfId=4
    --   conditionValueOfStatusCodes = ["accepted"]
    --   conditionValueOfItemName = ""
    -- JPQL
    SELECT
            o
        FROM
            ORDER o
        WHERE
            o.id = :id
            AND o.status.code IN :statusCodes
        ORDER BY
            o.id
    
    -- SQL
    SELECT
            order0_.id AS id1_6_
            ,order0_.created_by AS created2_6_
            ,order0_.created_date AS created3_6_
            ,order0_.last_modified_by AS last4_6_
            ,order0_.last_modified_date AS last5_6_
            ,order0_.status_code AS status6_6_
        FROM
            t_order order0_
        WHERE
            order0_.id = 4
            AND (
                order0_.status_code IN ('accepted')
            )
        ORDER BY
            order0_.id;
    

    項番

    説明

    (20)
    iteName以外の条件を指定した場合に発行されるJPQLとSQLの例。
    -- (21)
    --   conditionValueOfId=4
    --   conditionValueOfStatusCodes = []
    --   conditionValueOfItemName = ""
    -- JPQL
    SELECT
            o
        FROM
            ORDER o
        WHERE
            o.id = :id
        ORDER BY
            o.id
    
    -- SQL
    SELECT
            order0_.id AS id1_6_
            ,order0_.created_by AS created2_6_
            ,order0_.created_date AS created3_6_
            ,order0_.last_modified_by AS last4_6_
            ,order0_.last_modified_date AS last5_6_
            ,order0_.status_code AS status6_6_
        FROM
            t_order order0_
        WHERE
            order0_.id = 4
        ORDER BY
            order0_.id;
    

    項番

    説明

    (21)
    idのみを指定した場合に発行されるJPQLとSQLの例。

6.3.2.7.2. 動的条件に一致するEntityをページ検索

動的条件に一致するEntityの中から指定ページに該当するEntityを取得するQueryメソッドを実装し、呼び出す。

以下に実装例を示すが、該当ページを取得する箇所以外は、全件取得する場合と同じ仕様とする。
なお、全件取得で説明した箇所の説明は省略する。
  • カスタムRepositoryインタフェース

    public interface OrderRepositoryCustom {
    
        Page<Order> findPageByCriteria(OrderCriteria criteria, Pageable pageable); // (1)
    
    }
    

    項番

    説明

    (1)
    動的条件に一致するEntityの中から指定ページに該当するEntityを取得するQueryメソッドを定義する。
  • カスタムRepositoryクラス

    public class OrderRepositoryCustomImpl implements OrderRepositoryCustom {
    
        @PersistenceContext
        EntityManager entityManager;
    
        public Page<Order> findPageByCriteria(OrderCriteria criteria,
                Pageable pageable) { // (2)
    
            // collect dynamic conditions.
            final List<String> andConditions = new ArrayList<String>();
            final List<String> joinConditions = new ArrayList<String>();
            final Map<String, Object> bindParameters = new HashMap<String, Object>();
    
            if (criteria.getId() != null) {
                andConditions.add("o.id = :id");
                bindParameters.put("id", criteria.getId());
            }
            if (!CollectionUtils.isEmpty(criteria.getStatusCodes())) {
                andConditions.add("o.status.code IN :statusCodes");
                bindParameters.put("statusCodes", criteria.getStatusCodes());
            }
            if (StringUtils.hasLength(criteria.getItemName())) {
                joinConditions.add("o.orderItems oi");
                joinConditions.add("oi.item i");
                andConditions.add("i.name LIKE :itemName ESCAPE '~'");
                bindParameters.put("itemName", QueryEscapeUtils.toLikeCondition(criteria
                        .getItemName()));
            }
    
            if (andConditions.isEmpty()) {
                List<Order> orders = Collections.emptyList();
                return new PageImpl<Order>(orders, pageable, 0); // (3)
            }
    
            // create dynamic query.
            final StringBuilder queryString = new StringBuilder();
            final StringBuilder countQueryString = new StringBuilder(); // (4)
            final StringBuilder conditionsString = new StringBuilder(); // (4)
    
            queryString.append("SELECT o FROM Order o");
            countQueryString.append("SELECT COUNT(o) FROM Order o"); // (5)
    
            // add join conditions.
            for (String joinCondition : joinConditions) {
                conditionsString.append(" JOIN ").append(joinCondition);
            }
    
            // add conditions.
            Iterator<String> andConditionsIt = andConditions.iterator();
            if (andConditionsIt.hasNext()) {
                conditionsString.append(" WHERE ").append(andConditionsIt.next());
            }
            while (andConditionsIt.hasNext()) {
                conditionsString.append(" AND ").append(andConditionsIt.next());
            }
            queryString.append(conditionsString); // (6)
            countQueryString.append(conditionsString); // (6)
    
            // add order by condition.
            // (7)
            String orderByString = QueryUtils.applySorting("", pageable.getSort(), "o");
            queryString.append(orderByString);
    
            // create typed query.
            final TypedQuery<Long> countQuery = entityManager.createQuery(
                    countQueryString.toString(), Long.class); // (8)
    
            final TypedQuery<Order> findQuery = entityManager.createQuery(
                    queryString.toString(), Order.class);
    
            // bind parameters.
            for (Map.Entry<String, Object> bindParameter : bindParameters
                    .entrySet()) {
                countQuery.setParameter(bindParameter.getKey(), bindParameter
                        .getValue()); // (8)
                findQuery.setParameter(bindParameter.getKey(), bindParameter
                        .getValue());
            }
    
            long total = countQuery.getSingleResult().longValue(); // (9)
            List<Order> orders = null;
            if (total != 0) { // (10)
                findQuery.setFirstResult(pageable.getOffset());
                findQuery.setMaxResults(pageable.getPageSize());
                // execute query.
                orders = findQuery.getResultList();
            } else { // (11)
                orders = Collections.emptyList();
            }
    
            return new PageImpl<Order>(orders, pageable, total); // (12)
        }
    
    }
    

    項番

    説明

    (2)
    動的条件に一致するEntityの中から指定ページに該当するEntityを取得するQueryメソッドを実装する。
    上記実装例では説明のためにメソッド分割はしていないが、必要に応じてメソッド分割すること。
    (3)
    上記実装例では条件が指定されていない場合は、検索する必要がないため、空のページ情報を返却する。
    (4)
    件数取得用のQueryを組み立てるための変数と、条件(結合条件とAND条件)を組み立てるための変数を用意する。
    Entity取得用のQueryと件数取得用のQueryで同じ条件を使用する必要があるため、条件(結合条件とAND条件)を組み立てるための変数を用意している。
    (5)
    件数取得用Queryの静的なQuery要素を組み立てる。
    上記例では、SELECT句とFROM句は静的なQueryの構成要素として組み立てている。
    (6)
    Entity取得用Queryと件数取得用Queryの動的なQuery要素を組み立てる。
    (7)
    Entity取得用Queryに対してソート条件(ORDER BY句)を動的なQuery要素として組み立てている。
    ORDER BY句の組み立ては、 Spring Data JPAから提供されているユーティリティ部品(org.springframework.data.jpa.repository.query.QueryUtils)を使用している。
    (8)
    動的に組み立てた件数取得用のQuery文字列をjakarta.persistence.TypedQueryに変換し、Query実行に必要なバインド用のパラメータを設定する。
    (9)
    件数取得用のQueryを実行し、条件に一致する合計件数を取得する。
    (10)
    条件に一致するEntityが存在する場合は、Entity取得用Queryを実行し、該当ページの情報を取得する。
    Entity取得用Queryを実行する際は、取得開始位置(TypedQuery#setFirstResult)と取得件数(TypedQuery#setMaxResults)を指定する。
    (11)
    条件に一致するEntityが存在しない場合は、空のリストを取得結果とする。
    (12)
    該当ページのEntityのリスト、ページ情報、条件に一致した合計件数を引数に指定して、Pageオブジェクトを生成し返却する。
  • Service (Caller)

    // condition values for sample.
    Integer conditionValueOfId = 4;
    List<String> conditionValueOfStatusCodes = Arrays.asList("accepted");
    String conditionValueOfItemName = "Wat";
    
    // implementation of sample.
    OrderCriteria criteria = new OrderCriteria();
    criteria.setId(conditionValueOfId);
    criteria.setStatusCodes(conditionValueOfStatusCodes);
    criteria.setItemName(conditionValueOfItemName);
    Page<Order> orderPage = orderRepository.findPageByCriteria(criteria,
            pageable); // (13)
    if (!orderPage.hasContent()) {
        // omitted
    }
    

項番

説明

(13)
動的条件に一致するEntityの中から指定ページに該当するEntityを取得するQueryメソッドを呼び出す。

6.3.2.8. Entityの取得処理の実装

Entityの取得方法について、目的別に説明する。


6.3.2.8.1. IDを指定してEntityを1件取得

ID(Primary Key)がわかっている場合は、RepositryインタフェースのfindByIdメソッドを呼び出してEntityオブジェクトを取得する。

public Account getAccount(String accountUuid) {
    Account account = accountRepository.findById(accountUuid); // (1)
    if (account == null) { // (2)
        // ...
    }
    return account;
}

項番

説明

(1)
EntityのID(Primary Key)を指定して、RepositryインタフェースのfindById(ID)メソッドを呼び出す。
(2)
指定したIDのEntityが存在しない場合は、返却値がnullになるので、null判定が必要となる。
必要に応じて、指定したIDのEntityが存在しない場合の処理を実装する。

Note

返却されるEntityオブジェクトについて

指定したIDのEntityオブジェクトが、既にEntityManager上で管理されている場合は、永続層(DB)へのアクセスは行われずに、EntityManager上で管理されているEntityオブジェクトが返却される。

そのため、 findByIdメソッドを使用すると、永続層への無駄なアクセスを抑えることが出来る。

Note

関連Entityのロードタイミングについて

Query実行時の関連Entityのロードは、Entityの関連付けアノテーション(@jakarta.persistence.OneToOne@jakarta.persistence.OneToMany@jakarta.persistence.ManyToOne@jakarta.persistence.ManyToMany)のfetch属性に指定されている値によって決定される。

  • jakarta.persistence.FetchType#LAZYの場合は、JOIN FETCH対象からはずれるため、関連Entityのロードは初回アクセス時に行われる。

  • jakarta.persistence.FetchType#EAGERの場合は、JOIN FETCHされるため、関連Entityがロードされる。

fetch属性のデフォルトはアノテーションによって異なる。デフォルト値は以下の通り。

  • @OneToOneアノテーション :EAGER

  • @ManyToOneアノテーション :EAGER

  • @OneToManyアノテーション :LAZY

  • @ManyToManyアノテーション :LAZY

Note

1:N(N:M)の関連をもつ関連Entityの並び順について

関連Entityのプロパティのアノテーションに、@jakarta.persistence.OrderByアノテーションを指定して制御する。

Note

Hibernate 5.2.12以降における関連エンティティのロードについて

関連Entityのキー項目(@Idが付与されたフィールド)は主Entityに含まれるため、取得のため関連Entityをロードする必要がない。 このため、Hibernate 5.2.12より関連Entityのキー項目以外にアクセスしたときのみ関連Entityがロードされるよう改善された。

例を以下に示す。

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy // (1)
private Set<OrderItem> orderItems;

項番

説明

(1)
value属性を指定しない場合は、 IDの昇順となる。

Todo

TBD

今後、以下の内容を追加する予定である。

  • fetch属性をデフォルト値から変更した方がよいケースの例。


6.3.2.8.2. ID以外の条件を指定してEntityを1件取得

IDがわからない場合は、ID以外の条件でEntityを検索するQueryメソッドを呼び出す。

  • Repositroyインタフェース

    public interface AccountRepository extends JpaRepository<Account, String> {
    
        // (1)
        @Query("SELECT a FROM Account a WHERE a.accountId = :accountId")
        Account findByAccountId(@Param("accountId") String accountId);
    
    }
    

    項番

    説明

    (1)
    ID以外の項目を条件として、Entityを1件検索するQueryメソッドを定義する。
  • Service

    public Account getAccount(String accountId) {
        Account account = accountRepository.findByAccountId(accountId); // (2)
        if (account == null) { // (3)
            // omitted
        }
    

    項番

    説明

    (2)
    Repositryインタフェースに実装したQueryメソッドを呼び出す。
    (3)
    RepositryインタフェースのfindByIdメソッドと同様に、条件に一致するEntityが存在しない場合は、返却値がnullになるので、null判定が必要となる。
    必要に応じて、指定した条件に一致するEntityが存在しない場合の処理を実装する。
    条件に一致するEntityが複数存在した場合は、org.springframework.dao.IncorrectResultSizeDataAccessExceptionが発生する。

    Note

    返却されるEntityオブジェクトについて

    Queryメソッドを呼び出した場合、必ず永続層(DB)に対してQueryが実行される。ただし、Queryを実行して取得されたEntityが既にEntityManager上で管理されている場合は、 Queryを実行して取得されたEntityオブジェクトは破棄され、EntityManager上で管理されているEntityオブジェクトが返却される。

    Note

    ID + α を条件とするQueryメソッドについて

    ID + α を条件とするQueryメソッドは、原則作成しないようにすることを推奨する。このケースは、 findByIdメソッドを呼び出して取得したEntityオブジェクトのプロパティ値を比較するロジックを組むことで、同じことが実現できる。

    原則作成しないことを推奨する理由は、Queryを実行して取得されたEntityオブジェクトが破棄される可能性があり、無駄なQueryの実行となってしまうためである。IDがわかっているのであれば、 無駄なQueryの実行が行われないfindByIdメソッドを使用した方がよい。特に、性能要件が高いアプリケーションの場合は、意識して実装すること。

    ただし、以下の条件に当てはまる場合は、Queryメソッドを使用した方がQueryの実行回数が少なくなる事もあるので、その場合はQueryメソッドを使用してもよい。

    • + α の条件の部分に関連オブジェクトのプロパティ(関連テーブルのカラム)が含まれる。

    • 条件となる関連EntityへのFetchTypeにLAZYが含まれる。

    Note

    関連Entityのロードタイミングについて

    JOIN FETCHに指定された関連Entityは、Query実行直後にロードされる。

    JOIN FETCHに指定がない関連Entityは、関連付けアノテーション(@OneToOne@OneToMany@ManyToOne@ManyToMany)のfetch属性に指定されている値によって以下の動作になる。

    • jakarta.persistence.FetchType#LAZYの場合は、Lazy Load対象となり、関連Entityのロードは初回アクセス時に行われる。

    • jakarta.persistence.FetchType#EAGERの場合は、関連EntityをロードするためのQueryが実行され、関連Entityのオブジェクトがロードされる。

    Note

    1:N(N:M)の関連をもつ関連Entityの並び順について

    • JOIN FETCHに指定された関連Entityの並び順は、JPQLに”ORDER BY”句を指定して制御する。

    • Query実行後にロードされる関連Entityの並び順は、関連Entityのプロパティのアノテーションに@jakarta.persistence.OrderByアノテーションを指定して制御する。


6.3.2.9. Entityの追加処理の実装

Entityの追加方法について、目的別に実装例を説明する。


6.3.2.9.1. Entityの追加

Entityを追加したい場合は、Entityオブジェクトを生成し、Repositryインタフェースのsaveメソッドを呼び出す。

  • Service

    Order order = new Order("accepted"); // (1)
    order = orderRepository.save(order); // (2)
    

    項番

    説明

    (1)
    Entityオブジェクトのインスタンスを生成し、必要なプロパティに値を設定する。
    上記例では、IDの設定はJPAのID採番機能を使用している。JPAのID採番機能を使用する場合は、アプリケーションコードでIDの設定は行ってはいけない。
    アプリケーションコードでIDを設定してしまうと、EntityManagerのmergeメソッドが呼び出されるため、不要な処理が実行されてしまう。
    (2)
    Repositoryインタフェースのsaveメソッドを呼び出し、(1)で生成したEntityオブジェクトをEntityManagerの管理対象にする。
    EntityManagerによって管理されるEntityオブジェクトは、saveメソッドの引数に渡したEntityオブジェクトではなく、saveメソッドから返却されたEntityオブジェクトになるので注意すること。
    IDは、このタイミングでJPAのID採番機能によって設定される。

    Note

    mergeメソッドが呼び出されるデメリット

    EntityManagerのmergeメソッドは、EntityをEntityManagerの管理対象にする際に、永続層(DB)から同じIDをもつEntityを取得する仕組みになっている。

    Entityの追加処理としては、Entityの取得処理は無駄な処理となる。高い性能要件があるアプリケーションの場合は、IDの採番タイミングを意識すること。

    Note

    制約エラーのハンドリング

    saveメソッドを呼び出したタイミングでは、永続層(DB)にQuery(INSERT)は実行されない。そのため、一意制約違反などの制約系のエラーをハンドリングする必要がある場合は、Repositoryインタフェースのsaveメソッドではなく、saveAndFlushメソッドまたはflushメソッドを呼び出す必要がある。

  • Entity

    @Entity // (3)
    @Table(name = "t_order") // (4)
    public class Order implements Serializable {
    
        // (5)
        @SequenceGenerator(name = "GEN_ORDER_ID", sequenceName = "s_order_id",
                           allocationSize = 1)
        @GeneratedValue(strategy = GenerationType.SEQUENCE,
                        generator = "GEN_ORDER_ID")
        @Id // (6)
        private int id;
    
        // (7)
        @ManyToOne
        @JoinColumn(name = "status_code")
        private OrderStatus status;
    
        // omitted
    
        public Order(String statusCode) {
            this.status = new OrderStatus(statusCode);
        }
    
        // omitted
    
    }
    

    項番

    説明

    (3)
    Entityクラスには@jakarta.persistence.Entityアノテーションを付与する。
    (4)
    Entityクラスとマッピングするテーブル名は@jakarta.persistence.Tableアノテーションのname属性に指定する。
    エンティティ名からテーブル名が解決できる場合は指定しなくてもよいが、解決できない場合は指定すること。
    (5)
    JPAのID採番機能を使用するために必要なアノテーションを指定している。
    JPAのID採番機能を使用する場合は、@jakarta.persistence.GeneratedValueアノテーションを指定する。
    シーケンスオブジェクトを使用する場合は、@jakarta.persistence.SequenceGeneratorアノテーション、採番テーブルを使用する場合は、@jakarta.persistence.TableGeneratorアノテーションの指定も必要となる。
    上記例では、s_order_idという名前のシーケンスオブジェクトを使ってIDを採番している。
    (6)
    主キーを保持するプロパティに、@jakarta.persistence.Idアノテーションを付与する。
    複合キーの場合は、@jakarta.persistence.EmbeddedIdアノテーションを付与する。
    (7)
    別のEntityとの関連を保持するプロパティに、 関連付けアノテーション(@OneToOne@OneToMany@ManyToOne@ManyToMany)を付与する。

    Note

    ID採番用のアノテーションについて

    各アノテーションの詳細は、Jakarta Persistence APIのSpecificationを参照されたい。

    Note

    IDの採番方法について

    採番方法は、@GeneratedValueアノテーションのstrategy属性にjakarta.persistence.GenerationType列挙の値を指定する。

    指定可能な値は以下の通り。

    • TABLE: 永続層(DB)のテーブルを使用してIDを採番する。

    • SEQUENCE: 永続層(DB)のシーケンスオブジェクトを使用してIDを採番する。

    • IDENTITY: 永続層(DB)のidentity列を使用してIDを採番する。

    • AUTO: 永続層(DB)に最も適切な採番方法を選択してIDを採番する。

    原則AUTOは使用せず、明示的に使用するタイプを指定することを推奨する。


6.3.2.9.2. Entityと関連Entityの追加

Entityと関連Entityを一緒に追加したい場合は、Repositryインタフェースのsaveメソッドを呼び出してEntityオブジェクトをEntityManagerの管理対象にした後に、関連Entityのオブジェクトを生成し、Entityオブジェクトに関連づける。

この方法を使用するためには、関連EntityのCascade対象の操作に、persistが含まれている必要がある。

Note

Cascade対象となった場合の挙動について

関連EntityがCascade対象に指定されている場合、Entityに対するJPAの操作(persist,merge,remove,refresh,detach)が関連Entityに対して、連鎖して行われる。

Spring Data JPAのRepositoryインタフェースの操作に対するマッピングは以下の通り。

  • saveメソッド :persistormerge

  • deleteメソッド :remove

refresh,detachは Spring Data JPAのデフォルト実装で実行されることはない。

  • Service

    String itemCode = "ITM0000001";
    int itemQuantity = 10;
    String wayToPay = "card";
    
    Order order = new Order("accepted");
    order = orderRepository.save(order); // (1)
    
    OrderItem orderItem = new OrderItem(order.getId(), itemCode,
            itemQuantity);
    order.setOrderItems(Collections.singleton(orderItem));    // (2)
    order.setOrderPay(new OrderPay(order.getId(), wayToPay)); // (2)
    

    項番

    説明

    (1)
    Repositoryインタフェースのsaveメソッドを呼び出し、まずEntityオブジェクトをEntityManagerの管理対象にする。
    上記例では、Entityオブジェクトに対して、関連Entityのオブジェクトを設定する前に、saveメソッドを呼び出している。これは、関連EntityのIDの一部として使用されるEntityのID(orderId)を採番する必要があるためである。
    (2)
    EntityManagerの管理対象となっているEntityオブジェクトに対して、関連Entityのオブジェクトを設定する。
    トランザクションのコミット時に、Entityオブジェクトへのpersist操作(INSERT)が、関連Entityのオブジェクトに対して連鎖される。
  • Entity

    @Entity
    @Table(name = "t_order")
    public class Order implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        @Id
        @SequenceGenerator(name = "GEN_ORDER_ID", sequenceName = "s_order_id",
                           allocationSize = 1)
        @GeneratedValue(strategy = GenerationType.SEQUENCE,
                        generator = "GEN_ORDER_ID")
        private int id;
    
        @OneToMany(mappedBy = "order", cascade = CascadeType.ALL,  // (3)
                   orphanRemoval = true)
        @OrderBy
        private Set<OrderItem> orderItems;
    
        @OneToOne(mappedBy = "order", cascade = CascadeType.ALL,
                  orphanRemoval = true)
        private OrderPay orderPay;
    
        @ManyToOne
        @JoinColumn(name = "status_code")
        private OrderStatus status;
    
        // omitted
    
        public Order(String statusCode) {
            this.status = new OrderStatus(statusCode);
        }
    
        // omitted
    
    }
    

    項番

    説明

    (3)
    関連アノテーションのcascade属性に、Cascade対象にする操作のタイプ(jakarta.persistence.CascadeType)を指定する。
    上記例では、すべての操作を関連EntityのCascade対象としている。
    特に理由がない場合は、すべての操作をCascade対象にしておくことを推奨する。

    Warning

    cascade属性を指定してはいけない関連Entityについて

    トランザクション系のEntityからコード系およびマスタ系のEntityに関連をもつ場合は、cascade属性は指定してはいけない。

    上記例だと、OrderStatusはコード系のEntityになるのでOrderstatusプロパティの@ManyToOneアノテーションにcascade属性は指定してはいけない。

  • 関連Entity

    @Entity
    @Table(name = "t_order_item")
    public class OrderItem implements Serializable {
    
        @EmbeddedId
        private OrderItemPK id;
    
        private int quantity;
    
        @ManyToOne
        @JoinColumn(name = "order_id", insertable = false, updatable = false)
        private Order order;
    
        // omitted
    
        public OrderItem(Integer orderId, String itemCode, int quantity) {
            this.id = new OrderItemPK(orderId, itemCode);
            this.quantity = quantity;
        }
    
        // omitted
    
    }
    
    @Entity
    @Table(name = "t_order_pay")
    public class OrderPay implements Serializable {
    
        @Id
        @Column(name = "order_id")
        private Integer orderId;
    
        @Column(name = "way_to_pay")
        private String wayToPay;
    
        @OneToOne
        @JoinColumn(name = "order_id")
        private Order order;
    
        // omitted
    
        public OrderPay(int orderId, String wayToPay) {
            this.orderId = orderId;
            this.wayToPay = wayToPay;
        }
    
        // omitted
    
    }
    

6.3.2.9.3. 関連Entityの追加

関連Entityを追加したい場合は、Repositoryインタフェース経由で取得したEntityオブジェクトに対して、生成した関連Entityのオブジェクトを関連付ける。

この方法を使用するためには、関連EntityのCascade対象の操作に、persistmergeが含まれている必要がある。

String itemCode = "ITM0000003";
int quantity = 30;

Order order = orderRepository.findById(orderId).get(); // (1)

OrderItem orderItem = new OrderItem(order.getId(), itemCode, quantity);
order.getOrderItems().add(orderItem); // (2)

OrderPay orderPay = order.getOrderPay();
if (orderPay == null) {
    order.setOrderPay(new OrderPay(order.getId(), "cash")); // (3)
} else {
    orderPay.setWayToPay("cash");
}

項番

説明

(1)
Repositoryインタフェースのメソッドを使用して、Entityオブジェクトを取得する。
(2)
1:Nの関連Entityの場合、生成した関連Entityのオブジェクトを、Entityオブジェクトから取得したコレクションに追加する。
(3)
1:1の関連Entityの場合、生成した関連EntityのオブジェクトをEntityオブジェクトに設定する。

6.3.2.9.4. 関連Entityの直接追加

関連Entityを、親のEntityオブジェクトに関連付けせずに直接追加したい場合は、関連Entity用のRepositoryインタフェースを使用して保存する。

String itemCode = "ITM0000003";
int quantity = 40;

OrderItem orderItem = new OrderItem(orderId, itemCode, quantity); // (1)

orderItemRepository.save(orderItem); // (2)

項番

説明

(1)
関連Entityのオブジェクトを生成する。
(2)
関連Entity用のRepositoryインタフェースのsaveメソッドを呼び出す。

Note

関連Entityを直接保存するメリット

生成されるオブジェクトの数が少なくなる。 親のEntityオブジェクトを取得する場合、処理に不要な関連Entityのオブジェクトが生成される可能性がある。

Note

関連Entityを直接保存するデメリット

IDの一部に親EntityのIDを使用している場合、saveメソッドを呼び出す前にIDを設定しておく必要がある。 IDを設定してしまうとSpring Data JPAのデフォルト実装はEntityManagerのmergeメソッドを呼び出す。 そのため、必ず永続層(DB)からIDが同じEntityを取得する処理が実行されてしまう。

Warning

追加後に親のEntityオブジェクトを利用する際の注意点

関連Entity用Repositoryのsaveメソッドを使って関連Entity追加した場合は、親にあたるEntityオブジェクトには関連付けられていないため、親のEntityオブジェクトを経由して取得することができない。

回避方法は、

  1. 関連Entityのオブジェクトを直接追加するのではなく、親のEntityオブジェクトに関連付けて追加するようにする。

  2. saveAndFlushメソッドを使用することで、永続層(DB)と同期を親のEntityを取得する前に行う。

このケースの場合、特に理由がないのであれば、前者の方法で関連Entityのオブジェクトを追加すること。

後者は、親にあたるEntityオブジェクトが既に管理状態のEntityになっていた場合に、問題を回避することができない。


6.3.2.10. Entityの更新処理の実装

Entityの更新方法について、目的別に実装例を説明する。


6.3.2.10.1. Entityの更新

Entityを更新したい場合は、Repositoryインタフェースのメソッドを使用して取得したEntityオブジェクトに対して、変更したい値を設定する。

Order order = orderRepository.findById(orderId).get(); // (1)
order.setStatus(new OrderStatus("checking")); // (2)

項番

説明

(1)
Repositoryインタフェースのメソッドを使用して、Entityオブジェクトを取得する。
(2)
Entityオブジェクトの状態を、setterメソッドを呼び出して更新する。

Note

Repositoryのsaveメソッドの呼び出しについて

Repositoryインタフェースのメソッドを使用して取得したEntityオブジェクトは、EntityManager上で管理されている。

EntityManager上で管理されているEntityオブジェクトは、setterメソッドを使ってオブジェクトの状態を変更するだけで、トランザクションコミット時に変更内容が永続層(DB)に反映される仕組みになっている。そのため、Repositoryインタフェースのsaveメソッドを明示的に呼び出す必要はない。

ただし、EntityオブジェクトがEntityManager上で管理されていない場合は、 saveメソッドを呼び出す必要がある。

例えば、画面から送信されたリクエストパラメータをもとにEntityオブジェクトを生成した場合などが、このケースに当てはまる。


6.3.2.10.2. 関連Entityの更新

関連Entityを更新したい場合は、Repositoryインタフェースのメソッドを使用して取得したEntityオブジェクトから取得した関連Entityのオブジェクトに対して、変更したい値を設定する。

この方法を使用するためには、関連EntityのCascade対象の操作に、mergeが含まれている必要がある。

Order order = orderRepository.findById(orderId).get(); // (1)

for (OrderItem orderItem : order.getOrderItems()) {
    int newQuantity = quantityMap.get(orderItem.getId().getItemCode());
    orderItem.setQuantity(newQuantity); // (2)
}

order.getOrderPay().setWayToPay("cash"); // (3)

項番

説明

(1)
Repositoryインタフェースのメソッドを使用して、Entityオブジェクトを取得する。
(2)
1:Nの関連Entityの場合、Entityオブジェクトから取得したコレクションに格納されている関連Entityのオブジェクトの状態を、setterメソッドを呼び出して更新する。
(3)
1:1の関連Entityの場合、Entityオブジェクトから取得した関連Entityのオブジェクトの状態を、setterメソッドを呼び出して更新する。

6.3.2.10.3. 関連Entityの直接更新

関連Entityを、親のEntityを使用せず直接更新したい場合は、関連Entity用のRepositoryインタフェースのメソッドを使用して取得したEntityオブジェクトに対して、変更したい値を設定する。

int quantity = 43;

OrderItem orderItem = orderItemRepository.findById(new OrderItemPK(
        orderId, itemCode)).get(); // (1)

orderItem.setQuantity(quantity); // (2)

項番

説明

(1)
関連EntityのRepositoryインタフェースのfindByIdメソッドを呼び出して、関連Entityのオブジェクトを取得する。
(2)
関連Entityのオブジェクトの状態を、setterメソッドを使って更新する。

Note

更新後に親のEntityを利用した場合の動作について

関連Entity用Repositoryのsaveメソッドを使って関連Entityを更新した場合は、追加時とは異なり、親のEntityオブジェクトで保持している関連Entityにも反映される。これは、EntityManager上で管理されている同じインスタンスの参照を保持しているためである。


6.3.2.10.4. Queryメソッドを使用して更新

永続層(DB)のEntityを直接更新したい場合は、Queryメソッドを使用して更新する。
詳細は、「永続層のEntityを直接操作する」を参照されたい。

6.3.2.11. Entityの削除処理の実装

6.3.2.11.1. Entityと関連Entityの削除

Entityと関連Entityを一緒に削除したい場合は、Repositryインタフェースのdeleteメソッドを呼び出す。

この方法を使用するためには、関連EntityのCascade対象の操作にremoveが含まれているか、または関連Entityを削除するための設定を有効(関連付けアノテーションのorphanRemoval属性をtrue)にする必要がある。

  • Service

    orderRepository.deleteById(orderId); // (1)
    

    項番

    説明

    (1)
    IDまたはEntityオブジェクトを指定してRepositryインタフェースのdeleteメソッドを呼び出す。
  • Entity

    @Entity
    @Table(name = "t_order")
    public class Order implements Serializable {
    
        // omitted
    
        @OneToMany(mappedBy = "order",
                   cascade = CascadeType.ALL, orphanRemoval = true) // (2)
        @OrderBy
        private Set<OrderItem> orderItems;
    
        // omitted
    
    }
    

    項番

    説明

    (2)
    Cascade対象の操作にremoveを含め、 関連Entityを削除するための設定を有効 (関連付けアノテーションのorphanRemoval属性をtrue)にする。

    Note

    関連Entityの削除について

    関連Entityのオブジェクトを一緒に削除したくない場合は、 Cascade対象の操作にremoveが含まれないようにしておく必要がある。また、関連付けアノテーションのorphanRemoval属性もfalseに指定しておくこと。


6.3.2.11.2. 関連Entityの削除

関連Entityを削除したい場合は、Repositoryインタフェース経由で取得したEntityオブジェクトから、関連Entityのオブジェクトを削除する。

この方法を使用するためには、関連Entityを削除するための設定を有効(関連付けアノテーションのorphanRemoval属性をtrue)にする必要がある。

Note

関連Entityを削除する設定を有効にした場合の挙動について

関連Entityを削除する設定を有効にした場合、以下の動作になる。

  • 1:Nの関連Entityの場合は、関連Entityのオブジェクトをコレクションの中から削除すると、トランザクションコミット時に永続層(DB)からも削除される。

  • 1:1の関連Entityの場合は、関連Entityをnullに設定すると、トランザクションコミット時に永続層(DB)からも削除される。

orphanRemoval属性のデフォルト値となっているfalseが指定されている場合は、メモリ上からは消えるが、削除した関連Entityのオブジェクトに対する永続処理(UPDATE/DELETE)は行われない。

  • Service

    Order order = orderRepository.findById(orderId); // (1)
    
    // (2)
    Set<OrderItem> orderItemsOfRemoveTarget = new LinkedHashSet<OrderItem>(); // (3)
    for (OrderItem orderItem : order.getOrderItems()) {
        String itemCode = orderItem.getId().getItemCode();
        if (quantityMap.containsKey(itemCode)) {
            int newQuantity = quantityMap.get(itemCode);
            orderItem.setQuantity(newQuantity);
        } else {
            orderItemsOfRemoveTarget.add(orderItem); // (4)
        }
    }
    order.getOrderItems().removeAll(orderItemsOfRemoveTarget); // (5)
    
    order.setOrderPay(null); // (6)
    

    項番

    説明

    (1)
    Repositoryインタフェースのメソッドを使用して、Entityオブジェクトを取得する。
    (2)
    1:Nの関連Entityを削除する実装例について説明する。
    (3)
    削除する関連Entityのオブジェクトを格納しておくコレクションを用意する。
    (4)
    削除対象の関連Entityのオブジェクトを、(3)のコレクションに追加する。
    (5)
    削除対象の関連Entityのオブジェクトから取得したコレクションの中から削除する。
    (6)
    1:1の関連Entityの場合、削除したい関連Entityのオブジェクトを保持するプロパティをnullに設定する。
  • Entity

    @Entity
    @Table(name = "t_order")
    public class Order implements Serializable {
    
        @Id
        @SequenceGenerator(name = "GEN_ORDER_ID", sequenceName = "s_order_id", allocationSize = 1)
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "GEN_ORDER_ID")
        private int id;
    
        @OneToMany(mappedBy = "order", cascade = CascadeType.ALL,
                   orphanRemoval = true) // (7)
        @OrderBy
        private Set<OrderItem> orderItems;
    
        @OneToOne(mappedBy = "order", cascade = CascadeType.ALL,
                  orphanRemoval = true) // (7)
        private OrderPay orderPay;
    
        @ManyToOne
        @JoinColumn(name = "status_code")
        private OrderStatus status;
    
        // omitted
    
    }
    

    項番

    説明

    (7)
    関連Entityを削除するための設定を有効(関連付けアノテーションのorphanRemoval属性をtrue)にする。

    Note

    orphanRemoval属性が指定できる関連付けアノテーションについて

    orphanRemoval属性が指定できる関連付けアノテーションは、@OneToOne@OneToManyの2つとなっている。


6.3.2.11.3. 関連Entityの直接削除

関連Entityを、親のEntityを使用せず直接削除したい場合は、関連EntityのRepositoryインタフェースのdeleteメソッドを呼び出す。

int quantity = 43;

orderItemRepository.deleteByOrderItem(new OrderItemPK(orderId, itemCode)); // (1)

項番

説明

(1)
IDまたはEntityオブジェクトを指定して関連Entity用Repositryインタフェースのdeleteメソッドを呼び出す。

Warning

直接削除の注意点

関連Entity用Repositoryのdeleteメソッドを呼び出した場合、永続層(DB)から削除されないケースがあるので注意すること。

削除されないケースは以下の通り。

  • deleteメソッド内の中でfindByIdメソッドを呼び出しているため、親のEntityとの関連が@OneToOneの場合は、親のEntityがEntityManager上で管理されてしまう。 親のEntityが管理対象になった際に、deleteメソッドで削除しようとしていた関連Entityが親のEntityオブジェクト内にロードされる可能性がある。 親のEntityオブジェクト内にロードされてしまうと、永続層(DB)から削除されない。

  • deleteメソッド呼び出し後に、親のEntityオブジェクトを取得した場合、deleteメソッドで削除した関連Entityが親のEntityオブジェクト内にロードされる可能性がある。 親のEntityオブジェクト内にロードされてしまうと、永続層(DB)から削除されない。

回避方法は、

  1. 関連Entityのオブジェクトを直接削除するのではなく、親のEntityとの関連付けを削除するようにする。

関連付けアノテーションの見直しを行うことで回避出来る可能性はあるが、確実な回避方法は、直接削除を行わないようにする方法である。


6.3.2.11.4. Queryメソッドを使用して削除

永続層(DB)からEntityを直接削除したい場合は、Queryメソッドを使用して削除する。
詳細は、「永続層のEntityを直接操作する」を参照されたい。

Warning

関連オブジェクトの扱いについて

Queryメソッドを使用して永続層(DB)から直接Entityを削除した場合、関連付けアノテーションの指定に関係なく、関連Entityのオブジェクトは永続層から削除されない。

Repositoryインタフェースのvoid deleteAllInBatch(Iterable<T> entities)void deleteAllInBatch()も同様の動作となる。


6.3.2.12. LIKE検索時のエスケープについて

LIKE検索を行う場合は、検索条件として使用する値をLIKE検索用にエスケープする必要がある。
LIKE検索用のエスケープ処理は、共通ライブラリから提供しているorg.terasoluna.gfw.common.query.QueryEscapeUtilsクラスのメソッドを使用する事で実現することができる。
共通ライブラリから提供しているエスケープ処理の仕様については、「データベースアクセス(共通編)」の「LIKE検索時のエスケープについて」を参照されたい。

以下に、共通ライブラリから提供しているエスケープ処理の使用方法について説明する。


6.3.2.12.1. 一致方法をQuery側で指定する場合の使用方法

一致方法(前方一致、後方一致、部分一致)の指定をJPQLとして指定する場合は、エスケープのみ行うメソッドを使用する。

  • Repository

    // (1) (2)
    @Query("SELECT a FROM Article a WHERE"
            + " (a.title LIKE %:word% ESCAPE '~' OR a.overview LIKE %:word% ESCAPE '~')")
    Page<Article> findPageBy(@Param("word") String word, Pageable pageable);
    

    項番

    説明

    (1)
    @Queryアノテーションに指定するJPQL内にLIKE検索用のワイルドカード( “%“または “_“)を指定する。
    上記例では、引数wordの前後にワイルドカード( “%“)を指定することで、一致方法を部分一致にしている。
    (2)
    共通ライブラリから提供しているエスケープ処理は、エスケープ文字として”~“を使用しているため、 LIKE句の後ろにESCAPE '~'を指定する。

    Note

    ワイルドカード文字 “_” について

    ワイルドカード文字”%“は、「@Query アノテーションで指定する」で説明しているように、@Queryアノテーションを利用した場合に限り直接JPQL内で使用することができる。 ワイルドカード文字”_“は直接JPQL内のLIKE検索に利用することはできないため、下記2方法で利用すること。

    1. ワイルドカード文字”_“をバインド変数内に含める。

    2. ワイルドカード文字”_“をCONCATなどのデータベース製品が持つ文字列結合機能を利用してバインド変数に結合する。


  • Service

    @Transactional(readOnly = true)
    public Page<Article> searchArticle(ArticleSearchCriteria criteria,
            Pageable pageable) {
    
        String escapedWord = QueryEscapeUtils.toLikeCondition(criteria.getWord()); // (3)
    
        Page<Article> page = articleRepository.findPageBy(escapedWord, pageable); // (4)
    
        return page;
    }
    

    項番

    説明

    (3)
    LIKE検索の一致方法をQuery側で指定する場合は、QueryEscapeUtils#toLikeCondition(String)メソッドを呼び出し、LIKE検索用のエスケープのみ行う。
    (4)
    LIKE検索用にエスケープされた値をQueryメソッドの引数に渡す。

6.3.2.12.2. 一致方法をロジック側で指定する場合の使用方法

一致方法(前方一致、後方一致、部分一致)をロジック側で判定する場合は、エスケープされた値にワイルドカードを付与するメソッドを使用する。

  • Repository

    // (1)
    @Query("SELECT a FROM Article a WHERE"
            + " (a.title LIKE :word ESCAPE '~' OR a.overview LIKE :word ESCAPE '~')")
    Page<Article> findPageBy(@Param("word") String word, Pageable pageable);
    

    項番

    説明

    (1)
    @Queryアノテーションに指定するJPQL内にLIKE検索用のワイルドカードは指定しない。

  • Service

    @Transactional(readOnly = true)
    public Page<Article> searchArticle(ArticleSearchCriteria criteria,
            Pageable pageable) {
    
        String word = QueryEscapeUtils
                .toContainingCondition(criteria.getWord()); // (2)
    
        Page<Article> page = articleRepository.findPageBy(word, pageable); // (3)
    
        return page;
    }
    

    項番

    説明

    (2)
    ロジック側で一致方法を指定する場合は、 以下の何れかのメソッドを呼び出し、LIKE検索用のエスケープとLIKE検索用のワイルドカードを付与する。
    QueryEscapeUtils#toStartingWithCondition(String)
    QueryEscapeUtils#toEndingWithCondition(String)
    QueryEscapeUtils#toContainingCondition(String)
    (3)
    LIKE検索用にエスケープ+ワイルドカードが付与された値をQueryメソッドの引数に渡す。

6.3.2.13. JOIN FETCHについて

JOIN FETCHは、関連するEntityをJOINして一括で取得することで、Entityを取得するために発行するQueryの数を減らすための仕組みである。
任意のQueryを実行してEntityを取得する場合、関連付けアノテーションのfetch属性がEAGERとなっているプロパティは必ず JOIN FETCH すること。
JOIN FETCHしないと、関連Entityを取得するためのQueryが別途発行されるため、性能に影響を与える可能性がある。
関連付けアノテーションのfetch属性がLAZYとなっているプロパティについては、使用頻度を考慮して JOIN FETCH するか判断すること。
必ず参照される関連Entityの場合は JOIN FETCHすべきであるが、参照されるケースが少ないのであれば JOIN FETCH せず、初回アクセス時にQueryを発行して取得する方がよい。
  • Repository

    @Query("SELECT a FROM Article a"
            + " INNER JOIN FETCH a.articleClass"   // (1)
            + " WHERE a.publishedDate = :publishedDate"
            + " ORDER BY a.articleId DESC")
    List<Article> findAllByPublishedDate(
            @Param("publishedDate") Date publishedDate);
    

    項番

    説明

    (1)
    [LEFT [OUTER] | INNER] JOIN FETCH Join対象のプロパティの形式で指定する。
    具体的には以下のパターンとなる。

    1. LEFT JOIN FETCH
    関連Entityが存在しない場合でもEntityは取得される。
    SQLとしては、left outer join RelatedTable r on m.fkColumn = r.fkColumnとなる。
    2. LEFT OUTER JOIN FETCH
    1と同様。
    3. INNER JOIN FETCH
    関連Entityが存在しない場合は、Entityは取得されない。
    SQLとしては、inner join RelatedTable r on m.fkColumn = r.fkColumnとなる。
    4. JOIN FETCH
    3と同様。

    上記例では、ArticleクラスのarticleClassプロパティをJOIN FETCH対象として指定している。

    Note

    JOIN FETCHはN+1問題の解決策の一つとして使用される。

    N+1問題については、「N+1問題の対策方法」を参照されたい。


6.3.3. How to extend

6.3.3.1. カスタムメソッドの追加方法

Spring Dataでは、Repositoryインタフェースに対して、任意のカスタムメソッドを追加する仕組みを提供している。

項番

追加方法

使用ケース

Entity毎のRepositoryインタフェースに個別に追加する

Spring Dataより提供されているQueryメソッドの仕組みで表現できないQueryを実装する必要がある場合に使用する。
動的Queryを実装する場合は、この方法を使用してメソッドを追加する。

すべてのRepositoryインタフェースに一括で追加する

すべてのRepositoryインタフェースで使用できるような汎用的なQueryを実装する必要がある場合に使用する。
プロジェクト固有の汎用Queryを実装する場合は、この方法を使用してメソッドを追加する。
Spring Data JPAから提供されているデフォルト実装(SimpleJpaRepository)の振る舞いを変更する必要がある場合、この仕組みを使用してメソッドをオーバーライドすることで対応することが出来る。

6.3.3.1.1. Entity毎のRepositoryインタフェースに個別に追加する

Entity毎のRepositoryインタフェースに個別にカスタムメソッドを追加する方法について説明する。

  • Entity毎のカスタムRepositoryインタフェース

    // (1)
    public interface OrderRepositoryCustom {
    
        // (2)
        Page<Order> findByCriteria(OrderCriteria criteria, Pageable pageable);
    
    }
    

    項番

    説明

    (1)
    カスタムメソッドを定義するカスタムインタフェースを作成する。
    インタフェース名に特に制約はないが、Entity毎のRepositoryインタフェース名 +Customとすることを推奨する。
    (2)
    カスタムメソッドを定義する。
  • Entity毎のカスタムRepositoryクラス

    // (3)
    public class OrderRepositoryImpl implements OrderRepositoryCustom {
    
        @PersistenceContext
        EntityManager entityManager; // (4)
    
        // (5)
        public Page<Order> findByCriteria(OrderCriteria criteria, Pageable pageable) {
            // omitted
            return new PageImpl<Order>(orders, pageable, totalCount);
        }
    
    }
    

    項番

    説明

    (3)
    カスタムインタフェースの実装クラスを作成する。
    クラス名は、Entity毎のRepositoryインタフェース名 +Implとすること。
    (4)
    Queryを実行するために必要となるjakarta.persistence.EntityManager@jakarta.persistence.PersistenceContextアノテーションを使ってインジェクションする。
    (5)
    カスタムインタフェースに定義したメソッドを実装する。
  • Entity毎のRepositoryインタフェース

    public interface OrderRepository extends JpaRepository<Order, Integer>,
            OrderRepositoryCustom { // (6)
        // omitted
    }
    

    項番

    説明

    (6)
    Entity毎のRepositoryインタフェースでカスタムインタフェースを継承する。
    Repositoryインタフェースを継承することで、実行時にカスタムインタフェースの実装クラスのメソッドが呼び出される。
  • Service(Caller)

    public Page<Order> search(OrderCriteria criteria, Pageable pageable) {
        return orderRepository.findByCriteria(criteria, pageable); // (7)
    }
    

    項番

    説明

    (7)
    呼び出し側は、他のメソッドと同様にEntity毎のRepositoryインタフェースのメソッドを呼び出せばよい。
    上記例では、OrderRepository#findByCriteria(OrderCriteria, Pageable)を呼び出すとOrderRepositoryImpl#findByCriteria(OrderCriteria, Pageable)が実行される。

6.3.3.1.2. すべてのRepositoryインタフェースに一括で追加する

すべてのRepositoryインタフェースに一括でカスタムメソッドを追加する方法について説明する。

下記の実装例では、取得したEntityとのバージョンチェックを行い、バージョンが異なる場合に楽観排他エラーとするメソッドを追加している。

  • 共通のRepositoryインタフェース

    // (1)
    @NoRepositoryBean // (2)
    public interface MyProjectRepository<T, ID extends Serializable> extends
            JpaRepository<T, ID> {
    
        // (3)
        T findByIdWithValidVersion(ID id, Integer version);
    
    }
    

    項番

    説明

    (1)
    追加するメソッドを定義するための共通Repositoryインタフェースを作成する。
    上記例では、Spring Dataから提供されているJpaRepositoryを継承して共通Repositoryインタフェースを作成している。
    (2)
    共通Repositoryインタフェースの実装クラス( 例だとMyProjectRepositoryImpl)が、RepositoryのBeanとして自動登録されないようにするためのアノテーション。
    共通Repositoryインタフェースの実装クラスを<jpa:repositories>要素のbase-package属性で指定したパッケージ配下に格納する場合は、このアノテーションを指定しないと起動時にエラーとなる。
    (3)
    追加するメソッドを定義する。例では、取得したEntityのバージョンチェックを行うメソッドを追加している。
  • 共通Repositoryインタフェースの実装クラス

    // (6)
    public class MyProjectRepositoryImpl<T, ID extends Serializable>
            extends SimpleJpaRepository<T, ID>
            implements MyProjectRepository<T, ID> {
    
        private JpaEntityInformation<T, ID> entityInformation;
        private EntityManager entityManager;
        Method versionMethod;
    
        // (7)
        public MyProjectRepositoryImpl(
                JpaEntityInformation<T, ID> entityInformation,
                EntityManager entityManager) {
            super(entityInformation, entityManager);
    
            this.entityInformation = entityInformation; // (8)
            this.entityManager = entityManager; // (8)
    
            try {
                versionMethod = entityInformation.getJavaType().getMethod("getVersion");
            } catch (NoSuchMethodException | SecurityException e) { }
    
        }
    
        // (9)
        public T findByIdWithValidVersion(ID id, Integer version) {
    
            if (versionMethod == null) {
                throw new UnsupportedOperationException(
                        String.format(
                                "Does not found version field in entity class. class is '%s'.",
                                entityInformation.getJavaType().getName()));
            }
    
            T entity = findById(id);
    
            if (entity != null && version != null) {
                Integer currentVersion;
                try {
                    currentVersion = (Integer) versionMethod.invoke(entity);
                } catch (IllegalAccessException | IllegalArgumentException
                        | InvocationTargetException e) {
                    throw new IllegalStateException(e);
                }
                if (!version.equals(currentVersion)) {
                    throw new ObjectOptimisticLockingFailureException(
                            entityInformation.getJavaType().getName(), id);
                }
            }
    
            return entity;
        }
    
    }
    

    項番

    説明

    (6)
    共通Repositoryインタフェース(例だと、MyProjectRepository)の実装クラスを作成する。
    上記例では、Spring Dataから提供されているSimpleJpaRepositoryを継承して実装クラスを作成している。
    (7)
    org.springframework.data.jpa.repository.support.JpaEntityInformationjakarta.persistence.EntityManagerを引数にとるコンストラクタを作成する。
    (8)
    追加するメソッドの処理で必要な場合は、JpaEntityInformationおよびEntityManagerをフィールドに保持しておく。
    (9)
    共通Repositoryインタフェースに定義したメソッドの実装を行う。

    Note

    デフォルト実装の変更

    デフォルト実装(SimpleJpaRepository)の振る舞いを変更する場合は、このクラスで振る舞いを変更したいメソッドをオーバーライドすればよい。

  • Entity毎のRepositoryインタフェース

    public interface OrderRepository extends MyProjectRepository<Order, Integer> { // (10)
        // omitted
    }
    

    項番

    説明

    (10)
    Entity毎のRepositoryインタフェースでは、作成した共通インタフェース(例だとMyProjectRepository)を継承先として指定する。
  • XxxInfraConfig.java

    @Configuration
    @EnableJpaRepositories(basePackages = "xxx.yyy.zzz.domain.repository"
                           repositoryBaseClass = MyProjectRepositoryImpl.class) // (11)
    public class XxxInfraConfig {
    
        // omitted
    

    項番

    説明

    (11)
    @EnableJpaRepositoriesアノテーションの要素のrepositoryBaseClass属性に、作成した共通Repositoryインタフェースの実装クラス(例だとMyProjectRepositoryImpl) のクラス名を指定する。
  • Service(Caller)

    public Order updateOrder(Order chngedOrder, Integer version) {
    
        Order order = orderRepository.findByIdWithValidVersion(chngedOrder.getId(), version); // (12)
    
        // omitted
    
        return order;
    }
    

    項番

    説明

    (12)
    呼び出し側は、他のメソッドと同様にEntity毎のRepositoryインタフェースのメソッドを呼び出せばよい。
    上記例では、OrderRepository#findByIdWithValidVersion(Integer, Integer)を呼び出すとMyProjectRepositoryImpl#findByIdWithValidVersion(Integer, Integer)が実行される。

6.3.3.2. Entity以外のオブジェクトにQueryの取得結果を格納する方法

Queryの取得結果をEntityにマッピングするのではなく、別のオブジェクトにマッピングすることができる。 これは、永続層(DB)に格納されているレコードをEntity以外のオブジェクト(JavaBean)として扱いたい時に使用する。

Note

想定される適用ケース

  1. Query内で集計関数を使用して集計済みの情報を取得したい場合は、集計結果をEntityにマッピングすることはできたいため、別のオブジェクトにマッピングする必要がある。
  2. 巨大なEntityの中の一部の情報のみ参照したい場合や、ネストが深い関連Entityの一部の情報のみ参照したい場合は、必要なプロパティのみ定義したJavaBeanにマッピングして取得する事を検討した方がよい場合がある。
    Entityとして取得した場合、アプリケーションの処理に不要な項目に対するマッピング処理が行われる点や、処理に不要な情報を取得することでメモリを無駄に使用する点などにより、処理性能に影響を与える場合があるためである。
    処理性能に大きな影響を与えない場合は、Entityとして取得してよい。

以下の実装例を示す。

  • JavaBean

    // (1)
    public class OrderSummary implements Serializable {
    
        private Integer id;
        private Long totalPrice;
    
        // omitted
    
        public OrderSummary(Integer id, Long totalPrice) { // (2)
            super();
            this.id = id;
            this.totalPrice = totalPrice;
        }
    
        // omitted
    
    }
    

    項番

    説明

    (1)
    Query結果を格納するJavaBeanを作成する。
    (2)
    Queryを実行した結果でオブジェクト生成するためのコンストラクタを用意する。
  • Repositoryインタフェース

    // (3)
    @Query("SELECT NEW xxx.yyy.zzz.domain.model.OrderSummary(o.id, SUM(i.price*oi.quantity))"
            + " FROM Order o LEFT JOIN o.orderItems oi LEFT JOIN oi.item i"
            + " GROUP BY o.id ORDER BY o.id DESC")
    List<OrderSummary> findOrderSummaries();
    

    項番

    説明

    (3)
    (2)で作成したコンストラクタを指定する。
    “NEW”キーワードの後に FQCNでコンストラクタを指定する。

6.3.3.3. Audit用プロパティの設定方法

Spring Data JPAより提供されている、永続層のAudit用プロパティ(作成者、作成日時、最終更新者、最終更新日時)に値を設定する仕組みと適用方法について説明する。

Spring Data JPAでは、新たに作成されたEntityと更新されたEntityに対して、Audit用プロパティに値を設定する仕組みを提供している。
この仕組みを使用すると、ServiceなどのアプリケーションのコードからAudit用プロパティに値を設定する処理を切り離すことができる。

Note

アプリケーションのコードからAudit用プロパティに値を設定する処理を切り離す理由

  1. Audit用プロパティへの値の設定は、一般的にアプリケーション要件ではなく、データの監査要件によって必要になる処理である。
    本質的にはServiceなどのアプリケーション内のコードで意識すべき処理ではないため、アプリケーションのコードから切り離した方がよい。
  2. JPAでは、永続層から取得したEntityに対して値の変更があった場合のみ、永続層へ反映(SQLを発行)する仕様になっており、
    永続層へ無駄なアクセスが行われないように制御されている。
    ServiceなどのアプリケーションのコードでAudit用プロパティに無条件に値を設定してしまうと、Audit用プロパティ以外に変更がない場合でも、永続層への反映が行われることになるため、JPAの有効な機能を無駄にしてしまう。
    値の変更があった場合のみAudit用プロパティに値を設定するように制御すればこの問題は回避できるが、アプリケーションのコードが煩雑になるため推奨できない。

Note

値の変更有無に関係なく永続層のAudit用カラムを更新する必要がある場合

値に変更がなくても永続層のAudit用カラム(最終更新者、最終更新日時)を更新するのが、アプリケーションの仕様となっている場合は、ServiceなどのアプリケーションのコードでAudit用プロパティを設定する必要がある。

ただし、このケースではデータモデリングまたはアプリケーション仕様が間違っている可能性があるため、データモデリングおよびアプリケーション仕様の見直しを行った方がよい。


以下に、適用時の実装例を示す。

  • Entityクラス

    public class XxxEntity implements Serializable {
        private static final long serialVersionUID = 1L;
    
        // omitted
    
        @Column(name = "created_by")
        @CreatedBy // (1)
        private String createdBy;
    
        @Column(name = "created_date")
        @CreatedDate // (2)
        private LocalDateTime createdDate; // (3)
    
        @Column(name = "last_modified_by")
        @LastModifiedBy // (4)
        private String lastModifiedBy;
    
        @Column(name = "last_modified_date")
        @LastModifiedDate // (5)
        private LocalDateTime lastModifiedDate; // (3)
    
        // omitted
    
    }
    

    項番

    説明

    (1)
    作成者を保持するフィールドに@org.springframework.data.annotation.CreatedByアノテーションを付与する。
    (2)
    作成日時を保持するフィールドに@org.springframework.data.annotation.CreatedDateアノテーションを付与する。
    (3)
    作成日時を保持するフィールドの型は、java.util.Datejava.util.Calendarjava.lang.Longlong型 、Date and Time APIなどをサポートしている。
    最終更新日時のフィールドも同様。
    (4)
    最終更新者を保持するフィールドの型に@org.springframework.data.annotation.LastModifiedByアノテーションを付与する。
    (5)
    最終更新日時を保持するフィールドに@org.springframework.data.annotation.LastModifiedDateアノテーションを付与する。

    Warning

    @Typeアノテーションは、JPAの標準アノテーションではなく、Hibernate独自のアノテーションである。

  • AuditorAwareインタフェースの実装クラス

    // (6)
    @Component // (7)
    public class SpringSecurityAuditorAware implements AuditorAware<String> {
    
        // (8)
        public String getCurrentAuditor() {
            Authentication authentication = SecurityContextHolder.getContext()
                    .getAuthentication();
            if (authentication == null || !authentication.isAuthenticated()) {
                return null;
            }
            return ((UserDetails) authentication.getPrincipal()).getUsername();
        }
    
    }
    

    項番

    説明

    (6)
    org.springframework.data.domain.AuditorAwareインタフェースの実装クラスを作成する。
    AuditorAwareインタフェースは、Entityの操作者(作成者または最終更新者)を解決するためのインタフェースとなっている。
    このクラスはプロジェクト毎に作成する必要がある。
    (7)
    @Componentアノテーションを付与することで、component-scan対象になるようにしている。
    @Componentアノテーションを付けずに、Bean定義ファイルにbean定義してもよい。
    (8)
    Entityの操作者(作成者または最終更新者)のプロパティに設定するオブジェクトを返却する。
    上記例では、SpringSecurityによって認証されたログインユーザのユーザ名(文字列)をEntityの操作者として返却している。
  • Object/relational mapping file(orm.xml)

    <?xml version="1.0" encoding="UTF-8"?>
    
    <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm
        http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
        version="2.0">
    
        <persistence-unit-metadata>
            <persistence-unit-defaults>
                <entity-listeners>
                    <entity-listener
                        class="org.springframework.data.jpa.domain.support.AuditingEntityListener" /> <!-- (9) -->
                </entity-listeners>
            </persistence-unit-defaults>
        </persistence-unit-metadata>
    
    </entity-mappings>
    

    項番

    説明

    (9)
    Entity Listenerとして、org.springframework.data.jpa.domain.support.AuditingEntityListenerクラスを指定する。
    このクラスで実装しているメソッドにて、Audit用プロパティに値を設定している。
  • XxxInfraConfig.java

    @Configuration
    @EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware") // (10)
    // omitted
    public class XxxInfraConfig {
    
        // omitted
    

    項番

    説明

    (10)
    @EnableJpaAuditingアノテーションのauditorAwareRef属性に、(6)で作成したEntityの操作者を解決するためのクラスのbeanを指定する。
    上記例では、SpringSecurityAuditorAwareという実装クラスをcomponent-scanしているので、springSecurityAuditorAwareというbean名を指定している。

@CreatedDateおよび@LastModifiedDateアノテーションが付与されたフィールドに設定される値は、デフォルト実装だとorg.springframework.data.auditing.CurrentDateTimeProvidergetNow()メソッドから返却されるjava.util.Optionalにラップされた値が使用される。

以下に、使用する値の生成方法を変更する場合の拡張例を示す。

// (1)
@Component // (2)
public class AuditDateTimeProvider implements DateTimeProvider {

    @Inject
    ClockFactory clockFactory;

    // (3)
    @Override
    public Optional<TemporalAccessor> getNow() {
        Clock clock = clockFactory.fixed();
        Instant instant = clock.instant();
        return Optional.of(instant);
    }

}

項番

説明

(1)
org.springframework.data.auditing.DateTimeProviderインタフェースの実装クラスを作成する。
(2)
@Componentアノテーションを付与することで、component-scan対象になるようにしている。
@Componentアノテーションを付けずに、Bean定義ファイルにbean定義してもよい。
(3)
Entityの操作日時(作成日時、最終更新日時)のプロパティに設定するインスタンスを返却する。
上記例では、共通ライブラリから提供しているorg.terasoluna.gfw.common.time.ClockFactoryから取得したjava.time.Instantをラップしたjava.util.Optionalを操作日時として返却している。
ClockFactoryの詳細については、「システム時刻」を参照されたい。
  • XxxInfraConfig.java

    @Configuration
    @EnableJpaAuditing(dateTimeProviderRef = "auditDateTimeProvider") // (4)
    // omitted
    public class XxxInfraConfig {
    
        // omitted
    

    項番

    説明

    (4)
    @EnableJpaAuditingアノテーションのdateTimeProviderRef属性に、(1)で作成したEntityの操作日時に設定する値を返却するクラスのbeanを指定する。
    上記例では、AuditDateTimeProviderという実装クラスをcomponent-scanしているので、auditDateTimeProviderというbean名を指定している。

6.3.3.4. 永続層からEntityを取得するJPQLに共通条件を加える方法

永続層(DB)からEntityを取得するためのJPQLに共通条件を加える仕組みを紹介する。
これはJPAの標準仕様ではなく、Hibernateが拡張した仕組み機能である。

Note

想定される適用ケース

データの監査やデータの保存期間などの要件により、Entityを削除する際に、レコードの物理削除(DELETE)ではなく、論理削除(論理削除フラグのUPDATE)を行うようなアプリケーションが有効である。

このケースでは、アプリケーションとしては論理削除されたレコードは一律検索対象から除外する必要があるため、Queryひとつひとつに除外するための条件を加えるより、共通条件として指定した方がよい。

Warning

適用範囲について

この仕組みを適用したEntityを操作するすべてのQueryに対して、指定した条件が追加される点に注意すること。条件を加えたくないQueryが1つでもある場合は、この仕組みは使用できない。


6.3.3.4.1. Entityを取得するJPQLに共通条件を加える

Repositoryインタフェースのメソッド呼び出し時に実行されるJPQLに対して、共通の条件を加える方法を以下に示す。

  • Entity

    @Entity
    @Table(name = "t_order")
    @SQLRestriction(value = "is_logical_delete = false") // (1)
    public class Order implements Serializable {
        // omitted
        @Id
        private Integer id;
        // omitted
    }
    
  • RepositoryインタフェースのfindByIdメソッドを呼び出した時に発行されるSQL

    SELECT
            -- ....
        FROM
            t_order order0_
       WHERE
            order0_.id = 1
            AND (
                order0_.is_logical_delete = false -- (2)
            );
    

    項番

    説明

    (1)
    Entityのクラスアノテーションとして、@org.hibernate.annotations.SQLRestrictionアノテーションを付与し、 value属性に共通の条件を指定する。
    ここで指定するWHERE句の書式は、JPQLではなく、SQLで指定する必要がある。つまりJavaオブジェクトのプロパティ名ではなくカラム名を指定する必要がある。
    (2)
    @SQLRestrictionアノテーションで指定した条件が追加されている。

Note

指定可能なクラスについて

@SQLRestrictionアノテーションは、@Entityが付与されているクラスでのみ有効である。つまり、@jakarta.persistence.MappedSuperclassを付与した基底Entityクラスに付与してもSQLに付与されない。

Warning

@SQLRestrictionアノテーションは、JPAの標準アノテーションではなく、Hibernate独自のアノテーションである。


6.3.3.4.2. 関連Entityを取得するJPQLに共通条件を加える

Repositoryインタフェースのメソッド呼び出して取得したEntityの関連Entityを取得するためのJPQLに対して、共通の条件を加える方法を以下に示す。

  • Entity

    @Entity
    @Table(name = "t_order")
    @SQLRestriction(value = "is_logical_delete = false")
    public class Order implements Serializable {
        // omitted
        @Id
        private Integer id;
    
        @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
        @OrderBy
        @SQLRestriction(value ="is_logical_delete = false") // (1)
        private Set<OrderItem> orderItems;
        // omitted
    
    }
    
  • 関連Entityにアクセスした際に発行されるSQL(Lazy Load)

    SELECT
            -- ...
        FROM
            t_order_item orderitems0_
        WHERE
            (orderitems0.is_logical_delete = false) -- (2)
            AND orderitems0_.order_id = 1
        ORDER BY
            orderitems0_.item_code ASC
            ,orderitems0_.order_id ASC;
    
  • Entityと関連Entityを同時に取得する際に発行されるSQL(Eager Load / Join Fetch)

    SELECT
        -- ...
        FROM
            t_order order0_
                LEFT OUTER JOIN t_order_item orderitems1_
                    ON order0_.id = orderitems1_.order_id
                    AND (orderitems1_.is_logical_delete = false) -- (2)
        WHERE
            order0_.id = 1
            AND (
                order0_.is_logical_delete = false
            )
        ORDER BY
            orderitems1_.item_code ASC
            ,orderitems1_.order_id ASC;
    

    項番

    説明

    (1)
    関連Entityのフィールドアノテーションとして、@org.hibernate.annotations.Whereアノテーションを付与し、 value属性に共通の条件を指定する。
    ここで指定するWHERE句の書式は、JPQLではなく、SQLで指定する必要がある。つまりJavaオブジェクトのプロパティ名ではなくカラム名を指定する必要がある。
    (2)
    @SQLRestrictionアノテーションで指定した条件が追加されている。

    Note

    指定可能な関連の種類について

    関連Entityを取得する際のSQLに共通の条件を加えることができるのは、@OneToManyおよび@ManyToManyの関連をもつEntityに対してのみとなる。

    Warning

    @SQLRestrictionアノテーションは、JPAの標準アノテーションではなく、Hibernate独自のアノテーションである。


6.3.3.5. 複数のPersistenceUnitを使用する方法

Todo

TBD

今後、以下の内容を追加する予定である。

  • 複数のPersistenceUnitの使用例。


6.3.3.6. Nativeクエリの使用方法

Todo

TBD

今後、以下の内容を追加する予定である。

  • Nativeクエリを使用したQueryの実装例。