原标题:Spring认证|Spring Data JPA 参考文档五(内容来源:Spring中国教育管理中心)
从 Spring Data JPA 版本 1.4 开始,我们支持在使用@Query. 在运行查询时,这些表达式会根据一组预定义的变量进行评估。Spring Data JPA 支持一个名为entityName. 它的用法是select x from #{#entityName} x。它插入entityName与给定存储库关联的域类型的 。该entityName解决如下:如果域类型已设置的name属性@Entity的注释,它被使用。否则,将使用域类型的简单类名。
以下示例演示#{#entityName}了查询字符串中表达式的一个用例,您希望使用查询方法和手动定义的查询定义存储库接口:
示例 67.在存储库查询方法中使用 SpEL 表达式 - entityName
@Entity
public class User {
@Id
@GeneratedValue
Long id;
String lastname;
}
public interface UserRepository extends JpaRepository<User,Long> {
@Query("select u from #{#entityName} u where u.lastname = ?1")
List<User> findByLastname(String lastname);
}
为了避免在@Query注释的查询字符串中声明实际的实体名称,您可以使用该#{#entityName}变量。
该entityName可以通过使用定制@Entity的注释。orm.xmlSpEL 表达式不支持自定义。
当然,您可以User直接在查询声明中使用,但这也需要您更改查询。对 的引用将#entityName未来可能的User类重新映射到不同的实体名称(例如,通过使用@Entity(name = "MyUser").
#{#entityName}查询字符串中表达式的另一个用例是,如果您想为具体域类型定义具有专用存储库接口的通用存储库接口。为避免在具体接口上重复自定义查询方法的定义,可以@Query在通用存储库接口的注解的查询字符串中使用实体名称表达式,如下例所示:
示例 68.在存储库查询方法中使用 SpEL 表达式 - entityName 与继承
@MappedSuperclass
public abstract class AbstractMappedType {
…
String attribute
}
@Entity
public class ConcreteType extends AbstractMappedType { … }
@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>
extends Repository<T, Long> {
@Query("select t from #{#entityName} t where t.attribute = ?1")
List<T> findAllByAttribute(String attribute);
}
public interface ConcreteRepository
extends MappedTypeRepository<ConcreteType> { … }
在前面的示例中,该MappedTypeRepository接口是一些扩展AbstractMappedType. 它还定义了通用findAllByAttribute(…)方法,可用于专用存储库接口的实例。如果您现在调用findByAllAttribute(…)on ConcreteRepository,查询将变为select t from ConcreteType t where t.attribute = ?1。
操作参数的 SpEL 表达式也可用于操作方法参数。在这些 SpEL 表达式中,实体名称不可用,但参数可用。可以通过名称或索引访问它们,如下例所示。
示例 69.在存储库查询方法中使用 SpEL 表达式 - 访问参数。
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
对于like-conditions,人们通常希望附加%到字符串值参数的开头或结尾。这可以通过附加或前缀绑定参数标记或 SpEL 表达式来完成%。下面的例子再次证明了这一点。
示例 70.在存储库查询方法中使用 SpEL 表达式 - 通配符快捷方式。
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
当like对来自不安全来源的值使用-conditions 时,应该对值进行清理,这样它们就不能包含任何通配符,从而允许攻击者选择比他们应该能够选择的更多的数据。为此,该escape(String)方法在 SpEL 上下文中可用。它将第一个参数中的_和 的所有实例%加上第二个参数中的单个字符作为前缀。结合JPQL 和标准 SQL 中可用escape的like表达式子句,这可以轻松清理绑定参数。
示例 71.在存储库查询方法中使用 SpEL 表达式 - 清理输入值。
@Query("select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
List<User> findContainingEscaped(String namePart);
鉴于存储库接口中的此方法声明findContainingEscaped("Peter_")将找到Peter_Parker但不是Peter Parker. 所使用的转义字符可以通过设置来配置escapeCharacter所述的@EnableJpaRepositories注释。请注意,escape(String)SpEL 上下文中可用的方法只会转义 SQL 和 JPQL 标准通配符_和%. 如果底层数据库或 JPA 实现支持额外的通配符,这些将不会被转义。
修改查询
前面的所有部分都描述了如何声明查询以访问给定的实体或实体集合。您可以使用“ Spring Data Repositories 的自定义实现”中描述的自定义方法工具添加自定义修改行为。由于这种方法对于全面的自定义功能是可行的,您可以通过使用 注释查询方法来修改只需要参数绑定的查询@Modifying,如以下示例所示:
示例 72. 声明操作查询
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
这样做会触发注释到方法的查询作为更新查询而不是选择查询。作为EntityManager可能的修改查询执行后包含过时的实体,我们不会自动清除它(见的JavaDoc的EntityManager.clear()详细内容),因为这有效地删除所有非刷新的变化仍悬而未决的EntityManager。如果您希望EntityManager自动清除 ,可以将@Modifying注释的clearAutomatically属性设置为true。
该@Modifying注释是唯一与组合相关的@Query注释。派生查询方法或自定义方法不需要此注释。
派生删除查询
Spring Data JPA 还支持派生删除查询,让您不必显式声明 JPQL 查询,如以下示例所示:
示例 73.使用派生的删除查询
interface UserRepository extends Repository<User, Long> {
void deleteByRoleId(long roleId);
@Modifying
@Query("delete from User u where u.role.id = ?1")
void deleteInBulkByRoleId(long roleId);
}
尽管该deleteByRoleId(…)方法看起来基本上产生与 相同的结果deleteInBulkByRoleId(…),但在运行方式方面,这两个方法声明之间存在重要差异。顾名思义,后一种方法针对数据库发出单个 JPQL 查询(在注释中定义的查询)。这意味着即使当前加载的实例User也看不到调用的生命周期回调。
为了确保生命周期查询被实际调用,调用deleteByRoleId(…)运行一个查询,然后一个一个删除返回的实例,这样持久化提供者就可以实际调用@PreRemove这些实体的回调。
事实上,派生删除查询是运行查询,然后调用快捷CrudRepository.delete(Iterable<User> users)的结果,并保持行为的同步与其他的实现delete(…)的方法CrudRepository。
应用查询提示
要将 JPA 查询提示应用于存储库接口中声明的查询,您可以使用@QueryHints注释。它需要一组 JPA@QueryHint注释加上一个布尔标志来潜在地禁用应用于应用分页时触发的附加计数查询的提示,如以下示例所示:
示例 74. 将 QueryHints 与存储库方法一起使用
public interface UserRepository extends Repository<User, Long> {
@QueryHints(value = { @QueryHint(name = "name", value = "value")},
forCounting = false)
Page<User> findByLastname(String lastname, Pageable pageable);
}
前面的声明将应用@QueryHint为该实际查询配置的配置,但省略将其应用于触发以计算总页数的计数查询。
配置 Fetch- 和 LoadGraphs
JPA 2.1 规范引入了对指定 Fetch- 和 LoadGraphs 的支持,我们也支持@EntityGraph注释,它允许您引用@NamedEntityGraph定义。您可以在实体上使用该注释来配置结果查询的获取计划。获取的类型(Fetch或Load)可以通过使用注释type上的属性进行配置@EntityGraph。请参阅 JPA 2.1 规范 3.7.4 以获取进一步参考。
以下示例显示如何在实体上定义命名实体图:
示例 75. 在实体上定义命名实体图。
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {
// default fetch mode is lazy.
@ManyToMany
List<GroupMember> members = new ArrayList<GroupMember>();
…
}
以下示例显示如何在存储库查询方法上引用命名实体图:
示例 76.在存储库查询方法上引用命名实体图定义。
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
GroupInfo getByGroupName(String name);
}
也可以使用 来定义临时实体图@EntityGraph。提供的attributePaths被翻译成对应EntityGraph而无需显式添加@NamedEntityGraph到您的域类型,如以下示例所示:
示例 77.在存储库查询方法上使用 AD-HOC 实体图定义。
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(attributePaths = { "members" })
GroupInfo getByGroupName(String name);
}
预测
Spring Data 查询方法通常返回存储库管理的聚合根的一个或多个实例。但是,有时可能需要根据这些类型的某些属性创建投影。Spring Data 允许对专用返回类型进行建模,以更有选择地检索托管聚合的部分视图。
想象一个存储库和聚合根类型,例如以下示例:
示例 78. 示例聚合和存储库
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);
}
现在假设我们只想检索此人的姓名属性。Spring Data 提供什么方法来实现这一目标?本章的其余部分回答了这个问题。
基于界面的投影
将查询结果限制为仅名称属性的最简单方法是声明一个接口,该接口公开要读取的属性的访问器方法,如以下示例所示:
示例 79. 用于检索属性子集的投影接口
interface NamesOnly {
String getFirstname();
String getLastname();
}
这里的重要一点是这里定义的属性与聚合根中的属性完全匹配。这样做可以添加一个查询方法,如下所示:
示例 80. 使用基于接口的投影和查询方法的存储库
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
查询执行引擎在运行时为每个返回的元素创建该接口的代理实例,并将对公开方法的调用转发到目标对象。
可以递归地使用投影。如果您还想包含一些Address信息,请为其创建一个投影接口,并从 的声明中返回该接口getAddress(),如下例所示:
示例 81. 用于检索属性子集的投影接口
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
在方法调用时,address获取目标实例的属性并依次包装到投影代理中。
封闭式投影
其访问器方法都与目标聚合的属性匹配的投影接口被认为是封闭投影。以下示例(我们在本章前面也使用过)是一个封闭投影:
示例 82. 闭合投影
interface NamesOnly {
String getFirstname();
String getLastname();
}
如果使用封闭投影,Spring Data 可以优化查询执行,因为我们知道支持投影代理所需的所有属性。有关更多详细信息,请参阅参考文档中特定于模块的部分。
打开投影
投影接口中的访问器方法也可用于通过使用@Value注释计算新值,如以下示例所示:
例 83. 一个开放的投影
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
支持投影的聚合根在target变量中可用。使用的投影界面@Value是开放式投影。在这种情况下,Spring Data 无法应用查询执行优化,因为 SpEL 表达式可以使用聚合根的任何属性。
中使用的表达式@Value不应太复杂——您希望避免在String变量中编程。对于非常简单的表达式,一种选择可能是采用默认方法(在 Java 8 中引入),如以下示例所示:
示例 84. 使用自定义逻辑的默认方法的投影界面
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}
这种方法要求您能够纯粹基于投影接口上公开的其他访问器方法来实现逻辑。第二个更灵活的选择是在 Spring bean 中实现自定义逻辑,然后从 SpEL 表达式调用它,如以下示例所示:
示例 85. 示例 Person 对象
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
请注意 SpEL 表达式如何引用myBean和调用该getFullName(…)方法并将投影目标作为方法参数转发。由 SpEL 表达式评估支持的方法也可以使用方法参数,然后可以从表达式中引用这些参数。方法参数可通过Object名为的数组获得args。以下示例显示了如何从args数组中获取方法参数:
示例 86. 示例 Person 对象
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
同样,对于更复杂的表达式,您应该使用 Spring bean 并让表达式调用一个方法,如前所述。
可空包装器
投影接口中的 getter 可以使用可为空的包装器来提高空安全性。当前支持的包装器类型有:
java.util.Optional
com.google.common.base.Optional
scala.Option
io.vavr.control.Option
示例 87. 使用可为空包装器的投影接口
interface NamesOnly {
Optional<String> getFirstname();
}
如果基础投影值不是null,则使用包装器类型的当前表示返回值。如果支持值是null,则 getter 方法返回所用包装器类型的空表示。
基于类的预测 (DTO)
定义投影的另一种方法是使用值类型 DTO(数据传输对象),这些 DTO 包含应该检索的字段的属性。这些 DTO 类型可以以与使用投影接口完全相同的方式使用,除了不发生代理并且不可以应用嵌套投影。
如果存储通过限制要加载的字段来优化查询执行,则要加载的字段由公开的构造函数的参数名称确定。
以下示例显示了一个投影 DTO:
例 88. 一个投影 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
}
避免投影 DTO 的样板代码
您可以使用Project Lombok显着简化 DTO 的代码,它提供了一个@Value注解(不要与@Value前面的接口示例中显示的Spring 的注解混淆)。如果使用 Project Lombok 的@Value注解,之前显示的示例 DTO 将变为以下内容:
@Value
class NamesOnly {
String firstname, lastname;
}
字段是private final默认的,该类公开了一个构造函数,该构造函数接受所有字段并自动获取equals(…)和hashCode()实现方法。
基于类的投影不适用于本机查询。作为一种解决方法,您可以使用命名查询ResultSetMapping或 Hibernate 特定的ResultTransformer
动态投影
到目前为止,我们已经使用投影类型作为集合的返回类型或元素类型。但是,您可能希望选择在调用时使用的类型(这使其成为动态的)。要应用动态投影,请使用如下例所示的查询方法:
示例 89. 使用动态投影参数的存储库
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
这样,该方法可用于按原样或应用投影获取聚合,如以下示例所示:
示例 90.使用具有动态投影的存储库
void someMethod(PersonRepository people) {
Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}