Projection

一般来讲,我们进行查询都会返回 Entity 类型本身,例如:

class Person {
    @Id UUID id;
    String firstname, lastname;
    Address address;

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

interface PersonRepository extends Repository<Person, UUID> {
    Collection<Person> findByLastname(String lastname);
}

但是,有时候我们可能只喜欢获取某些属性,例如我们只希望获得用户的名字,不关心用户的地址。这种情况下,我们就可以创建一个投影:

interface NamesOnly {
    String getFirstname();
    String getLastname();
}
interface PersonRepository extends Repository<Person, UUID> {
    Collection<NamesOnly> findByLastname(String lastname);
}

投影是可以递归使用的。如果你想包含一个 Address 投影也是可以的,如下例:

interface PersonSummary {
    String getFirstname();
    String getLastname();
    AddressSummary getAddress();

    interface AddressSummary {
        String getCity();
    }
}

闭合投影:如果一个投影接口,其所有 getter 方法都与实体类的属性相匹配,那么它被称为一个闭合投影。

之前看到的 NameOnly 接口就是一个闭合投影。

If you use a closed projection, Spring Data can optimize the query execution, because we know about all the attributes that are needed to back the projection proxy. For more details on that, see the module-specific part of the reference documentation.

开放投影:投影接口中存在 getter 方法不与实体类的任何属性匹配,但存在 @Value 注解计算属性。

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

The aggregate root backing the projection is available in the target variable. A projection interface using @Value is an open projection. Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root.

@Value 中使用的表达式不应该太复杂(你应该避免在字符串变量中编程)。 对于非常简单的表达式,一种选择可能是采用默认方法(在 Java 8 中引入),如以下示例所示:

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

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

这种方法要求您能够纯粹基于投影接口上公开的其他 getter 方法来实现逻辑。 第二个更灵活的选择是在一个 Spring Bean 中实现自定义逻辑,然后从 SpEL 表达式调用它,如以下示例所示:

@Component
class MyBean {
    String getFullName(Person person) {
        // logic here
    }
}

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

使用 SpEL,你也可以传参:

interface NamesOnly {
    @Value("#{args[0] + ' ' + target.firstname + '!'}")
    String getSalutation(String prefix);
}

但是,记住,如果逻辑复杂,请使用一个单独的 Spring Bean 去实现其逻辑。

使用 Nullable Wrapper 来保证 null-safety:

interface NamesOnly {
    Optional<String> getFirstname();
}

如果查询结果为 null,使用 java.util.Optional 容器可以保证返回的结果不是 null,而是一个 empty 的 Optional 对象。

动态投影

interface PersonRepository extends Repository<Person, UUID> {
    <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

使用泛型以实现动态投影后,我们就不需要繁琐的编写多次 findByLastname 来获得不同类型的返回值了。

void someMethod(PersonRepository people) {
    // 调用变得非常方便
    Collection<Person> aggregates =
        people.findByLastname("Matthews", Person.class);

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

最后更新于