Couchbase repositories

Spring Data 存储库抽象的目标是显著减少针对各种持久性存储实现数据访问层所需的样板代码量。 默认情况下,如果操作是单文档操作并且 ID 已知,则操作由键/值支持。对于其他所有操作,默认情况下会生成 N1QL 查询,因此必须创建适当的索引以实现高性能数据访问。 请注意,你可以调整你查询所需的相容性(请参阅Querying with consistency),并且可以使用不同的存储区支持不同的存储库(请参阅[couchbase.repository.multibucket])。

Configuration

虽然始终提供对存储库的支持,但您需要在通用情况下或对特定命名空间启用它们。如果您扩展 AbstractCouchbaseConfiguration,只需使用 @EnableCouchbaseRepositories 注释。它提供了许多可能的选项来缩小或自定义搜索路径,最常见的一个选项是 basePackages

还要注意,如果您在 Spring 启动程序内部运行,则自动配置支持已为您设置了注释,因此您只需在需要覆盖默认值时使用它。

Example 1. Annotation-Based Repository Setup
@Configuration
@EnableCouchbaseRepositories(basePackages = {"com.couchbase.example.repos"})
public class Config extends AbstractCouchbaseConfiguration {
    //...
}

高级用法在 [couchbase.repository.multibucket] 中有所描述。

QueryDSL Configuration

Spring Data Couchbase 支持 QueryDSL 来构建类型安全的查询。要启用代码生成,您需要在项目中将 spring-data-couchbase 设置为注释处理器。

Example 2. Maven Configuration Example
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>[compiler-plugin-version]</version>
            <configuration>
                <annotationProcessorPaths>
                    <!-- path to the annotation processor -->
                    <path>
                        <groupId>com.querydsl</groupId>
                        <artifactId>querydsl-apt</artifactId>
                        <version>[version]</version>
                    </path>
                    <path>
                        <groupId>org.springframework.data</groupId>
                        <artifactId>spring-data-couchbase</artifactId>
                        <version>[version]</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>
Example 3. Gradle Configuration Example
annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}'
annotationProcessor 'org.springframework.data:spring-data-couchbase:${springDataCouchbaseVersion}'

Usage

在最简单的情况下,您的存储库将扩展 CrudRepository<T, String>, 其中 T 是您想要公开的实体。让我们看一下 UserInfo 的存储库:

Example 4. A UserInfo repository
import org.springframework.data.repository.CrudRepository;

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

请注意,这只是一个接口,而不是一个实际的类。在后台,当您的上下文得到初始化时,将会创建您存储库描述的实际实现,您可以通过常规 bean 访问它们。这意味着您将节省大量的样板代码,同时仍向您的服务层和应用程序公开完整的 CRUD 语义。

现在,让我们假设我们将 UserRepository @Autowire 到一个使用它的类。我们有哪些可用方法呢?

Table 1. Exposed methods on the UserRepository
Method Description

UserInfo save(UserInfo entity)

Save the given entity.

Iterable<UserInfo> save(Iterable<UserInfo> entity)

保存实体列表。

UserInfo findOne(String id)

按其唯一 ID 查找实体。

boolean exists(String id)

通过其唯一 ID 检查给定实体是否存在。

Iterable<UserInfo> findAll()

在此 bucket 中查找具有此类型的全部实体。

Iterable<UserInfo> findAll(Iterable<String> ids)

根据此类型和给定的 ID 列表查找全部实体。

long count()

计算 bucket 中的实体数目。

void delete(String id)

根据其 ID 删除实体。

void delete(UserInfo entity)

Delete the entity.

void delete(Iterable<UserInfo> entities)

Delete all given entities.

void deleteAll()

删除 bucket 中具有该类型的全部实体。

这太酷了!只需定义一个接口,我们就可以在托管实体之上获得完整的 CRUD 功能。

虽然公开的方法为您提供了各种访问模式,但您常常需要定义自定义模式。您可以通过向您的接口添加方法声明来执行此操作,这些方法声明将在后台自动解析为请求,正如我们将在下一节中看到的。

Repositories and Querying

N1QL based querying

前提条件是在实体将存储到的存储桶上创建了主键索引。

这是一个示例:

Example 5. An extended UserInfo repository with N1QL queries
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 会被重建实体所需的返回子句替换。

我们建议你始终将 selectEntity SpEL 与 WHERE 子句和 filter SpEL 结合使用(否则你的查询可能会受到来自其他存储库的实体的影响)。

基于字符串的查询支持参数化查询。您可以使用位置占位符(如 “$1”),在这种情况下,每个方法参数将按顺序映射到 $1$2$3……或者,您可以使用使用 “$someString” 语法的命名占位符。方法参数将使用参数的名称与它们对应的占位符匹配,可以用 @Param(例如 @Param("someString"))注释每个参数(PageableSort 除外)来覆盖此名称。您不能在查询中混合使用这两种方法,这样做会得到 IllegalArgumentException

