- JPQL入門(JPA)
- UPDATE文やDELETE文を書く
- LIKE検索をする
- JPQLは色々制約がある
- in句を使用する
- エンティティ
- 主キー
- エンティティに紐づかないSelect句の場合エラーとなる
- コンストラクタ式を使わない場合
- @EmbeddedId
- @Embeddableアノテーションの複合主キーで注意
- findByIdの引数に複合主キーを指定する
- メソッド名からJPQLを生成する
- @EmbeddedIdを使用していると命名規則がややこしい
- nativeな関数を使う方法
- org.springframework.data.domain.Sortクラスでソートする
- @QueryHintsを使用して大量データをフェッチする
- @EntityListeners(AuditingEntityListener.class)で登録者、登録時間、更新者、更新時間を設定する
- DB側で設定する
- jpa-named-queries.propertiesファイルを使用してJPQLをpropertiesに記述する
- deleteメソッドよりもdeleteInBatchメソッドを使用する
- deleteAllメソッドよりもdeleteAllInBatchメソッドを使用する
- jpqlのパフォーマンス
- org.springframework.data.domain.Pageクラスを使用したページング処理
- Union,Union Allサポートされてません
- No identifier specified for entity
- Not supported for DML operations
- column “xxx” is of type timestamp without time zone but expression is of type bytea
- 関連
JPQL入門(JPA)
JPAでDB(正確にはエンティティ)からデータを取得するSQLをJPQLと言います。
SQLでfrom句に書いていたのはテーブル名ですが、JPQLではfrom句にはエンティティを書きます。(ここが重要)
@QueryアノテーションでJPQLを記述しますが、通常のSQL構文と少しだけ構文が変わります。
以下は簡単なJPQLの例です。
@Query("select m from EmpMaster where m.empId = :id") @Query(value = "select m from EmpMaster wher m.empId = :id")
上記では、mがエイリアスです。select *
ではなくselect m
とします。value = は省略可能です。
JPQLで注意しないといけないのはテーブル名はエンティティクラス名を書く、カラム名はエンティティクラスのフィールド名を書く、という点です。
実際のテーブル名がemp_masterであってもそのテーブルに対応するエンティティクラスがEmpMasterクラスであれば、JPQL文ではEmpMasterと書きます。
UPDATE文やDELETE文を書く
JPQL文でUPDATE文やDELETE文を書くにはアノテーションがいくつか増えます。
アノテーション | 意味 |
---|---|
@Transactional | クラスorメソッドが異常終了すればロールバックされる |
@Modifying | update文,insert文,delete文につける |
コーディング例は以下の通りです。
@Modifying @Query("update ~")
LIKE検索をする
JPQLではLIKE検索ができます。バインド変数に%を付ける場合などは以下の通り記述します。
@Query(value = "select m EmpMaster from m.empId like :id%") List<EmpMaster> findBySample(@Param("id") Long aaa)
JPQLは色々制約がある
JPQLは色々と制約があるので、その場合はSQL文を書くしかありません。
SQLを記述する場合は、nativeQuery=trueとします。以下、例です。
@Query(nativeQuery=true, value="select ~")
参考サイト:JPQLでFROM句に副問い合わせが使えない
ちなみにMySQLの日付計算で使うinterval,DAY,MONTH,YEARなどもJPQLでは使えないのでnativeQuery=trueにする必要があります。
後述のFUNCTION式を使って回避することはできます。
in句を使用する
条件がin句のみならfindByXXInメソッドが使えますが、nativeQuery=true
でin句がある場合、Listをパラメータとして渡します。
@Repository public interface HogeRepository extends JpaRepository<HogeEntity, Integer> { @Query(value = "select * from hoge_tbl " + " where id in :id ", nativeQuery = true) List<HogeEntity> findById(@Param("id") List<Integer> id); }
エンティティ
エンティティクラスを作成しますが、決まりがあります。
- @Entityアノテーションを付与
- いずれかのフィールドに@Idを付与
- 引数なしコンストラクタを実装
@Tableアノテーションは必須ではなく、テーブル名≠エンティティ名の場合に@Tableアノテーションでテーブル名を設定します。
@Columnも必須ではなく、列名≠フィールド名の場合に@Columnを付与します。
主キー
エンティティの主キーには@Idアノテーションを追加します。
idフィールドが主キーの場合はエンティティクラスは以下のように@Idを指定します。
@Table(name="auto_increment") @Entity @Getter @Setter public class AutoIncrementtEntity { @Id @GeneratedValue private Integer id; @Column(name="name") private String name; }
主キーがサロゲートキーの場合、@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 |
@Table(name="auto_increment") @Entity @Getter @Setter public class AutoIncrementtEntity { @Id @GeneratedValue(GenerationType.IDENTITY) private Integer id; @Column(name="name") private String name; }
PostgreSQLの場合でSequenceオブジェクトを使用している場合は、@SequenceGeneratorのsequenceName属性でSequenceオブジェクト名を指定する必要があります。
/** ID. */ @Id @GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "id_seq") @SequenceGenerator(name = "id_seq",sequenceName = "id_seq",allocationSize = 1) @Column(name = "id") private Integer id;
エンティティに紐づかないSelect句の場合エラーとなる
JPQLが簡単なら良いのですが、例えば年月ごとに集計を求めるGroupなどを使う場合色々ややこしかったりします。
エンティティクラス
@Table @Entity @Getter @Setter public class Sample { @Id @Column("date") private LocalDate date; }
こんなエンティティがある場合にJPQLで年月日ではなく、年月をSelect句に入れるとエラーとなります。
@Query(value = "select data_format(date, '%Y-%m') from Sample")
これはLocalDateに対して年月にフォーマットした値になっているため、エラーとなります。
回避方法は新たにクラスを作成し、そのインスタンス生成時のコンストラクタに文字列として突っ込んであげます。
select new(コンストラクタ式)を使用する場合はnativeQueryは使えませんのでご注意ください。
@Query(value = "select NEW jp.co.confrage.xxx.domain.entity.SampleEntity" + " ( " + " date AS DT" + " ) " + " from Sample ")
SampleEntityはこんな感じで単なるクラスです。
@AllArgsConstructor @Data public class SampleEntity { private String DT; }
これでOKです。 コンストラクタ式に@Paramの引数(名前付きパラメータ)を入れたい場合はCASTしてあげる必要があります。
@ParamがString型であってもCASTでString型にCASTする必要があるので注意です。(MySQLで確認)
@Query(value = "SELECT NEW jp.co.confrage.xxx.domain.entity.SampleEntity" + " ( " + " CAST(:name as java.lang.String)" // こんな感じ + " ) " + " from Sample ") List<SampleEntity> findByXXXX(@Param("name") String name); // この引数をコンストラクタ式に入れる場合
nameはString型ですが、コンストラクタ式の引数で使用する場合はCASTしてあげる必要があります。
また完全修飾名で指定する必要があります。
コンストラクタ式を使わない場合
必ずコンストラクタ式を使わないといけないわけではありません。 例えば以下のようなメソッドも書くことができます。
@Query(value="select m.aaa.accountNumber,m.branchNumber from AccountEntity m where m.aaa.id = 1") List<Object[]> findXXX();
List<Object[]>にすればOKです。
DTOなどのクラスに以下のようなメソッドを作成してあげます。
import java.math.BigDecimal; import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Data; @AllArgsConstructor @Data public class HogeResource { private Integer id; private BigDecimal version; private LocalDateTime date; /** * こんなメソッド作ってあげる.リソースクラス生成. */ public static HogeResource toResource(Object[] obj) { return new HogeResource( (Integer)obj[0], (BigDecimal)obj[1], (LocalDateTime)obj[2]); } }
リポジトリのメソッド呼び出し側は以下のようにstream使えばきれいにコーディングができます。
List<SampleDto> obj = accountRepository.findXXX() .stream() .map(SampleDto::toResource) .collect(Collectors.toList());
これでコンストラクタ式を使わないでも記述することができます。
@EmbeddedId
主キーが一つであれば、@Idでよいのですが、複合主キーの場合は@IdClassや、@EmbeddedIdを使用します。
@IdClassを使うケースがありますが冗長になるので敬遠されがちのようです。
エンティティは以下のように定義します。
package jp.co.confrage; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @Entity @Table(name="employee_master") public class Employee { @EmbeddedId private PrimaryK id; // 複合主キーのクラス @Column(name="empname") private String empname; @Embeddable @Data @AllArgsConstructor @NoArgsConstructor public static class PrimaryK implements Serializable { // PrimaryKクラスをstaticで定義 private static final long serialVersionUID = -2523459362991270288L; @Column(name="id") private String id; // pk @Column(name="empno") private String empno; // pk } }
implements Serializableしないとエラーとなります。その為、この複合主キーに対して@Embeddableアノテーションを付加します。 リポジトリクラスは以下のように定義します。
package jp.co.confrage; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import jp.co.confrage.Employee.PrimaryK; @Repository public interface EmployeeRepository extends JpaRepository<Employee, PrimaryK> { // ここ注意 @Query(value="select m.id.empno from Employee m ") public List<Integer> findByPk(); }
複合主キーの場合は、@Queryの書き方に注意しないといけなくて、@Embeddedを使用する場合は、JPQLがm.id.empno
というようになります。
@Embeddableアノテーションの複合主キーで注意
便利ですが、コンストラクタ式としてこのキーをNEWするとエラーとなりましたので、コンストラクタ式でNEWしたい場合は@IdClassを利用します。
findByIdの引数に複合主キーを指定する
JpaRepositoryがデフォルトで用意しているfindByIdメソッドの引数に複合主キーを指定してみます。先ほど書いたエンティティを使用します。
サービスクラスからリポジトリをDIして呼び出します。
EmployeeRepository repository; empRepository.findById(new PrimaryK("1","1")); // こんな感じで複合主キーをnewする
メソッド名からJPQLを生成する
メソッド名に一定の決まりがあって、その名前からJPQLを生成することができます。 Employeeというエンティティがあったとして、リポジトリを以下にします。
public Employee findByName(String name);
findByNameというメソッドを作成すると以下のようになります。
from エンティティ where name = :name
条件が2つある場合はfindByNameAndEmpnoとします。
public Employee findByNameAndEmpno(String name,String empno);
このメソッドは以下を意味します。
from エンティティ where name = :name and empno = :empno;
以下はメソッド名に指定すると特別に意味を持ちます。
単語 | メソッド名 | 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を付けたフィールド名を付けないといけなくなります。
package jp.co.confrage; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Embeddable; import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @Entity @Table(name="employee_master") public class Employee { @EmbeddedId private PrimaryK id; @Column(name="empname") private String empname; @Embeddable @Data @AllArgsConstructor @NoArgsConstructor public static class PrimaryK implements Serializable{ private static final long serialVersionUID = -2523459362991270288L; @Column(name="id") private String id; @Column(name="empno") private String empno; } }
例えば、empnoで検索したい場合は
public Employee findByEmpno(String empno); // エラー public Employee findByIdEmpno(String empno); // 正解
となります。
nativeな関数を使う方法
JPQLに用意されていない関数や各RDBに依存する関数をJPQLで使用したい場合
SELECT FUNCTION('関数名', 引数1, 引数2゙…) ~
というようにします。
org.springframework.data.domain.Sortクラスでソートする
クエリー文のorder byでソートしなくてもorg.springframework.data.domain.Sortクラスを渡してソートすることが可能です。 org.springframework.data.domain.Sort.Directionというenumがあるので、ASCなら昇順、DESCなら降順です。第二引数はエンティティのフィールド名を指定します。
// 呼び出し側 List<AccountEntity> list = accountRepository.findList(new Sort(Direction.ASC, "depositAmount"));
以下、リポジトリクラスです。JPQLでソートせずにSortクラスのインスタンスを渡します。
@Query(value="select m from AccountEntity m") List<AccountEntity> findList(Sort sort);
@QueryHintsを使用して大量データをフェッチする
@QueryHintsアノテーションを使用するとフェッチサイズを調整することができます。 フェッチサイズは@QueryHintのvalue属性に文字列で指定します。 nameにはorg.hibernate.jpa.QueryHints.HINT_FETCH_SIZE
を指定します。以下はフェッチサイズを1000にした例です。
@QueryHints(value=@QueryHint(name = org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE, value="1000")) @Query(value = "select m.empname from UserEntity m ") public List<String> findByPk();
@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; } }
これだけでは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"); } }; } }
これで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; }
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
Sample.find=\ SELECT m FROM Employee
これで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ページ当たりのデータ単位 |
Page<エンティティ> paging = リポジトリ.findAll(PageRequest.of(0, 3));
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)); }
Union,Union Allサポートされてません
JPQLでUnion,Union Allはサポートされていません。nativeQuery=trueとした場合はSQL実行できますが、プログラム側でList.addするようにします。
No identifier specified for entity
エンティティに@Id(javax.persistence.Id)が指定されていない場合にこのエラーが発生します。
@Idはエンティティに一つは必須です。
Not supported for DML operations
更新系のSQLを発行した際に@Modifyingアノテーションをつけていない場合に「 Not supported for DML operations」エラーが発生します。
@Modifying @Query(value="update User m set m.name = 'takahashi'") public void updateBySample();
limit,offset PostgreSQLでimit,offsetなどが使用できますが、JPQLでは対応していない為、TypedQueryインタフェースのsetFirstResultメソッド、setMaxResultsメソッドで代用できます。
もしくはtop,firstキーワードを使用します。
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でアップデートされます。
KHI入社して退社。今はCONFRAGEで正社員です。関西で140-170/80~120万から受け付けております^^
得意技はJS(ES6),Java,AWSの大体のリソースです
コメントはやさしくお願いいたします^^
座右の銘は、「狭き門より入れ」「願わくは、我に七難八苦を与えたまえ」です^^
コメント