Couchbase repositories
Spring Data 存储库抽象的目标是显著减少针对各种持久性存储实现数据访问层所需的样板代码量。 默认情况下,如果操作是单文档操作并且 ID 已知,则操作由键/值支持。对于其他所有操作,默认情况下会生成 N1QL 查询,因此必须创建适当的索引以实现高性能数据访问。 请注意,你可以调整你查询所需的相容性(请参阅Querying with consistency),并且可以使用不同的存储区支持不同的存储库(请参阅[couchbase.repository.multibucket])。
Configuration
虽然始终提供对存储库的支持,但您需要在通用情况下或对特定命名空间启用它们。如果您扩展 AbstractCouchbaseConfiguration
,只需使用 @EnableCouchbaseRepositories
注释。它提供了许多可能的选项来缩小或自定义搜索路径,最常见的一个选项是 basePackages
。
还要注意,如果您在 Spring 启动程序内部运行,则自动配置支持已为您设置了注释,因此您只需在需要覆盖默认值时使用它。
@Configuration
@EnableCouchbaseRepositories(basePackages = {"com.couchbase.example.repos"})
public class Config extends AbstractCouchbaseConfiguration {
//...
}
高级用法在 [couchbase.repository.multibucket] 中有所描述。
QueryDSL Configuration
Spring Data Couchbase 支持 QueryDSL 来构建类型安全的查询。要启用代码生成,您需要在项目中将 spring-data-couchbase
设置为注释处理器。
<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>
annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}'
annotationProcessor 'org.springframework.data:spring-data-couchbase:${springDataCouchbaseVersion}'
Usage
在最简单的情况下,您的存储库将扩展 CrudRepository<T, String>
, 其中 T 是您想要公开的实体。让我们看一下 UserInfo 的存储库:
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<UserInfo, String> {
}
请注意,这只是一个接口,而不是一个实际的类。在后台,当您的上下文得到初始化时,将会创建您存储库描述的实际实现,您可以通过常规 bean 访问它们。这意味着您将节省大量的样板代码,同时仍向您的服务层和应用程序公开完整的 CRUD 语义。
现在,让我们假设我们将 UserRepository
@Autowire
到一个使用它的类。我们有哪些可用方法呢?
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
前提条件是在实体将存储到的存储桶上创建了主键索引。
这是一个示例:
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")
)注释每个参数(Pageable
或 Sort
除外)来覆盖此名称。您不能在查询中混合使用这两种方法,这样做会得到 IllegalArgumentException
。
请注意,可以混合使用 N1QL 占位符和 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 组件(如 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 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
您可以使用此方法同时使用计数查询和 [repositories.limit-query-result] 功能。
借助 N1QL,存储库的另一种可能的接口是 PagingAndSortingRepository
(它扩展了 CrudRepository
)。它添加了两个方法:
Method | Description |
---|---|
Iterable<T> findAll(Sort sort); |
允许在某一属性上进行排序,同时检索所有相关实体。 |
Page<T> findAll(Pageable pageable); |
允许以页面形式检索实体。返回的 |
你还可以将 |
如果分页和排序参数与内联查询一起使用,则内联查询本身不应该包含任何 order by、limit 或 offset 子句,否则服务器会拒绝该查询,因为它格式不正确。 |
Automatic Index Management
默认情况下,用户预期创建和管理其查询的最优索引。尤其是在开发的早期阶段,自动创建索引以使其能够快速启动会非常方便。
针对 N1QL,提供了以下注释,需要附加到实体上(在类或字段上):
-
@QueryIndexed
:置于一个字段上以表明该字段应该是索引的一部分。 -
@CompositeQueryIndex
:置于类上以表明应创建多个字段(复合)的索引。 -
@CompositeQueryIndexes
:如果应该创建多个CompositeQueryIndex
,此注释将获取它们的列表。
例如,这是在实体上定义复合索引的方式:
@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;
}
}
默认情况下,索引创建被禁用。如果您想启用它,您需要在配置中覆盖它:
@Override
protected boolean autoIndexCreation() {
return true;
}
Querying with consistency
默认情况下,使用 N1QL 的存储库查询使用 NOT_BOUNDED
扫描一致性。这意味着结果返回很快,但索引中的数据可能还不包含先前写入操作的数据(称为最终一致性)。如果您需要为查询使用“就绪读自己的写入”语义,您需要使用 @ScanConsistency
注释。以下是一个示例:
@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
是主键 -
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
详细信息呢?您可以通过定义一个或多个投影,向存储库服务使用者提供替代方案。
interface NoAddresses { 1
String getFirstName(); 2
String getLastName(); 3
}
该投影包含以下详情:
1 | 使之声明化的普通 Java 接口。 |
2 | Export the firstName . |
3 | Export the lastName . |
NoAddresses
投影只对 firstName
和 lastName
具有 getter,这意味着它不会提供任何地址信息。在这种情况下,查询方法定义将返回 NoAdresses
,而不是 Person
。
interface PersonRepository extends CrudRepository<Person, Long> {
NoAddresses findByFirstName(String firstName);
}
投影声明了底层类型与公开属性相关的签名类型的契约。因此,需要根据底层类型的属性名称来命名 getter 方法。如果底层属性名称为 firstName
,那么 getter 方法必须命名为 getFirstName
,否则 Spring Data 无法查找源属性。