JPQL入門(JPA)

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;
}
https://gist.github.com/takahashi-h5/907c808af2a58c78060e4f933df24743

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キーワードを使用します。

Spring Data JPA - Reference Documentation

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でアップデートされます。

コメント

タイトルとURLをコピーしました