在使用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方法,然后复制过来,删掉不需要的即可。
interface PersonRepository extends Repository {
Collection findByLastname(String lastname);
}
这样我们就可以查询到部分字段了。
除了前面提到的声明一个带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
}
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动态调整呢?我们可以使用泛型来实现这个功能。
interface PersonRepository extends Repository {
Collection findByLastname(String lastname, Class type);
}
然后再使用的时候就可以根据类型来返回自己所需要的字段:
void someMethod(PersonRepository people) {
Collection aggregates =
people.findByLastname("Matthews", Person.class);
Collection aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}