Couchbase 仓库
Spring Data 仓库抽象的目标是显著减少实现各种持久化存储的数据访问层所需的样板代码量。
默认情况下,如果是单文档操作并且已知 ID,则操作由键/值存储支持。对于所有其他操作,默认会生成 N1QL 查询,因此必须创建适当的索引以实现高效的数据访问。
请注意,您可以调整查询所需的一致性(请参见 Querying with consistency),并且可以有不同的存储库由不同的桶支持(请参见 [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 语义暴露给您的服务层和应用程序。
To understand the methods available when you @Autowired
the UserRepository
in a Spring application, we first need to consider what UserRepository
typically extends. In a typical Spring Data JPA setup, UserRepository
would extend JpaRepository
or CrudRepository
.
Here are the common methods you would have available:
-
Basic CRUD Operations:
save(S entity)
: Saves a given entity.findById(ID id)
: Retrieves an entity by its id.findAll()
: Retrieves all entities.deleteById(ID id)
: Deletes the entity with the given id.delete(S entity)
: Deletes a given entity.
-
Additional Methods (depending on the repository interface):
count()
: Returns the number of entities.existsById(ID id)
: Checks if an entity with the given id exists.
-
Custom Query Methods: If you define custom query methods in your
UserRepository
, you can also call those. For example:findByUsername(String username)
: If you define a method to find a user by their username.
Here’s a simple example of how you might use UserRepository
in a service class:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
public void saveUser(User user) {
userRepository.save(user);
}
// ... other methods using userRepository
}
In this example, UserService
uses the UserRepository
to perform operations on User
entities.
表 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 功能。
虽然暴露的方法为您提供了多种访问模式,但您通常需要定义自定义模式。您可以通过向接口添加方法声明来实现这一点,这些声明将在后台自动解析为请求,正如我们将在接下来的章节中看到的那样。
Repositories 和查询
基于 N1QL 的查询
前提是必须在存储实体的桶上创建一个 PRIMARY INDEX。
这是一个例子:
示例 5. 一个扩展的 UserInfo 存储库,带有 N1QL 查询
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(Spring 表达式语言)通过将 SpEL 表达式块放在 #{
和 }
之间来支持。通过 SpEL 提供了一些 N1QL 特定的值:
-
#n1ql.selectEntity
允许确保语句将选择构建完整实体所需的所有字段(包括文档 ID 和 CAS 值)。 -
#n1ql.filter
在 WHERE 子句中添加与 Spring Data 用于存储类型信息的字段匹配的条件。 -
#n1ql.bucket
将被替换为实体存储的桶的名称,并用反引号转义。 -
#n1ql.scope
将被替换为实体存储的作用域的名称,并用反引号转义。 -
#n1ql.collection
将被替换为实体存储的集合的名称,并用反引号转义。 -
#n1ql.fields
将被替换为重建实体所需的字段列表(例如,SELECT 子句中的字段)。 -
#n1ql.delete
将被替换为delete from
语句。 -
#n1ql.returning
将被替换为重建实体所需的返回子句。
我们建议您始终使用 selectEntity
SpEL 和带有 filter
SpEL 的 WHERE 子句(因为否则您的查询可能会受到其他仓库中的实体的影响)。
字符串基础的查询支持参数化查询。您可以使用位置占位符,如$1
,在这种情况下,每个方法参数将按顺序映射到 $1
、$2
、$3
…… 另外,您可以使用$someString
语法来使用命名占位符。方法参数将通过参数的名称与其对应的占位符匹配,参数名称可以通过在每个参数(除了 Pageable
或 Sort
)上使用 @Param
注解来覆盖(例如,@Param("someString")
)。在查询中不能混合这两种方法,如果这样做,将会抛出 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 应该是一个包含 % 和 _(匹配 n 和 1 个字符)的字符串 |
IsNotLike,NotLike | findByFieldNotLike | field NOT LIKE "a" - a 应该是一个包含 % 和 _(匹配 n 和 1 个字符)的字符串 |
IsStartingWith,StartingWith,StartsWith | findByFieldStartingWith | field LIKE "a%" - a 应该是一个字符串前缀 |
IsEndingWith,EndingWith,EndsWith | findByFieldEndingWith | field LIKE "%a" - a 应该是一个字符串后缀 |
IsContaining,Containing,Contains | findByFieldContains | field LIKE "%a%" - a 应该是一个字符串 |
IsNotContaining,NotContaining,NotContains | findByFieldNotContaining | field NOT LIKE "%a%" - a 应该是一个字符串 |
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 是一个正则表达式的字符串形式 |
Exists | findByFieldExists | field IS NOT MISSING - 用于验证 JSON 中是否包含此属性 |
OrderBy | findByFieldOrderByLastnameDesc | field = a ORDER BY lastname DESC |
IgnoreCase | findByFieldIgnoreCase | LOWER(field) = LOWER("a") - a 必须是一个字符串 |
您可以使用计数查询和 [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。 |
你也可以使用 Page
和 Slice
作为方法返回类型,前提是使用 N1QL 支持的存储库。
如果在内联查询中使用了 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 Repositories 通常在使用查询方法时返回领域模型。然而,有时出于各种原因,您可能需要更改该模型的视图。在本节中,您将学习如何定义投影,以提供简化和减少的资源视图。
请提供您想要我查看的领域模型的详细信息或代码,我将帮助您分析或修改它。
@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);
}
// ... existing code ...
public interface AddressRepository extends JpaRepository<Address, Long> {
// Option to retrieve only the address attribute
@Query("SELECT a FROM Address a WHERE a.id = :id")
Address findAddressById(@Param("id") Long id);
}
// ... existing code ...
在这个代码块中,我添加了一个 AddressRepository
接口,它扩展了 JpaRepository
,并定义了一个查询方法 findAddressById
,用于仅检索 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,这意味着它不会提供任何地址信息。查询方法定义在这种情况下返回 NoAddresses
而不是 Person
。
interface PersonRepository extends CrudRepository<Person, Long> {
NoAddresses findByFirstName(String firstName);
}
投影声明了基础类型与与暴露的属性相关的方法签名之间的契约。因此,必须根据基础类型的属性名称命名 getter 方法。如果基础属性命名为 firstName
,则 getter 方法必须命名为 getFirstName
,否则 Spring Data 将无法查找源属性。