JPQL入門(JPA)

JPQL入門(JPA)

JPAでDB(正確にはエンティティ)からデータを取得するSQLをJPQLと言います。

SQLでfrom句に書いていたのはテーブル名ですが、JPQLではfrom句にはエンティティを書きます。(ここが重要)

@QueryアノテーションでJPQLを記述しますが、通常のSQL構文と少しだけ構文が変わります。

以下は簡単なJPQLの例です。

上記では、mがエイリアスです。select *ではなくselect mとします。value = は省略可能です。

JPQLで注意しないといけないのはテーブル名はエンティティクラス名を書く、カラム名はエンティティクラスのフィールド名を書く、という点です。

実際のテーブル名がemp_masterであってもそのテーブルに対応するエンティティクラスがEmpMasterクラスであれば、JPQL文ではEmpMasterと書きます。

UPDATE文やDELETE文を書く

JPQL文でUPDATE文やDELETE文を書くにはアノテーションがいくつか増えます。

アノテーション 意味
@Transactional クラスorメソッドが異常終了すればロールバックされる
@Modifying update文,insert文,delete文につける

コーディング例は以下の通りです。

LIKE検索をする

JPQLではLIKE検索ができます。バインド変数に%を付ける場合などは以下の通り記述します。

JPQLは色々制約がある

JPQLは色々と制約があるので、その場合はSQL文を書くしかありません。

SQLを記述する場合は、nativeQuery=trueとします。

以下、例です。

参考サイト:JPQLでFROM句に副問い合わせが使えない

ちなみにMySQLの日付計算で使うinterval,DAY,MONTH,YEARなどもJPQLでは使えないのでnativeQuery=trueにする必要があります。後述のFUNCTION式を使って回避することはできます。

エンティティ

エンティティクラスを作成しますが、決まりがあります。

  • @Entityアノテーションを付与
  • いずれかのフィールドに@Idを付与
  • 引数なしコンストラクタを実装

@Tableアノテーションは必須ではなく、テーブル名≠エンティティ名の場合に@Tableアノテーションでテーブル名を設定します。@Columnも必須ではなく、列名≠フィールド名の場合に@Columnを付与します。

主キー

エンティティの主キーには@Idアノテーションを追加します。

idフィールドが主キーの場合はエンティティクラスは以下のように@Idを指定します。

主キーがサロゲートキーの場合、@GeneratedValueアノテーションを付加します。ただしMySQLではjava.sql.SQLSyntaxErrorException: Table 'スキーマ.hibernate_sequence' doesn't existのエラーが発生します。これを回避するには@GeneratedValueアノテーションのstrategy属性にGenerationType.IDENTITYを指定します。(MySQLのAUTO_INCREMENTの場合です)

@GeneratedValueのstrategy属性は未指定の場合は、GenerationType.AUTOになります。

strategy値 意味
GenerationType.AUTO デフォルトの自動生成方法(MySQL不可)
GenerationType.IDENTITY MYSQL
GenerationType.SEQUENCE ORACLE or PostgreSQL
GenerationType.TABLE ORACLE

PostgreSQLの場合でSequenceオブジェクトを使用している場合は、@SequenceGeneratorのsequenceName属性でSequenceオブジェクト名を指定する必要があります。

エンティティに紐づかないSelect句の場合エラーとなる

JPQLが簡単なら良いのですが、例えば年月ごとに集計を求めるGroupなどを使う場合色々ややこしかったりします。

エンティティクラス

こんなエンティティがある場合にJPQLで年月日ではなく、年月をSelect句に入れるとエラーとなります。

これはLocalDateに対して年月にフォーマットした値になっているため、エラーとなります。

回避方法は新たにクラスを作成し、そのインスタンス生成時のコンストラクタに文字列として突っ込んであげます。select new(コンストラクタ式)を使用する場合はnativeQueryは使えませんのでご注意ください。

SampleEntityはこんな感じで単なるクラスです。

これでOKです。

コンストラクタ式に@Paramの引数(名前付きパラメータ)を入れたい場合はCASTしてあげる必要があります。

