Spring Data JPA で遊んでみる 〜その9〜

昨日のSpecificationの続きでSpecificationを複数組み合わせて使う際に便利な、Specificationsというヘルパークラスが用意されている。

public class Specifications<T> implements Specification<T> {
  private final Specification<T> spec;
  private Specifications(Specification<T> spec) {...}
  public static <T> Specifications<T> where(Specification<T> spec) {...}
  public Specifications<T> and(final Specification<T> other) {...}
  public Specifications<T> or(final Specification<T> other) {...}
  public static <T> Specifications<T> not(final Specification<T> spec) {...}
  public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {...}
}

幾つかメソッドが定義されていますがwhereを基点としてandとかorとかでつなげていく感じ。

例えば2つSpecがあるとして

public class EmpSpecifications {
    public static Specification<Emp> idLessThanOrEqualTo(final Long id) {
        return new Specification<Emp>() {
            @Override
            public Predicate toPredicate(Root<Emp> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.lessThanOrEqualTo(root.get(Emp_.id), id);
            }
        };
    }

    public static Specification<Emp> hasDept(final Dept dept) {
        return new Specification<Emp>() {
            @Override
            public Predicate toPredicate(Root<Emp> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.equal(root.get(Emp_.dept), dept);
            }
        };
    }
}

これを組み合わせて使う場合は

List<Emp> emps = repository.findAll(where(idLessThanOrEqualTo(9L)).and(hasDept(Dept.of(1L))));

とかいうふうに使えます。

サンプルこのへん。
https://github.com/yamkazu/springdata-jpa-example/tree/complexspec

Spring Data JPA で遊んでみる 〜その10〜

Spring Data JPA では Querydsl なるものがサポートされています。
http://www.querydsl.com/

色々機能があるみたいですが、カンタに言うとEntityクラスからAPTでメタ情報のクラスを生成して、そのクラスを利用してタイプセーフで流れるようにクエリが書けるようなものです。s2jdbcチックな感じです。

APTなので若干セットアップがかったるいですが。
まずpomにライブラリの追加。

    <!-- Query Dsl -->
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-jpa</artifactId>
      <version>2.2.3</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-apt</artifactId>
      <version>2.2.3</version>
      <scope>provided</scope>
    </dependency>

mavenでaptを実行するようにしておきます。

      <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>maven-apt-plugin</artifactId>
        <version>1.0</version>
        <executions>
          <execution>
            <phase>generate-sources</phase>
            <goals>
              <goal>process</goal>
            </goals>
            <configuration>
              <outputDirectory>target/generated-sources</outputDirectory>
              <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
          </execution>
        </executions>
      </plugin>

ついでにソースを追加するあれをいれておくとよい。

      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
        <executions>
          <execution>
            <id>add-source</id>
            <phase>compile</phase>
            <goals>
              <goal>add-source</goal>
            </goals>
            <configuration>
              <sources>
                <source>target/generated-sources</source>
              </sources>
            </configuration>
          </execution>
        </executions>
      </plugin>

最近はm2eの仕組みが色々と変わってEclipseで連携する場合は色々とめんどい。。。ので書きません。

良い感じにソースコードが自動生成されるようになったらリポジトリにQueryDslPredicateExecutorを継承させます。

public interface EmpRepository extends QueryDslPredicateExecutor<Emp>

QueryDslPredicateExecutorに定義されているのは以下のメソッド

T findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
Page<T> findAll(Predicate predicate, Pageable pageable);
long count(Predicate predicate);

使うときは自動生成されたメタクラスを使用しつつクエリを組み立てて食わせます。

@Test
public void Querydslを使う() throws Exception {
    Iterable<Emp> emps = repository.findAll(QEmp.emp.id.lt(9L).and(QEmp.emp.dept.id.eq(1L)));
}

サンプルはこんなん
https://github.com/yamkazu/springdata-jpa-example/tree/querydsl

Spring Data JPA で遊んでみる 〜その8〜

Specificationの話です。

SpecificationはDDDのパターンの一つですが、JPA2から導入されたCriteriaを利用して、Spring Data JPAではSpecificationパターンみたいなことが出来ます。

Specificationを使用するにはリポジトリの定義でJpaSpecificationExecutorを継承する必要があります。

public interface EmpRepository extends JpaRepository<Emp, Long>, JpaSpecificationExecutor<Emp> {
//..

JpaSpecificationExecutorに定義されているメソッドは以下のようなもの。

T findOne(Specification<T> spec);
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
List<T> findAll(Specification<T> spec, Sort sort);
long count(Specification<T> spec);

要はSpecificationを生成して引数として渡します。中身はCriteriaで条件を指定していきます。
こんなん

