Couchbase 仓库
Spring Data 仓库抽象的目标是显著减少为各种持久化存储实现数据访问层所需的样板代码量。
默认情况下,如果操作是单文档操作且已知 ID,则由键值(Key/Value)存储提供支持。对于所有其他操作,默认会生成 N1QL 查询,因此必须创建适当的索引以实现高性能的数据访问。
请注意,您可以为查询调整所需的一致性级别(请参阅以一致性方式查询),并让不同的仓库由不同的桶提供支持(请参阅 [couchbase.repository.multibucket])。
配置
虽然对仓库的支持始终存在,但您仍需在整体上或针对特定命名空间启用它们。如果您扩展了 AbstractCouchbaseConfiguration,只需使用 @EnableCouchbaseRepositories 注解即可。它提供了许多可选参数来收窄或自定义搜索路径,其中最常用的一个是 basePackages。
另请注意,如果您运行在 Spring Boot 中,自动配置支持已经为您设置好了该注解,因此只有在您想覆盖默认值时才需要使用它。
示例 1. 基于注解的仓库设置
@Configuration
@EnableCouchbaseRepositories(basePackages = {"com.couchbase.example.repos"})
public class Config extends AbstractCouchbaseConfiguration {
//...
}
更高级的用法在 [couchbase.repository.multibucket] 中介绍。
QueryDSL 配置
Spring Data Couchbase 支持使用 QueryDSL 构建类型安全的查询。要启用代码生成,需要将 CouchbaseAnnotationProcessor 设置为注解处理器。此外,运行时还需要 querydsl-apt 以在仓库上启用 QueryDSL。
示例 2. Maven 配置示例
. existing depdendencies including those required for spring-data-couchbase
.
.
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydslVersion}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>annotation-processing</id>
<phase>generate-sources</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<proc>only</proc>
<annotationProcessors>
<annotationProcessor>org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor</annotationProcessor>
</annotationProcessors>
<generatedTestSourcesDirectory>target/generated-sources</generatedTestSourcesDirectory>
<compilerArgs>
<arg>-Aquerydsl.logInfo=true</arg>
</compilerArgs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
示例 3. Gradle 配置示例
dependencies {
annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}'
annotationProcessor 'org.springframework.data:spring-data-couchbase'
testAnnotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}'
testAnnotationProcessor 'org.springframework.data:spring-data-couchbase'
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs += [
"-processor",
"org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor"]
}
用法
在最简单的情况下,您的仓库将扩展 CrudRepository<T, String>,其中 T 是您想暴露的实体。让我们看一个针对 UserInfo 的仓库:
示例 4. 一个 UserInfo 仓库
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<UserInfo, String> {
}
请注意,这只是一个接口,而不是一个实际的类。在后台,当您的上下文初始化时,会为您的仓库描述创建实际的实现,您可以通过常规 Bean 访问它们。这意味着您将节省大量样板代码,同时仍向服务层和应用暴露完整的 CRUD 语义。
现在,假设我们将 UserRepository @Autowire 注入到一个使用它的类中。我们有哪些可用的方法呢?
表 1. UserRepository 上暴露的方法
| 方法 | 描述 |
|---|---|
| UserInfo save(UserInfo entity) | 保存给定的实体。 |
| Iterable<UserInfo> save(Iterable<UserInfo> entity) | 保存实体列表。 |
| UserInfo findOne(String id) | 根据唯一 ID 查找一个实体。 |
| boolean exists(String id) | 根据唯一 ID 检查给定实体是否存在。 |
| Iterable<UserInfo> findAll() | 查找桶中该类型的所有实体。 |
| Iterable<UserInfo> findAll(Iterable<String> ids) | 根据该类型和给定的 ID 列表查找所有实体。 |
| long count() | 统计桶中实体的数量。 |
| void delete(String id) | 根据 ID 删除实体。 |
| void delete(UserInfo entity) | 删除该实体。 |
| void delete(Iterable<UserInfo> entities) | 删除所有给定的实体。 |
| void deleteAll() | 删除桶中该类型的所有实体。 |
这太棒了!只需定义一个接口,我们就能在受管实体之上获得完整的 CRUD 功能。
虽然暴露的方法为您提供了丰富的访问模式,但您常常还需要定义自定义方法。您可以通过向接口添加方法声明来实现这一点,这些声明会在后台自动解析为请求,我们将在后续章节中看到。
仓库与查询
基于 N1QL 的查询
先决条件是在将存储实体的桶上创建一个 PRIMARY INDEX。
下面是一个示例:
示例 5. 带 N1QL 查询的扩展 UserInfo 仓库
public interface UserRepository extends CrudRepository<UserInfo, String> {
@Query("#{#n1ql.selectEntity} WHERE role = 'admin' AND #{#n1ql.filter}")
List<UserInfo> findAllAdmins();
List<UserInfo> findByFirstname(String fname);
}
这里我们看到两种由 N1QL 支持的查询方式。
第一种方法使用 Query 注解以内联方式提供一条 N1QL 语句。通过将 SpEL 表达式块包裹在 #{ 和 } 之间,即可支持 SpEL(Spring 表达式语言)。SpEL 还提供了一些特定于 N1QL 的值:
-
#n1ql.selectEntity可轻松确保该语句会选择构建完整实体所需的所有字段(包括文档 ID 和 CAS 值)。 -
WHERE 子句中的
#n1ql.filter会添加一个条件,使实体类型与 Spring Data 用于存储类型信息的字段相匹配。 -
#n1ql.bucket会被替换为存储该实体的桶的名称,并用反引号转义。 -
#n1ql.scope会被替换为存储该实体的作用域的名称,并用反引号转义。 -
#n1ql.collection会被替换为存储该实体的集合的名称,并用反引号转义。 -
#n1ql.fields会被替换为重构实体所需的字段列表(例如用于 SELECT 子句)。 -
#n1ql.delete会被替换为delete from语句。 -
#n1ql.returning会被替换为重构实体所需的 returning 子句。
我们建议您始终使用 selectEntity SpEL,并在 WHERE 子句中使用 filter SpEL(否则您的查询可能会受到来自其他仓库的实体的影响)。
基于字符串的查询支持参数化查询。您既可以使用像“$1”这样的位置占位符,此时每个方法参数会依次映射到 $1、$2、$3……;也可以使用“$someString”语法的命名占位符。方法参数会根据参数名与其对应的占位符相匹配,参数名可以通过用 @Param(例如 @Param("someString"))标注每个参数(Pageable 或 Sort 除外)来覆盖。您不能在查询中混用这两种方式,否则会得到一个 IllegalArgumentException。
请注意,您可以混用 N1QL 占位符和 SpEL。N1QL 占位符仍会考虑所有方法参数,因此请务必像下例那样使用正确的索引:
示例 6. 混用 SpEL 与 N1QL 占位符的内联查询
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND #{[0]} = $2")
public List<User> findUsersByDynamicCriteria(String criteriaField, Object criteriaValue)
这让您只需通过一个方法声明,就能生成类似于 AND name = "someName" 或 AND age = 3 这样的查询。
您还可以在 N1QL 查询中进行单值投影(前提是它只选择一个字段并只返回一个结果,通常是像 COUNT、AVG、MAX 这样的聚合)。此类投影会具有像 long、boolean 或 String 这样简单的返回类型。这不用于向 DTO 的投影。
另一个示例:
#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND test = $1
等价于
SELECT #{#n1ql.fields} FROM #{#n1ql.collection} WHERE #{#n1ql.filter} AND test = $1
第二种方法使用 Spring Data 的查询派生机制,根据方法名和参数构建一条 N1QL 查询。这会产生如下形式的查询:SELECT … FROM … WHERE firstName = "valueOfFnameAtRuntime"。您可以组合这些条件,甚至可以用像 countByFirstname 这样的名称做计数,或用像 findFirst3ByLastname 这样的名称做限制。
实际上,生成的 N1QL 查询还会包含一个额外的 N1QL 条件,以便只选择与该仓库实体类相匹配的文档。
大多数 Spring Data 关键词都受支持:@Query(N1QL)方法名内受支持的关键词。
| 关键词 | 示例 | N1QL WHERE 子句片段 |
|---|---|---|
And | findByLastnameAndFirstname | lastName = a AND firstName = b |
Or | findByLastnameOrFirstname | lastName = a OR firstName = b |
Is,Equals | findByField,findByFieldEquals | field = a |
IsNot,Not | findByFieldIsNot | field != a |
Between | findByFieldBetween | field BETWEEN a AND b |
IsLessThan,LessThan,IsBefore,Before | findByFieldIsLessThan,findByFieldBefore | field < a |
IsLessThanEqual,LessThanEqual | findByFieldIsLessThanEqual | field ⇐ a |
IsGreaterThan,GreaterThan,IsAfter,After | findByFieldIsGreaterThan,findByFieldAfter | field > a |
IsGreaterThanEqual,GreaterThanEqual | findByFieldGreaterThanEqual | field >= a |
IsNull | findByFieldIsNull | field IS NULL |
IsNotNull,NotNull | findByFieldIsNotNull | field IS NOT NULL |
IsLike,Like | findByFieldLike | field LIKE "a" - a 应为包含 % 和 _ 的 String(匹配 n 个和 1 个字符) |
IsNotLike,NotLike | findByFieldNotLike | field NOT LIKE "a" - a 应为包含 % 和 _ 的 String(匹配 n 个和 1 个字符) |
IsStartingWith,StartingWith,StartsWith | findByFieldStartingWith | field LIKE "a%" - a 应为 String 前缀 |
IsEndingWith,EndingWith,EndsWith | findByFieldEndingWith | field LIKE "%a" - a 应为 String 后缀 |
IsContaining,Containing,Contains | findByFieldContains | field LIKE "%a%" - a 应为一个 String |
IsNotContaining,NotContaining,NotContains | findByFieldNotContaining | field NOT LIKE "%a%" - a 应为一个 String |
IsIn,In | findByFieldIn | field IN array - 请注意下一个参数值(如果是集合/数组,则为其子元素)应能兼容地存储在 JsonArray 中) |
IsNotIn,NotIn | findByFieldNotIn | field NOT IN array - 请注意下一个参数值(如果是集合/数组,则为其子元素)应能兼容地存储在 JsonArray 中) |
IsTrue,True | findByFieldIsTrue | field = TRUE |
IsFalse,False | findByFieldFalse | field = FALSE |
MatchesRegex,Matches,Regex | findByFieldMatches | REGEXP_LIKE(field, "a") - 请注意此处会忽略 ignoreCase,a 是 String 形式的正则表达式 |
Exists | findByFieldExists | field IS NOT MISSING - 用于验证 JSON 是否包含此属性 |
OrderBy | findByFieldOrderByLastnameDesc | field = a ORDER BY lastname DESC |
IgnoreCase | findByFieldIgnoreCase | LOWER(field) = LOWER("a") - a 必须是一个 String |
使用这种方式,您既可以使用计数查询,也可以使用 [repositories.limit-query-result] 特性。
对于 N1QL,仓库的另一个可用接口是 PagingAndSortingRepository(它扩展了 CrudRepository)。它新增了两个方法:
表 2. PagingAndSortingRepository 上暴露的方法
| 方法 | 描述 |
|---|---|
| Iterable<T> findAll(Sort sort); | 允许在按实体的某个属性排序的同时检索所有相关实体。 |
| Page<T> findAll(Pageable pageable); | 允许分页检索您的实体。返回的 Page 可让您轻松获取下一页的 Pageable 以及条目列表。首次调用时,请使用 new PageRequest(0, pageSize) 作为 Pageable。 |
对于由 N1QL 支持的仓库,您也可以使用 Page 和 Slice 作为方法的返回类型。
如果将 pageable 和 sort 参数与内联查询一起使用,内联查询本身不应包含任何 order by、limit 或 offset 子句,否则服务器会因查询格式错误而拒绝它。
自动索引管理
默认情况下,应由用户为其查询创建并管理最优的索引。尤其是在开发的早期阶段,自动创建索引以便快速上手会非常方便。
对于 N1QL,本库提供了以下注解,它们需要附加到实体上(加在类上或字段上):
-
@QueryIndexed:加在字段上,表示该字段应作为索引的一部分。 -
@CompositeQueryIndex:加在类上,表示应创建一个基于多个字段的(复合)索引。 -
@CompositeQueryIndexes:如果需要创建多个CompositeQueryIndex,此注解接收它们的列表。
例如,下面就是在实体上定义复合索引的方式:
示例 7. 带排序的双字段复合索引
@Document
@CompositeQueryIndex(fields = {"id", "name desc"})
public class Airline {
@Id
String id;
@QueryIndexed
String name;
@PersistenceConstructor
public Airline(String id, String name) {
this.id = id;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}
默认情况下,索引创建是禁用的。如果您想启用它,需要在配置中覆盖它:
示例 8. 启用自动索引创建
@Override
protected boolean autoIndexCreation() {
return true;
}
以一致性方式查询
默认情况下,使用 N1QL 的仓库查询采用 NOT_BOUNDED 扫描一致性。这意味着结果返回很快,但索引中的数据可能尚未包含先前写入操作的数据(称为最终一致性)。如果您的查询需要“读到自己刚写入的数据”的语义,则需要使用 @ScanConsistency 注解。下面是一个示例:
示例 9. 使用不同的扫描一致性
@Repository
public interface AirportRepository extends PagingAndSortingRepository<Airport, String> {
@Override
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Iterable<Airport> findAll();
}
DTO 投影
Spring Data 仓库在使用查询方法时通常返回领域模型。然而,有时您可能因各种原因需要改变该模型的视图。在本节中,您将学习如何定义投影,以提供资源的简化视图和缩减视图。
请看以下领域模型:
@Entity
public class Person {
@Id @GeneratedValue
private Long id;
private String firstName, lastName;
@OneToOne
private Address address;
…
}
@Entity
public class Address {
@Id @GeneratedValue
private Long id;
private String street, state, country;
…
}
这个 Person 有以下几个属性:
-
id是主键 -
firstName和lastName是数据属性 -
address是指向另一个领域对象的链接
现在假设我们如下创建一个对应的仓库:
interface PersonRepository extends CrudRepository<Person, Long> {
Person findPersonByFirstName(String firstName);
}
Spring Data 会返回包含其所有属性的领域对象。要仅检索 address 属性,有两种选择。一种选择是像这样为 Address 对象定义一个仓库:
interface AddressRepository extends CrudRepository<Address, Long> {}
在这种情况下,使用 PersonRepository 仍会返回整个 Person 对象。而使用 AddressRepository 只会返回 Address。
然而,如果您根本不想暴露 address 的细节怎么办?您可以通过定义一个或多个投影,为仓库服务的使用者提供另一种选择。
示例 10. 简单投影
interface NoAddresses { 1
String getFirstName(); 2
String getLastName(); 3
}
这个投影具有以下细节:
一个普通的 Java 接口,使其具有声明式。
导出
firstName。导出
lastName。
NoAddresses 投影只有 firstName 和 lastName 的 getter,这意味着它不会提供任何地址信息。在这种情况下,查询方法定义返回 NoAdresses 而非 Person。
interface PersonRepository extends CrudRepository<Person, Long> {
NoAddresses findByFirstName(String firstName);
}
投影在底层类型与所暴露属性相关的方法签名之间声明了一份契约。因此,必须按照底层类型的属性名来命名 getter 方法。如果底层属性名为 firstName,那么 getter 方法必须命名为 getFirstName,否则 Spring Data 将无法查找到源属性。