Spring Data JPA 3.5でSpecification.whereが非推奨になった件

/**
 * Simple static factory method to add some syntactic sugar around a {@link Specification}.
 *
 * @apiNote with 4.0, this method will no longer accept {@literal null} specifications.
 * @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
 * @param spec can be {@literal null}.
 * @return guaranteed to be not {@literal null}.
 * @since 2.0
 * @deprecated since 3.5, to be removed with 4.0 as we no longer want to support {@literal null} specifications.
 */
@Deprecated(since = "3.5.0", forRemoval = true)
static <T> Specification<T> where(@Nullable Specification<T> spec) {
    return spec == null ? (root, query, builder) -> null : spec;
}

非推奨になったけれども、まだ代替手段はないようだ。Nullを許容しなくなることを警告するためのものらしい。

The where method merely caters for the broken nullability allowance and is replaced by where(PredicateSpecification) in 4.0.

まだ確定ではないかもしれないが、mainでは下記の様に修正されている。

/**
 * Simple static factory method to add some syntactic sugar translating {@link PredicateSpecification} to
 * {@link Specification}.
 *
 * @param <T> the type of the {@link Root} the resulting {@literal Specification} operates on.
 * @param spec the {@link PredicateSpecification} to wrap.
 * @return guaranteed to be not {@literal null}.
 */
static <T> Specification<T> where(PredicateSpecification<T> spec) {

    Assert.notNull(spec, "PredicateSpecification must not be null");

    return (root, update, criteriaBuilder) -> spec.toPredicate(root, criteriaBuilder);
}

@Deprecatedを付けるのはまだ早かったのではないだろうか。(同じようにNullを許容しなくなる他のメソッドにはついていないし)

Hibernate ORMとHibernate Searchで検索結果をソートする

備忘録として、いくつかの実装例を記載しておく。

Hibernate ORMのソート(Spring Data JPAを使用する)

Spring Bootを使用していなくても、下記のように依存関係を読み込むことでSpring Data JPAを使用できる。

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>${spring-data.version}</version>
</dependency>

クエリメソッドを使用する

ルールに沿ったメソッド名を使用することにより、自動的にHibernateへSQLが発行される。

public interface CrudRepository<T, ID> extends Repository<T, ID> {
    // 登録、または更新
    <S extends T> S save(S entity);      
    // プライマリーキーで検索
    Optional<T> findById(ID primaryKey); 
    // 全検索
    Iterable<T> findAll();               
    // 全件数
    long count();                        
    // 削除
    void delete(T entity);               
    // 存在チェック
    boolean existsById(ID primaryKey); 

例えば、小説データを全検索して、小説のタイトルでソートをかけたい場合、下記のように書く。複数のソート条件を指定する場合は続けて記載する。

@Repository
public interface NovelRepository extends JpaRepository<Novel, Long>, JpaSpecificationExecutor<Novel> {
    List<Novel> findAllByOrderByTitle();
}

※「findAll」の後に余分な「By」があるのは、ルール的にそこに検索条件が入ることになるため、今回は全検索なので「By」だけが残っている。

条件 APIを使用する

criteria を記述することにより、クエリの where 句を定義している場合、ソートは以下のように定義する。複数のソート条件を指定する場合は引数を追加する。

novelRepository.findAll(spec, JpaSort.by("title"))
@Repository
public interface NovelRepository extends JpaSpecificationExecutor<Novel> {
}

Hibernate Searchのソート

ソートの処理実装前に、エンティティに@KeywordFieldを追記する必要がある。

    /** タイトル */
    @FullTextField(analyzer = "japanese")
    @KeywordField(name = "titleSort", sortable = Sortable.YES)
    private String title;

検索時に上記で定義した名称をソート条件に指定する。

import org.hibernate.search.mapper.orm.Search;

@Service
public class NovelService {
    private final EntityManager entityManager;

    @Transactional
    public List<Novel> findAllIndex() {
        return Search.session(entityManager)
                .search(Novel.class)
                .where(f -> f.matchAll())
                .sort(f -> f.field("titleSort"))
                .fetchAllHits();
    }

ソートの条件を指定していないときは、スコアの高い順にソートされている。大雑把に言えば、スコアが高いほど、より多くの述語にマッチしているか、より良くマッチしていることを意味する。そのため、Hibernate Searchに関しては明示的にソートの条件を指定しない方が、良いのかもしれない。