请注意,可以混合使用 N1QL 占位符和 SpEL。 N1QL 占位符仍然会考虑所有方法参数,因此请务必使用正确的索引,如下面的示例所示:

Example 6. An inline query that mixes SpEL and N1QL placeholders
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND #{[0]} = $2")
public List<User> findUsersByDynamicCriteria(String criteriaField, Object criteriaValue)

这允许您生成类似于(例如) 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

A practical application of SpEL with Spring Security

当您想要执行取决于其他 Spring 组件(如 Spring Security)注入的数据的查询时,SpEL 会很有用。以下是扩展 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) {};
  }
}

现在,所要做的就是声明一个相应的 bean 配置,以使 Spring Data Couchbase 能够访问关联的 SpEL 值:

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

这可以帮助根据已连接用户的角色制作一个查询,例如:

@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND " +
"role = '?#{hasRole('ROLE_ADMIN') ? 'public_admin' : 'admin'}'")
List<UserInfo> findAllAdmins(); //only ROLE_ADMIN users will see hidden admins

删除查询示例:

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

第二种方法使用 Spring-Data 的查询推导机制,从方法名和参数中构建一个 N1QL 查询。这将生成一个类似这样的查询:“SELECT …​ FROM …​ WHERE firstName = "valueOfFnameAtRuntime"”。您可以结合这些条件,甚至可以按 countByFirstname 之类的名称计数,或按 findFirst3ByLastname 之类的名称限制…​

实际上,生成的 N1QL 查询还将包含一个附加的 N1QL 标准,以便仅选择与存储库实体类匹配的文档。

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

Keyword Sample N1QL WHERE clause snippet

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%" - 应为 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)。它添加了两个方法:

Table 2. Exposed methods on the PagingAndSortingRepository
Method Description

Iterable<T> findAll(Sort sort);

允许在某一属性上进行排序,同时检索所有相关实体。

Page<T> findAll(Pageable pageable);

允许以页面形式检索实体。返回的 Page 允许轻松获取下一页面的 Pageable 和项目列表。对于第一次调用,请使用 new PageRequest(0, pageSize) 作为 Pageable。

你还可以将 PageSlice 用作方法返回类型以及 N1QL 后备存储库。

如果分页和排序参数与内联查询一起使用,则内联查询本身不应该包含任何 order by、limit 或 offset 子句,否则服务器会拒绝该查询,因为它格式不正确。

Automatic Index Management

默认情况下,用户预期创建和管理其查询的最优索引。尤其是在开发的早期阶段,自动创建索引以使其能够快速启动会非常方便。

针对 N1QL,提供了以下注释,需要附加到实体上(在类或字段上):

  • @QueryIndexed:置于一个字段上以表明该字段应该是索引的一部分。

  • @CompositeQueryIndex:置于类上以表明应创建多个字段(复合)的索引。

  • @CompositeQueryIndexes:如果应该创建多个 CompositeQueryIndex,此注释将获取它们的列表。

例如,这是在实体上定义复合索引的方式:

Example 7. Composite index on two fields with ordering
@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;
	}

}

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

Example 8. Enable auto index creation
@Override
protected boolean autoIndexCreation() {
 return true;
}

Querying with consistency

默认情况下,使用 N1QL 的存储库查询使用 NOT_BOUNDED 扫描一致性。这意味着结果返回很快,但索引中的数据可能还不包含先前写入操作的数据(称为最终一致性)。如果您需要为查询使用“就绪读自己的写入”语义,您需要使用 @ScanConsistency 注释。以下是一个示例:

Example 9. Using a different scan consistency
@Repository
public interface AirportRepository extends PagingAndSortingRepository<Airport, String> {

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

}

DTO Projections

使用查询方法时,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 是主键

  • firstNamelastName 是数据属性

  • 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 详细信息呢?您可以通过定义一个或多个投影,向存储库服务使用者提供替代方案。

Example 10. Simple Projection
interface NoAddresses {  1

  String getFirstName(); 2

  String getLastName();  3
}

该投影包含以下详情:

1 使之声明化的普通 Java 接口。
2 Export the firstName.
3 Export the lastName.

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

interface PersonRepository extends CrudRepository<Person, Long> {

  NoAddresses findByFirstName(String firstName);
}

投影声明了底层类型与公开属性相关的签名类型的契约。因此,需要根据底层类型的属性名称来命名 getter 方法。如果底层属性名称为 firstName,那么 getter 方法必须命名为 getFirstName,否则 Spring Data 无法查找源属性。