    public class Specifications {
        public static Specification<Emp> idLessThanOrEqualTo(final Long id) {
            return new Specification<Emp>() {
                @Override
                public Predicate toPredicate(Root<Emp> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                    return cb.lessThanOrEqualTo(root.get(Emp_.id), id);
                }
            };
        }
    }

使うときは

    @Test
    public void Specificationを使ってみる() throws Exception {
        List<Emp> emps = repository.findAll(idLessThanOrEqualTo(4L));
        assertThat(emps.size(), is(not(equalTo(0))));
    }

こんな感じになるわけです。

なかなかよいかも。

サンプルはこのへん。
https://github.com/yamkazu/springdata-jpa-example/tree/specification

Spring Data JPA で遊んでみる 〜その6〜

次はメソッドの定義です。Spring Data JPA はメソッドからクエリを自動生成しますが、そのメソッド名には命名規則があります。

まず戻り値はRepositryに指定している総称型のエンティティのListか、もしくは、そのエンティティにします。Listの場合はJPAのgetResultListが、エンティティの場合はgetSingleResultが呼ばれるという感じですかね?中身見てないのでわかりませんが。定義はこんなんです。

    Emp findByName(String name);
    List<Emp> findByDept(Dept dept);

次にメソッド名のprefixは、findBy、readBy、getByが使用出来ます。単にfind、read、getとか使えるような記述がマニュアルに書いてあったのですが、エラーになっちゃいました。ようわからん。次のメソッドはすべて同じ意味です。

List<Emp> findByDept(Dept dept);
List<Emp> readByDept(Dept dept);
List<Emp> getByDept(Dept dept);
List<Emp> dept(Dept dept);

よくわからんのですがプレフィクスはなくてもいいみたいです。いろいろ使えるみたいですが、findByが個人的には一般的な気がするので、他のはわりとどうでもいいやという。

プレフィックに続いてエンティティのプロパティを指定してい行きます。その指定に合わせて、引数でそのプロパティを受け取るようにします。複数の条件を組み合わせる場合はAnd、Orで組み合わせられます。その他にもプロパティの後にキーワードを設定することで条件を設定できます(これはJPA限定みたいですけど)。

マニュアルそのままですが、いろいろあります。

Keyword Sample JPQL snippet
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Between findByStartDateBetween … where x.startDate between 1? and ?2
LessThan findByAgeLessThan … where x.age < ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection age) … where x.age not in ?1

例えばこんな感じで定義できます。

List<Emp> findByDeptAndIdGreaterThan(Dept dept, Long id);

基本はメソッド名で指定したプロパティを引数に取るようにしますが、特別な引数があります。それが、PageableとSortです。

    Page<Emp> findByDeptAndIdGreaterThan(Dept dept, Long id, Pageable pageable);
    List<Emp> findByDeptAndIdGreaterThan(Dept dept, Long id, Sort sort);

Pageableを使用する場合は戻り値の型にPageを指定できますが、Listでも問題ありません。Pageableを使用する場合は必ずentityManagerFactoryのBeanの定義の中にjpaVendorAdapterを定義しないと動きません(ちょっとはまった)。

  <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
    <property name="persistenceUnitName" value="persistenceUnit" />
    <property name="dataSource" ref="dataSource" />
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
    </property>
  </bean>

あとProperty expressionsというのもあります。これは関連エンティティをたぐってプロパティを指定するような場合に使用出来ます。例えば

List<Emp> findByDeptNameLike(String name);

これはx.dept.nameのプロパティを条件にしています。引数もこの場合はDeptの型ではなく、その関連をたぐったプロパティの型になります。DeptのnameはStringなので、ここではStringを指定しています。

場合によっては、このProperty expressionsはうまく行かない時があります。プロパティの名前がかぶっているようなときです。そんなときはアンスコをプロパティの区切りに指定できます。

List<Emp> findByDept_NameLike(String name);

これは先程の定義とまったく同じ意味です。

サンプルはこのへん。
https://github.com/yamkazu/springdata-jpa-example/tree/querycreation

だんだんテストとか適当になって来ましたが。。。

Spring Data JPA で遊んでみる 〜その7〜

名前付きクエリの話。JPAはもともとこの名前付きクエリをサポートしています。エンティティにつけるアレです。