@ParamがString型であってもCASTでString型にCASTする必要があるので注意です。(MySQLで確認)

nameはString型ですが、コンストラクタ式の引数で使用する場合はCASTしてあげる必要があります。また完全修飾名で指定する必要があります。

コンストラクタ式を使わない場合

必ずコンストラクタ式を使わないといけないわけではありません。

例えば以下のようなメソッドも書くことができます。

List<Object[]>にすればOKです。

DTOなどのクラスに以下のようなメソッドを作成してあげます。

リポジトリのメソッド呼び出し側は以下のようにstream使えばきれいにコーディングができます。

これでコンストラクタ式を使わないでも記述することができます。

@EmbeddedId

主キーが一つであれば、@Idでよいのですが、複合主キーの場合は@IdClassや、@EmbeddedIdを使用します。@IdClassを使うケースがありますが冗長になるので敬遠されがちのようです。

エンティティは以下のように定義します。

implements Serializableしないとエラーとなります。で、この複合主キーに対して@Embeddableアノテーションを付加します。

リポジトリクラスは以下のように定義します。

複合主キーの場合は、@Queryの書き方に注意しないといけなくて、@Embeddedを使用する場合は、JPQLがm.id.empnoというようになります。

@Embeddableアノテーションの複合主キーで注意

便利ですが、コンストラクタ式としてこのキーをNEWするとエラーとなりましたので、コンストラクタ式でNEWしたい場合は@IdClassを利用します。

findByIdの引数に複合主キーを指定する

JpaRepositoryがデフォルトで用意しているfindByIdメソッドの引数に複合主キーを指定してみます。先ほど書いたエンティティを使用します。

サービスクラスからリポジトリをDIして呼び出します。

メソッド名からJPQLを生成する

メソッド名に一定の決まりがあって、その名前からJPQLを生成することができます。

Employeeというエンティティがあったとして、リポジトリに

というメソッドを作成すると

となります。

条件が2つある場合はfindByNameAndEmpnoとします。

このメソッドは以下を意味します。

以下はメソッド名に指定すると特別に意味を持ちます。

単語 メソッド名 JPQL
By findByXX where XX = :XX
And findByXXAndYY where XX = :XX and YY = :YY
Or findByOr where XX = :XX or YY = :YY
Like findByXXLike where XX like :XX
NotLike findByXXNotLike where XX not like :XX
Containing findByXXContaining where XX like ‘%:XX%’
IsNull findByXXIsNull where XX is null
IsNotNull findByXXIsNotNull where XX is not null
NotNull findByXX where XX is not null
Between findByXXBetween where XX between :XX1 and :XX2
LessThan findByXXLessThan where XX < :XX(数値) | | GreaterThan | findByXXGreaterThan | where XX > :XX(数値)
After findByXXAfter where XX > :XX(日時)
Before findByXXBefore where XX < :XX(日時)
OrderBy findByXXOrderByYYAsc where XX = :XX order by YY asc
Not findByXXNot where XX <> :XX
In findByXXIn where XX in (?,?…)
NotIn findByXXNotIn where XX not in (?,?…)
True findByXXTrue where XX = true
False findByXXFalse where XX = false
StartingWith findByXXStartingWith where XX like ‘:XX%’
EndingWith findByXXEndingWith where XX like ‘%:XX’

@EmbeddedIdを使用していると命名規則がややこしい

@EmbeddedIdを使用している場合(複合主キー)、メソッド名がちょっとやっかいです。以下のような複合主キーのエンティティでPKを使用したい場合はメソッド名に@EmbeddedIdを付けたフィールド名を付けないと行けなくなります。

例えば、empnoで検索したい場合は

となります。

nativeな関数を使う方法

JPQLに用意されていない関数や各RDBに依存する関数をJPQLで使用したい場合

というようにします。

org.springframework.data.domain.Sortクラスでソートする

クエリー文のorder byでソートしなくてもorg.springframework.data.domain.Sortクラスを渡してソートすることが可能です。

org.springframework.data.domain.Sort.Directionというenumがあるので、ASCなら昇順、DESCなら降順です。第二引数はエンティティのフィールド名を指定します。

