Spring Data JPA进阶(四):Example查询

x33g5p2x  于2021-12-19 转载在 其他  
字(3.2k)|赞(0)|评价(0)|浏览(617)

Example查询翻译过来叫“按例查询(QBE)”。是一种用户界面友好的查询技术。 它允许动态创建查询,并且不需要编写包含字段名称的查询。 而且按示例查询不需要使用特定的数据库的查询语言来编写查询语句。

官方文档有一个优劣势的说明:

优势:

  1. 可以使用动态或者静态的限制去查询
  2. 在重构你的实体的时候,不用担心影响到已有的查询
  3. 可以独立地工作在数据查询API之外

劣势:

  1. 不支持组合查询,比如:firstname = ?0 or (firstname = ?1 and lastname = ?2).
  2. 只支持字符串的starts/contains/ends/regex匹配,对于非字符串的属性,只支持精确匹配。换句话说,并不支持大于、小于、between等匹配。

简单来说,Example查询具有更好的可读性。但是笔者使用了一下,觉得功能太弱,不如Specification查询。就是使用很简单,但功能很弱。

使用实体构造Example

默认是所有字段都精确匹配。

@Test
void couldGetUser_givenExampleUser() {

    entityManager.persist(User.builder()
            .username("example_user")
            .name("Bob")
            .locked(true)
            .age(5)
            .build());

    User user = User.builder().name("Bob").build();
    Optional<User> userOptional = userRepository.findOne(Example.of(user));

    assertThat(userOptional.isPresent()).isTrue();
    userOptional.ifPresent(x -> assertThat(x.getName()).isEqualTo("Bob"));
}

就是自己先创建一个实体,给它一些属性。然后把它传入Example.of()方法,就可以构造一个Example对象。然后就可以直接查询了。

这里我们的Repository是继承的JpaRepository,它继承了QueryByExampleExecutor,这个方法里定义了一些用于Example查询的基本方法:

public interface QueryByExampleExecutor<T> {
    <S extends T> Optional<S> findOne(Example<S> example);
    <S extends T> Iterable<S> findAll(Example<S> example);
    <S extends T> Iterable<S> findAll(Example<S> example, Sort sort);
    <S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
    <S extends T> long count(Example<S> example);
    <S extends T> boolean exists(Example<S> example);
}
字符串的匹配

刚刚说了,对字符串以外的字段只支持精确匹配,字符串默认也是精确匹配。但对字符串还支持其他的匹配方式:

@Test
void couldGetUser_givenExampleWithProbAndMatcher() {
    Address address = entityManager.persist(Address.builder()
                                            .detail("xxx Street")
                                            .build());
    entityManager.persist(User.builder()
                          .username("example_user")
                          .name("Bob")
                          .address(address)
                          .build());

    Address exampleAddress = Address.builder().detail("Street").build();
    User exampleUser = User.builder()
        .name("B")
        .username("Example_User")
        .address(exampleAddress)
        .build();
    ExampleMatcher matcher = ExampleMatcher.matching()
        .withMatcher("name", m -> m.startsWith())
        .withMatcher("address.detail", m -> m.endsWith())
        .withMatcher("username", m -> m.ignoreCase());

    Optional<User> userOptional = userRepository.findOne(Example.of(exampleUser, matcher));

    assertThat(userOptional.isPresent()).isTrue();
    userOptional.ifPresent(x -> assertThat(x.getName()).isEqualTo("Bob"));
}

这里注意Address是User里面的一个OneToOne的属性,在使用Matcher的时候,可以使用“address.detail”这种方式来指定内联属性。

原理

从生成的SQL语句我们可以看到,它的判断条件是根据实体的属性来生成查询语句的。如果实体的属性是null,它就会忽略它;如果不是,就会取其值作为匹配条件。

在第一个例子中,输出的sql:

select
    user0_.id as id1_2_,
    user0_.address_id as address_7_2_,
    user0_.age as age2_2_,
    user0_.locked as locked3_2_,
    user0_.name as name4_2_,
    user0_.password as password5_2_,
    user0_.username as username6_2_ 
from
    user user0_ 
where
    user0_.name=?

这里有一个坑,

如果一个字段是不是包装类型,而是原始类型,它也会参与where条件中,其值是默认值。

我们现在的User实体中,locked是Boolean类型,age是Integer类型。如果我们把它们改成boolean和int,就会输出下面的sql:

select
    user0_.id as id1_2_,
    user0_.address_id as address_7_2_,
    user0_.age as age2_2_,
    user0_.locked as locked3_2_,
    user0_.name as name4_2_,
    user0_.password as password5_2_,
    user0_.username as username6_2_ 
from
    user user0_ 
where
    user0_.age=0 
    and user0_.name=? 
    and user0_.locked=?

这就会导致查询失败。所以我们在定义实体的时候,最好是使用包装类型。

相关文章