@Entity
@NamedQuery(name = "Emp.findByUseNamedQuery", query = "select e from Emp e where e.id > ?1")
public class Emp {
// ..

これをSpring Data JPAリポジトリで利用するには、nameの指定にルールがあり、ドメインクラス名(エンティティ).メソッド名となるようにします。

この定義したクエリをリポジトリのメソッドで使うには以下のように、nameで指定したメソッド名で定義します。

List<Emp> findByUseNamedQuery(Long id);

これでfindByUseNamedQueryを実行すると、定義されているクエリが使用されます。

これ以外にもSpring Data JPA独自の @Query というアノテーションを使用出来ます。通常JPAでの名前付きクエリはエンティティクラス、またはXMLに定義します(XMLとかあまり使わないけど)が、@Query は リポジトリのメソッドに付与してクエリを定義できるものです。

@Query("select e from Emp e where e.id > ?1")
List<Emp> findByUseQueryAnnotation(Long id);

上記の様に、クエリ内の変数は、メソッドの引数の順番で処理することができますが、この方法はリファクタリング等、変更に非常に弱いので、名前付きでの指定も可能になっています。

こんなん

@Query("select e from Emp e where e.id > :id")
List<Emp> findByUseQueryAnnotationWithParam(@Param("id") Long id);

個々まで見てきたのでは、selectのみですが、updateもできます。

@Query("update Emp e set e.name = :name where e.id = :id")
@Modifying
//    @Modifying(clearAutomatically = false)
int setName(@Param("id") Long id, @Param("name") String name);

メソッドの戻り値定義と@Modifyingが必要です。@Modifyingが指定されているとEntityManager#clearが自動的に呼びされますが、呼び出したくない場合はclearAutomatically = falseを指定してください。

@Modifyingが正直よくわからなくてJPAのexecuteUpdate()になるやつで必要なのかなぁーと思ったのですが

@Query("delete from Emp e where e.id = :id")
int deleteById(@Param("id") Long id);

deleteは指定しなくても動きました。中身は覗いていない。

サンプルはこのへん。
https://github.com/yamkazu/springdata-jpa-example/tree/namedquery

Spring Data JPA で遊んでみる 〜その5〜

query-lookup-strategyについて。
Spring Data は リポジトリに定義されたメソッドから自動的にクエリを生成することができますが、その戦略です。設定は3つあります。

  • CREATE
  • USE_DECLARED_QUERY
  • CREATE_IF_NOT_FOUND

CREATEはメソッドからクエリを生成します。USE_DECLARED_QUERYはユーザ定義されたクエリを使用します。これは見つからなかったらエラーになります。

CREATE_IF_NOT_FOUNDはこの両方を合わせたやつで、もしユーザ定義のクエリが見つからない場合は、メソッドからクエリを生成します。これがdefaultです。

あんまり、CREATE、USE_DECLARED_QUERYを明示的に指定したいシチュエーションが今のところあまり思いつかないので、特に弄ることもないのかなと思います。

設定はrepositoryの定義をする所で出来ます。例えば

<jpa:repositories base-package="org.yamkazu.springdata" query-lookup-strategy="create-if-not-found"/>

defaultはcreate-if-not-foundなので、create-if-not-foundでいい場合は、明示的に書く必要はありません。

Spring Data JPA で遊んでみる 〜その4〜

こんどは

public interface PagingAndSortingRepository<T, ID extends Serializable> extends
		CrudRepository<T, ID> {
    // ...
    Page<T> findAll(Pageable pageable);
}

のPageableを使ってみる。なんとなく何するものかわかりますが、ぺーじゃーてきなあれです。Pageableの実装クラスは今のところPageRequestだけです。これをnewして使います。コンストラクタはこんなん。

    public PageRequest(int page, int size) 
    public PageRequest(int page, int size, Direction direction,
										 String... properties) 
    public PageRequest(int page, int size, Sort sort) 

page番号と、pageに含める要素数、ソートの設定をします。Sortはまえ紹介したやつです。

基本的な使い方はこんなん。

Page<Emp> page = repository.findAll(new PageRequest(3, 3));

戻りはPage型です。ここから色々とページの情報がとれます。

 List<Emp> emps = page.getContent(); // 取得した要素をListで取得
 int number = page.getNumber(); // ページ番号を取得
 int numberOfElements = page.getNumberOfElements(); // 取得したページの要素数
 int size = page.getSize(); // 指定したページの要素数
 long totalElements = page.getTotalElements(); // 全件の数を取得
 int totalPages = page.getTotalPages(); // ページ数を取得

ソートを指定する場合は、前回のようにSortを使って食わせるだけです。

        // ソートを指定する
        page = repository.findAll(new PageRequest(0, 4, ASC, "dept.id", "name"));
        assertThat(page.getSize(), is(4));

        // OrderBySourceを使用してソートを指定する
        page = repository.findAll(new PageRequest(0, 5, new OrderBySource("NameDesc").toSort()));
        assertThat(page.getSize(), is(5));

サンプルはこのへん
https://github.com/yamkazu/springdata-jpa-example/tree/pageable