以下、リポジトリクラスです。JPQLでソートせずにSortクラスのインスタンスを渡します。

@QueryHintsを使用して大量データをフェッチする

@QueryHintsアノテーションを使用するとフェッチサイズを調整することができます。

フェッチサイズは@QueryHintのvalue属性に文字列で指定します。

nameにはorg.hibernate.jpa.QueryHints.HINT_FETCH_SIZEを指定します。以下はフェッチサイズを1000にした例です。

@EntityListeners(AuditingEntityListener.class)で登録者、登録時間、更新者、更新時間を設定する

エンティティに@EntityListeners(AuditingEntityListener.class)アノテーションを付与すると、登録者、更新者、登録時間、更新時間を設定することができます。

設定したいプロパティにアノテーションを付与します。

アノテーション 意味 クラス
@Version バージョン Integerとか
@CreatedBy 登録者 String
@CreatedDate 登録日時 LocalDateTime
@LastModifiedBy 更新者 String
@LastModifiedDate 更新日時 LocalDateTime

上記アノテーションを各プロパティに付与します。

以下はエンティティの例です。

import java.io.Serializable;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Table;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Table(name="account")
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Data
@EntityListeners(AuditingEntityListener.class)
public class AccountEntity {
@EmbeddedId
private Pk aaa;
@Column(name="branch_number")
private Integer branchNumber;
@Column(name="deposit_amount")
private Integer depositAmount;
@CreatedBy
@Column(name="register_user")
private String registerUser;
@CreatedDate
@Column(name="register_date")
private LocalDateTime registerDate;
@LastModifiedBy
@Column(name="update_user")
private String updateUser;
@LastModifiedDate
@Column(name="update_date")
private LocalDateTime updateDate;
@Embeddable
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Pk implements Serializable {
private static final long serialVersionUID = 624797775027966843L;
@Column(name="id")
private String id;
@Column(name="account_number")
private String accountNumber;
}
}

view raw
audit
hosted with ❤ by GitHub

これだけではsaveメソッドでインサートしても何も監査情報が登録されません。

設定クラスを作成しておく必要がありますorg.springframework.data.domain.AuditorAwareインタフェースを使用して以下のクラスを作成しておきます。ここでは、べた書きでtestuserとしています。

import java.util.Optional;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
public class AuditConfig {
@Bean
public AuditorAware<String> auditorAware() {
return new AuditorAware<String>() {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("testuser");
}
};
}
}

view raw
audit
hosted with ❤ by GitHub

これでsaveすると登録者や更新者にtestuser、登録時間や更新時間にその時間が自動で設定されるようになります。nativeQuery=trueの場合は自動で設定はされません。

監査情報はどのテーブルでも持つ情報なので親クラスを作成し、そちらに書く方が良いです。

ここで、親クラスに@MappedSuperclassアノテーションを付与し、各エンティティでextendsしてあげます。

親クラスのAbstractEntityクラスの例です。

package jp.co.confrage.domain.entity;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractEntity {
/** バージョン. */
@Version
@Column(name = "version")
private Integer version;
/** 作成者. */
@CreatedBy
@Column(name = "creator")
private String creatorCode;
/** 作成日時. */
@CreatedDate
@Column(name = "create_time")
private LocalDateTime createdTim;
/** 更新者. */
@LastModifiedBy
@Column(name = "updater")
private String updaterCode;
/** 更新日時. */
@LastModifiedDate
@Column(name = "update_time")
private LocalDateTime updatedTim;
}

view raw
AbstractEntity.java
hosted with ❤ by GitHub

DB側で設定する

MySQLの場合、テーブルで自動で設定することも可能です。

MySQLのテーブルに登録日時と更新日時を自動で設定する方法

jpa-named-queries.propertiesファイルを使用してJPQLをpropertiesに記述する

propertiesファイルにクエリーを記述することができます。デフォルトでは、META-INF/jpa-named-queries.propertiesに記述します。

