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

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

beta版の時に少し遊んだのですが、気がつけば 1.0.1.RELEASE なんてのが出ててたので改めて触って見ました。

Spring Data は Springのアプリケーションで簡単にPersistanceの機能を作るためのアレで、Spring Data JPAはそのJPAバージョンです。この他にもNoSQLの様々なプロダクトがあります。ちまたで噂の Java 9 のクラウド対応とかこういう感じのものになるのかなぁとかも妄想したり。あと、Spring Roo 1.2でもこのSpring Data JPAとの連携が新しく追加されています。

Spring Data JPA はインタフェースでリポジトリを定義します。メソッドの命名規則があったり、インタフェースで定義するあたりはs2daoに非常に似ています。

エンティティクラスは当前ですが、JPAで定義します。

@Entity
public class Emp {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    // getter setter ...
}

これに対するリポジトリのインタフェースはこんなん。

public interface EmpRepository extends JpaRepository<Emp, Long> {
    Emp findByName(String name);
}

継承しているJpaRepositoryはJPA操作の基本となるCRUDのメソッドが定義されています。それに拡張する形でインタフェースにメソッドを定義します。ここれはnameをキーにした検索メソッドを定義しています。メソッドの命名規則についてはまた今度書きます。

あとはこのリポジトリをBeanとして定義します。

 <jpa:repositories base-package="org.yamkazu.springdata" />

これでおしまい。

使うときはBeanを取得して、リポジトリを操作できます。

    @Inject
    EmpRepository repository;

    @Test
    public void はじめてのSpringDataJpa() {
        Emp emp = new Emp();
        emp.setName("HomuHomu");
        repository.save(emp);

        Emp e = repository.findByName("HomuHomu");
        assertThat(e, is(notNullValue()));
        assertThat(e.getName(), is(equalTo("HomuHomu")));
    }

このへんにコードをおいておきます。
https://github.com/yamkazu/springdata-jpa-example/tree/quickstart

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

その2の続きです。

前回あった

List<T> findAll(Sort sort);

というSortという面白そうなのがあるので、使ってみます。基本は、Sortをnewして使うのですが、Orderクラスと絡ませたり、Stringでプロパティで食わせたりと、幾つかアプローチがあるので、掻い摘んで紹介します。

まず基本。

    @Test
    public void findAll_初めてのソート() throws Exception {
        // 一番シンプルなやり方
        List<Emp> emps = repository.findAll(new Sort(ASC, "name"));
        assertThat(emps.size(), is(not(equalTo(0))));
        for (int i = 0; i < emps.size() - 1; i++) {
            if (emps.get(i).getName().compareTo(emps.get(i + 1).getName()) > 0) {
                fail();
            }
        }

        // 上の指定と同じ
        emps = repository.findAll(new Sort(new Order(ASC, "name")));
        assertThat(emps.size(), is(not(equalTo(0))));
    }

基本は ASC or DESC どちらかを指定して ソートで使用するプロパティを文字列で指定します。OrderBySourceというASC or DESCの指定、複数のプロパティの組み合わせを、ひとつのStringで定義できるというやつもいます。

    @Test
    public void findAll_sort_OrderBySourceを使う() throws Exception {
        List<Emp> emps = repository.findAll(new OrderBySource("NameAsc").toSort());
        assertThat(emps.size(), is(not(equalTo(0))));
    }

関連クラスのプロパティを指定といったことも可能ですし、複数のキーを設定するということも出来ます。

    @Test
    public void findAll_sort_関連クラスのプロパティを指定() throws Exception {
        List<Emp> emps = repository.findAll(new Sort(new Order(DESC, "dept.id"), new Order(ASC, "name")));
        assertThat(emps.size(), is(not(equalTo(0))));

        // OrderBySourceを使用した例、上と同じ意味
        emps = repository.findAll(new OrderBySource("DeptIdDescNameAsc", Emp.class).toSort());
        assertThat(emps.size(), is(not(equalTo(0))));
    }

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