Spring Data JPA进阶(二):Projections

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

在使用Spring Data JPA的时候,我们可能需要查询某个实体的部分字段。比如我的个人网站,文章内容其实是存在数据库里面的,这个字段很大。但是我们在展示文章列表的时候,其实是没必要把文章内容查出来的,只需要查询其它字段。

在上篇自定义实现中,示例代码里展示了如何查询某个实体的单个字段:

@Query("select u.name from User u where u.id=?1")
String getNameFromId(Long id);

那假如有查询多个字段的需求呢?可以用以下方法:

首先,根据自己需要查询的字段创建一个相应的构造方法:

public User(String name, int age) {
    this.name = name;
    this.age = age;
}

然后,在自定义的接口上面这样写Query:

@Query("select new User(u.name, u.age) from User u where u.id=?1")
User getNameAndAgeFromId(Long id);

在查询的字段较少时,这样做很方便。但是如果遇到我上面提到的那种需求,只有一两个字段不查,其它的都要查。那再使用这种方式就会让查询语句变得非常长,不利于阅读。

Spring Data JPA提供了Projections功能来做这个事情。具体怎么操作呢?

查询部分字段
  • 创建一个接口
    这个接口发挥了一个类似“视图”的作用。比如我们的实体定义是这个样子的:

class Person {

@Id UUID id;
String firstname, lastname;
Address address;

static class Address {
  String zipCode, city, street;
}

}

如果我们不想查询Address字段,那我们可以创建这样一个接口,这个接口里面应该有你需要的所有属性的get方法:

interface NamesOnly {
  String getFirstname();
  String getLastname();
}

Tips: 不用自己去写get方法,可以先在实体里面生成get方法,然后复制过来,删掉不需要的即可。

  • 将这个接口作为返回值
    然后就可以在自定义的Repository里面使用这个接口类型作为返回值。
interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

这样我们就可以查询到部分字段了。

使用DTO

除了前面提到的声明一个带get方法的接口以外,你还可以使用一个DTO类来做这个事情:

class NamesOnly {

  private final String firstname, lastname;

  NamesOnly(String firstname, String lastname) {

    this.firstname = firstname;
    this.lastname = lastname;
  }

  String getFirstname() {
    return this.firstname;
  }

  String getLastname() {
    return this.lastname;
  }

  // equals(…) and hashCode() implementations
}
  • 使用Lombok来简化代码
    Lombok提供了对DTO的注解@Value。注意这个注解跟Spring的@Value注解不是同一个,自己引包的时候要注意一下。上面一段代码简化之后是这个样子:
import lombok.Value;

@Value
class NamesOnly {
	String firstname, lastname;
}

看起来比使用接口更简洁!

字段重组

除了查询部分字段以外,Projections还可以对你的字段进行重组。比如:

import org.springframework.beans.factory.annotation.Value;

interface NamesOnly {

  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();
  …
}

注意这个@Value注解是用的Spring的注解,跟上面的Lombok的@Value注解不是同一个,引包的时候需要注意。

这里使用的是SpEL的语法,target即原始类型的对象。在Java 8以后的版本,除了使用@Value注解,你还可以使用接口的default方法:

interface NamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname.concat(" ").concat(getLastname());
  }
}

前面提到了使用SpEL,你可以利用SpEL做更多更炫酷的事情,比如使用一个Bean的方法:

@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();
  …
}

再比如:

interface NamesOnly {

  @Value("#{args[0] + ' ' + target.firstname + '!'}")
  String getSalutation(String prefix);
}
动态的Projection

可不可以有很多Projection都用同一个方法,但是返回值根据Projection动态调整呢?我们可以使用泛型来实现这个功能。

interface PersonRepository extends Repository<Person, UUID> {

  <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

然后再使用的时候就可以根据类型来返回自己所需要的字段:

void someMethod(PersonRepository people) {

  Collection<Person> aggregates =
    people.findByLastname("Matthews", Person.class);

  Collection<NamesOnly> aggregates =
    people.findByLastname("Matthews", NamesOnly.class);
}

相关文章