リポジトリインターフェースの@Query(name=Sample.find)というように記述しこのname属性に指定した値とマッピングするJPQLをMETA-INF/jpa-named-queries.propertiesに記述します。

jpa-named-queries.properties

これでMETA-INF/jpa-named-queries.propertiesに記述することができました。

deleteメソッドよりもdeleteInBatchメソッドを使用する

Spring Data JPAではあらかじめdeleteメソッドが用意されているのですが、複数行削除する場合はその行数delete文が発行されてしまいます。その為deleteInBatchメソッドでバルクデリートしたほうが良いです。

deleteAllメソッドよりもdeleteAllInBatchメソッドを使用する

deleteAllメソッドもあらかじめ用意されているメソッドなのですが、全件削除してくれるのですが、レコード件数分delete文が実行されるだけです。その為、通信が多くなります。

deleteAllメソッドを使用するなら、deleteAllInBatchメソッドがありますので、そちらを使用すればバルクデリートすることが可能です。

jpqlのパフォーマンス

パフォーマンスと言っても速度やメモリ消費量などいろいろ観点があると思いますが、jpqlはnativeQuery=trueとした方がメモリ消費量はダントツに少ないです。

デフォルト(nativeQuery=false)だと、Javaのインスタンスを生成するので大量のデータを取得するとメモリを一気に消費します。あまりにも多いデータの場合はOOMEが発生するのでnativeQuery=trueが推奨されます。こちら速度について検証されいています。

参考サイト

org.springframework.data.domain.Pageクラスを使用したページング処理

大量のデータを取得する場合(findAllとか)Pageクラスを使用してページングしたほうが、表示件数を制限することができて、ページ単位の処理を行うことができて便利です。パフォーマンスの観点からもメモリ使用量がグッと減ります。

引数 意味
第一 0を基底値としたページ
第二 1ページ当たりのデータ単位

PageRequestクラスはnewではなくofメソッドでインスタンスを生成します。newしてインスタンス生成するのは非推奨になっているようです。

Page<エンティティ>クラスのインスタンスは以下のメソッドを持ちます。

戻り値 メソッド 意味
boolean hasContent コンテンツがあるかどうか
List getContent 1ページ当たりのコンテンツ
int getNumber 現在のページ番号
int getSize 1ページ当たりのサイズ
int getNumberOfElements 現在コンテンツで取得した件数
int getTotalElements ページ関係なく全レコード件数
int getTotalPages 全ページ数

ページをインクリメントしながら取得する場合は以下のように書けばよいと思います。

int i = 0; // ページの基底値
int size = 5; // 1ページ当たりのデータ件数
Page<AccountEntity> paging = accountRepository.findAll(PageRequest.of(i, size));
while(paging.hasContent()) {
List<AccountEntity> list = paging.getContent();
list.stream().forEach(e -> System.out.println(e.getAaa().getAccountNumber()));
paging = accountRepository.findAll(PageRequest.of(++i, size));
}

view raw
pageable
hosted with ❤ by GitHub

No identifier specified for entity

エンティティに@Id(javax.persistence.Id)が指定されていない場合にこのエラーが発生します。

@Idはエンティティに一つは必須です。

 Not supported for DML operations

更新系のSQLを発行した際に@Modifyingアノテーションをつけていない場合に「 Not supported for DML operations」エラーが発生します。

limit,offset

PostgreSQLでimit,offsetなどが使用できますが、JPQLでは対応していない為、TypedQueryインタフェースのsetFirstResultメソッド、setMaxResultsメソッドで代用できます。もしくはtop,firstキーワードを使用します。

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limit-query-result

column “xxx” is of type timestamp without time zone but expression is of type bytea

PostgreSQLでネイティブクエリのアップデート文で「column “xxx” is of type timestamp without time zone but expression is of type bytea」エラーが発生する場合があります。

hogeというtimestamp(6) without time zone型のカラムにnullを設定してアップデート文発行する為で、nativeQuery=falseにすれば正常にnullでアップデートされます。

  • このエントリーをはてなブックマークに追加
  • Evernoteに保存Evernoteに保存

コメントをどうぞ

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA