跳到主要内容

Couchbase 仓库

ChatGPT-4o-mini 中英对照 Couchbase repositories

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 {
//...
}
java

高级用法在 [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>
xml

示例 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"]
}
groovy

用法

在最简单的情况下,您的仓库将扩展 CrudRepository<T, String>,其中 T 是您想要暴露的实体。让我们来看一个用于 UserInfo 的仓库:

示例 4. 一个 UserInfo 仓库

import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<UserInfo, String> {
}
java

请注意,这只是一个接口,而不是一个实际的类。在后台,当您的上下文初始化时,您的仓库描述的实际实现会被创建,您可以通过常规的 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:

  1. 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.
  2. 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.
  3. 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
}
java

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);
}
java

在这里,我们看到两种基于 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 将被替换为重建实体所需的返回子句。

important

我们建议您始终使用 selectEntity SpEL 和带有 filter SpEL 的 WHERE 子句(因为否则您的查询可能会受到其他仓库中的实体的影响)。

字符串基础的查询支持参数化查询。您可以使用位置占位符,如$1,在这种情况下,每个方法参数将按顺序映射到 $1$2$3…… 另外,您可以使用$someString语法来使用命名占位符。方法参数将通过参数的名称与其对应的占位符匹配,参数名称可以通过在每个参数(除了 PageableSort)上使用 @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)
java

这允许你生成类似于 AND name = "someName"AND age = 3 的查询,使用单个方法声明。

你也可以在 N1QL 查询中进行单一投影(前提是只选择一个字段并且返回一个结果,通常是像 COUNTAVGMAX 这样的聚合)。这样的投影将具有简单的返回类型,如 longbooleanString。这不是用于将数据投影到 DTO 的。

另一个例子:
#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND test = $1
相当于
SELECT #{#n1ql.fields} FROM #{#n1ql.collection} WHERE #{#n1ql.filter} AND test = $1

SpEL 与 Spring Security 的实际应用

当你想根据由其他 Spring 组件(如 Spring Security)注入的数据执行查询时,SpEL(Spring 表达式语言)非常有用。以下是你需要做的事情,以扩展 SpEL 上下文以访问这些外部数据。

首先,你需要实现一个 EvaluationContextExtension(使用如下支持类):

class SecurityEvaluationContextExtension extends EvaluationContextExtensionSupport {

@Override
public String getExtensionId() {
return "security";
}

@Override
public SecurityExpressionRoot getRootObject() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return new SecurityExpressionRoot(authentication) {};
}
}
java

然后,你只需要在配置中声明一个相应的 Bean,以便 Spring Data Couchbase 能够访问关联的 SpEL 值:

@Bean
EvaluationContextExtension securityExtension() {
return new SecurityEvaluationContextExtension();
}
java

这在根据连接用户的角色构造查询时非常有用,例如:

@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND " +
"role = '?#{hasRole('ROLE_ADMIN') ? 'public_admin' : 'admin'}'")
List<UserInfo> findAllAdmins(); //只有 ROLE_ADMIN 用户会看到隐藏的管理员
java

删除查询示例:

@Query("#{HERE #{#n1ql.filter} AND " +
"username = $1 #{#n1ql.returning}")
UserInfo removeUser(String username);
java

第二种方法使用 Spring-Data 的查询推导机制,通过方法名和参数构建一个 N1QL 查询。这将生成一个类似于以下的查询:SELECT …​ FROM …​ WHERE firstName = "valueOfFnameAtRuntime"。你可以结合这些条件,甚至使用类似 countByFirstname 的名称进行计数,或者使用类似 findFirst3ByLastname 的名称进行限制查询……​

备注

实际上,生成的 N1QL 查询还将包含一个额外的 N1QL 条件,以便只选择与仓库的实体类匹配的文档。

大多数 Spring-Data 关键字都受到支持:在 @Query (N1QL) 方法名称中支持的关键字

关键词示例N1QL WHERE 子句片段
AndfindByLastnameAndFirstnamelastName = a AND firstName = b
OrfindByLastnameOrFirstnamelastName = a OR firstName = b
Is,EqualsfindByField,findByFieldEqualsfield = a
IsNot,NotfindByFieldIsNotfield != a
BetweenfindByFieldBetweenfield BETWEEN a AND b
IsLessThan,LessThan,IsBefore,BeforefindByFieldIsLessThan,findByFieldBeforefield < a
IsLessThanEqual,LessThanEqualfindByFieldIsLessThanEqualfield ⇐ a
IsGreaterThan,GreaterThan,IsAfter,AfterfindByFieldIsGreaterThan,findByFieldAfterfield > a
IsGreaterThanEqual,GreaterThanEqualfindByFieldGreaterThanEqualfield >= a
IsNullfindByFieldIsNullfield IS NULL
IsNotNull,NotNullfindByFieldIsNotNullfield IS NOT NULL
IsLike,LikefindByFieldLikefield LIKE "a" - a 应该是一个包含 % 和 _(匹配 n 和 1 个字符)的字符串
IsNotLike,NotLikefindByFieldNotLikefield NOT LIKE "a" - a 应该是一个包含 % 和 _(匹配 n 和 1 个字符)的字符串
IsStartingWith,StartingWith,StartsWithfindByFieldStartingWithfield LIKE "a%" - a 应该是一个字符串前缀
IsEndingWith,EndingWith,EndsWithfindByFieldEndingWithfield LIKE "%a" - a 应该是一个字符串后缀
IsContaining,Containing,ContainsfindByFieldContainsfield LIKE "%a%" - a 应该是一个字符串
IsNotContaining,NotContaining,NotContainsfindByFieldNotContainingfield NOT LIKE "%a%" - a 应该是一个字符串
IsIn,InfindByFieldInfield IN array - 注意,下一参数值(如果是集合/数组,则其子项)应该能够存储在 JsonArray
IsNotIn,NotInfindByFieldNotInfield NOT IN array - 注意,下一参数值(如果是集合/数组,则其子项)应该能够存储在 JsonArray
IsTrue,TruefindByFieldIsTruefield = TRUE
IsFalse,FalsefindByFieldFalsefield = FALSE
MatchesRegex,Matches,RegexfindByFieldMatchesREGEXP_LIKE(field, "a") - 注意,这里的 ignoreCase 会被忽略,a 是一个正则表达式的字符串形式
ExistsfindByFieldExistsfield IS NOT MISSING - 用于验证 JSON 中是否包含此属性
OrderByfindByFieldOrderByLastnameDescfield = a ORDER BY lastname DESC
IgnoreCasefindByFieldIgnoreCaseLOWER(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。
提示

你也可以使用 PageSlice 作为方法返回类型,前提是使用 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;
}

}
java

默认情况下,索引创建是禁用的。如果你想启用它,你需要在配置中覆盖它:

示例 8. 启用自动索引创建

@Override
protected boolean autoIndexCreation() {
return true;
}
java

一致性查询

默认情况下,使用 N1QL 的仓库查询使用 NOT_BOUNDED 扫描一致性。这意味着查询结果返回速度较快,但索引中的数据可能尚未包含之前写入操作的数据(称为最终一致性)。如果您需要查询的“已准备好您的写入”语义,则需要使用 @ScanConsistency 注解。以下是一个示例:

示例 9. 使用不同的扫描一致性

@Repository
public interface AirportRepository extends PagingAndSortingRepository<Airport, String> {

@Override
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
Iterable<Airport> findAll();

}
java

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;


}
java

这个 Person 有多个属性:

  • id 是主键

  • firstNamelastName 是数据属性

  • address 是指向另一个领域对象的链接

现在假设我们创建一个相应的仓库,如下所示:

interface PersonRepository extends CrudRepository<Person, Long> {

Person findPersonByFirstName(String firstName);
}
java
// ... 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 ...
java

在这个代码块中,我添加了一个 AddressRepository 接口,它扩展了 JpaRepository,并定义了一个查询方法 findAddressById,用于仅检索 address 属性。

interface AddressRepository extends CrudRepository<Address, Long> {}
java

在这种情况下,使用 PersonRepository 仍然会返回整个 Person 对象。使用 AddressRepository 仅会返回 Address

然而,如果您根本不想暴露 address 细节怎么办?您可以通过定义一个或多个投影,为您的存储库服务的消费者提供替代方案。

示例 10. 简单投影

interface NoAddresses {  1

String getFirstName(); 2

String getLastName(); 3
}
java

该投影具有以下细节:

  • 一个简单的 Java 接口,使其具有声明性。

  • 导出 firstName

  • 导出 lastName

NoAddresses 投影仅具有 firstNamelastName 的 getter,这意味着它不会提供任何地址信息。查询方法定义在这种情况下返回 NoAddresses 而不是 Person

interface PersonRepository extends CrudRepository<Person, Long> {

NoAddresses findByFirstName(String firstName);
}
java

投影声明了基础类型与与暴露的属性相关的方法签名之间的契约。因此,必须根据基础类型的属性名称命名 getter 方法。如果基础属性命名为 firstName,则 getter 方法必须命名为 getFirstName,否则 Spring Data 将无法查找源属性。