跳到主要内容

8. Micronaut Data Azure Cosmos

Micronaut Data Azure Cosmos 支持 JPA 实现的部分功能,包括:

与其他数据模块一样,不支持级联和连接。更多规格信息,参阅此处。

对象层与 Azure Cosmos Db 序列化/反序列化之间的交互是使用 Micronaut 序列化实现的。

8.1 快速入门

目前仍无法通过 Micronaut Launch 创建支持 Azure Cosmos Db 的 Micronaut 项目。我们的团队将在不久的将来解决这个问题。

要开始使用 Micronaut Data Azure Cosmos,请在注解处理器路径中添加以下依赖:

annotationProcessor("io.micronaut.data:micronaut-data-document-processor")
注意

对于 Kotlin,依赖关系应在 kapt 作用域中;对于 Groovy,依赖关系应在 compileOnly 作用域中。

然后,您应该在 micronaut-data-azure-cosmos 模块上配置编译作用域依赖关系:

implementation("io.micronaut.data:micronaut-data-azure-cosmos")

接下来,你需要配置至少一个数据源。应用程序配置文件中的以下片段是配置默认 Azure Cosmos Db 数据源的示例:

micronaut.application.name=example
azure.cosmos.default-gateway-mode=true
azure.cosmos.endpoint-discovery-enabled=false
azure.cosmos.endpoint=https://localhost:8081
azure.cosmos.key=
azure.cosmos.database.throughput-settings.request-units=1000
azure.cosmos.database.throughput-settings.auto-scale=false
azure.cosmos.database.database-name=testDb

你可以从这里查找关于配置的更多信息。

要从数据库中检索对象,需要定义一个注解为 @MappedEntity 的类:

@MappedEntity
public class Book {
@Id
@GeneratedValue
@PartitionKey
private String id;
private String title;
private int pages;
@MappedProperty(converter = ItemPriceAttributeConverter.class)
@Nullable
private ItemPrice itemPrice;
@DateCreated
private Date createdDate;
@DateUpdated
private Date updatedDate;

public Book(String title, int pages) {
this.title = title;
this.pages = pages;
}
// ...
}

随后是一个从 CrudRepository 扩展而来的接口:

package example;

import io.micronaut.data.annotation.Id;
import io.micronaut.data.cosmos.annotation.CosmosRepository;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.Slice;
import io.micronaut.data.repository.CrudRepository;

import java.util.List;


@CosmosRepository // (1)
interface BookRepository extends CrudRepository<Book, String> { // (2)
Book find(String title);
}
  1. 接口注解为 @CosmosRepository
  2. CrudRepository 接口接受 2 个通用参数,实体类型(在本例中是 Book)和 ID 类型(在本例中是 String)。

现在您可以对实体执行 CRUD(创建、读取、更新、删除)操作。example.BookRepository 的实现是在编译时创建的。要获得对它的引用,只需注入 Bean:

@Inject
BookRepository bookRepository;

使用 Micronaut Data Azure Cosmos 时,每个 MappedEntity 都将与容器相对应。一个容器只能容纳一个实体或文档类型。默认情况下,用 @MappedEntity 注解的类的简单名称将用作容器名称。如果实体类是 CosmosBook,那么预期的容器名称将是 cosmos_book,除非在 MappedEntity 注解值中没有被覆盖。实体字段的默认命名策略是 Raw 策略,用户通常不需要覆盖它。

保存实例(创建)

要保存实例,请使用 CrudRepository 接口的 save 方法:

Book book = new Book("The Stand", 1000);
book.setItemPrice(new ItemPrice(200));
bookRepository.save(book);

检索实例(读取)

要获取一个 book,请使用 findById

book = bookRepository.findById(id).orElse(null);

更新实例(更新)

有了 Micronaut Data Azure Cosmos,你可以使用 CrudRepositorysave 方法或手动实现更新方法。您可以为仓库中的更新定义显式更新方法。例如:

void update(@Id String id, int pages);

void update(@Id String id, String title);

然后就可以这样调用了:

bookRepository.update(book.getId(), "Changed");

删除实例(删除)

要删除一个实例,请使用 deleteById

bookRepository.deleteById(id);

恭喜您实现了第一个 Micronaut Data Azure Cosmos 仓库!继续阅读,了解更多。

8.2 配置

在现有容器中使用现有 Azure Cosmos 数据库时,除了端点、密钥和数据库名称外,不需要任何特殊配置。但是,出于测试目的或需要在应用程序启动期间创建数据库容器时,还有其他配置容器的选项。

正如[快速入门中提到的,在 Azure Cosmos Db 中,每个用 @MappedEntity 标注的类都对应一个容器。如果属性 azure.cosmos.database.update-policy 设置为 NONE,则不会尝试创建容器。如果该值设置为 CREATE_IF_NOT_EXISTS,那么如果容器不存在,应用程序将尝试创建容器。而如果值为 UPDATE,应用程序将尝试替换现有的任何容器及其属性。

目前,只能为数据库和容器配置一小部分属性。数据库的吞吐量属性可以配置,而容器的吞吐量属性和分区密钥路径可以配置。

注意

配置分区键值的另一种方法是添加注解 @PartitionKey

下面是一个应用程序配置示例,显示了在创建新容器(如果容器不存在)时使用的容器和数据库属性:

micronaut.application.name=example
azure.cosmos.default-gateway-mode=true
azure.cosmos.endpoint-discovery-enabled=false
azure.cosmos.endpoint=https://localhost:8081
azure.cosmos.key=
azure.cosmos.database.throughput-settings.request-units=1000
azure.cosmos.database.throughput-settings.auto-scale=false
azure.cosmos.database.database-name=testDb
azure.cosmos.database.packages=io.micronaut.data.azure.entities
azure.cosmos.database.update-policy=CREATE_IF_NOT_EXISTS
azure.cosmos.database.container-settings[0].container-name=family
azure.cosmos.database.container-settings[0].partition-key-path=/lastname
azure.cosmos.database.container-settings[0].throughput-settings.request-units=1000
azure.cosmos.database.container-settings[0].throughput-settings.auto-scale=false
azure.cosmos.database.container-settings[1].container-name=book
azure.cosmos.database.container-settings[1].partition-key-path=/id
azure.cosmos.database.container-settings[1].throughput-settings.request-units=1200
azure.cosmos.database.container-settings[1].throughput-settings.auto-scale=false

8.3 仓库

快速入门中所述,Micronaut Data 中的 Azure Cosmos 数据库被定义为带有 @CosmosRepository 注解的接口。

例如:

@CosmosRepository (1)
public interface BookRepository extends CrudRepository<Book, String> {
Optional<Book> findByAuthorId(@NotNull String authorId);
}
  1. @CosmosRepository 标识访问 Azure Cosmos 数据库的接口

根据方法签名或为 GenericRepository 接口指定的通用类型参数,确定查询时作为根实体的实体。

如果无法确定根实体,则会出现编译错误。

Azure Cosmos Data 支持与 JPA 实现相同的接口。

请注意,除了接口外,您还可以将存储库定义为抽象类:

package example;

import io.micronaut.data.cosmos.annotation.CosmosRepository;
import io.micronaut.data.repository.CrudRepository;

import java.util.List;

@CosmosRepository
public abstract class AbstractBookRepository implements CrudRepository<Book, String> {

public abstract List<Book> findByTitle(String title);
}

8.4 带有标准 API 的仓库

在某些情况下,您需要在运行时以编程方式建立查询;为此,Micronaut Data 实现了 Jakarta Persistence Criteria API 3.0 的子集,可用于 Micronaut Data Azure Cosmos 功能。

为了实现无法在编译时定义的查询,Micronaut Data 引入了 JpaSpecificationExecutor 资源库接口,可用于扩展您的仓库接口:

@MongoRepository
public interface PersonRepository extends CrudRepository<Person, ObjectId>, JpaSpecificationExecutor<Person> {
}

每种方法都需要一个 "规范",它是一个功能接口,包含一组旨在以编程方式建立查询的 Criteria API 对象。

Micronaut Criteria API 目前只实现了 API 的一个子集。其中大部分在内部用于创建带有谓词和投影的查询。

目前,不支持 JPA Criteria API 功能:

  • 使用自定义 ON 表达式和类型化连接方法(如 joinSet 等)进行连接
  • 子查询
  • 集合操作:isMember
  • 自定义或元组结果类型
  • 转换表达式,如 concat、substring 等
  • 案例和函数

有关 Jakarta Persistence Criteria API 3.0 的更多信息,参阅官方 API 规范

8.4.1 查询

要查找一个或多个实体,您可以使用 JpaSpecificationExecutor 接口中的以下方法之一:

Optional<Person> findOne(PredicateSpecification<Person> spec);

Optional<Person> findOne(QuerySpecification<Person> spec);

List<Person> findAll(PredicateSpecification<Person> spec);

List<Person> findAll(QuerySpecification<Person> spec);

List<Person> findAll(PredicateSpecification<Person> spec, Sort sort);

List<Person> findAll(QuerySpecification<Person> spec, Sort sort);

Page<Person> findAll(PredicateSpecification<Person> spec, Pageable pageable);

Page<Person> findAll(QuerySpecification<Person> spec, Pageable pageable);

如你所见,findOne/findAll 方法有两种变体。

第一个方法是期待 PredicateSpecification,这是一个简单的规范接口,可以通过实现它来返回一个谓词:

import static jakarta.persistence.criteria.*;

public interface PredicateSpecification<T> {

(1)
@Nullable
Predicate toPredicate(@NonNull Root<T> root, (2)
@NonNull CriteriaBuilder criteriaBuilder (3)
);

}
  1. 该规范正在生成一个查询限制谓词
  2. 实体根
  3. 标准生成器

该接口还可用于更新和删除方法,并提供了 orand 方法,用于组合多个谓词。

第二个接口仅用于查询标准,因为它包含 jakarta.persistence.criteria.CriteriaQuery 作为参数。

import static jakarta.persistence.criteria.*;

public interface QuerySpecification<T> {

(1)
@Nullable
Predicate toPredicate(@NonNull Root<T> root, (2)
@NonNull CriteriaQuery<?> query, (3)
@NonNull CriteriaBuilder criteriaBuilder (4)
);

}
  1. 该规范正在生成一个查询限制谓词
  2. 实体根
  3. 标准查询实例
  4. 标准生成器

实现计数查询可使用以下方法:

long count(PredicateSpecification<Person> spec);

long count(QuerySpecification<Person> spec);

您可以定义有助于创建查询的标准规范方法:

class Specifications {

static PredicateSpecification<Person> nameEquals(String name) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get("name"), name);
}

static PredicateSpecification<Person> ageIsLessThan(int age) {
return (root, criteriaBuilder) -> criteriaBuilder.lessThan(root.get("age"), age);
}

}

然后,您可以将它们组合起来进行 findcount 查询:

Person denis = personRepository.findOne(nameEquals("Denis")).orElse(null);

long countAgeLess30 = personRepository.count(ageIsLessThan(30));

long countAgeLess20 = personRepository.count(ageIsLessThan(20));

long countAgeLess30NotDenis = personRepository.count(ageIsLessThan(30).and(not(nameEquals("Denis"))));

List<Person> people = personRepository.findAll(where(nameEquals("Denis").or(nameEquals("Josh"))));

Micronaut Azure Cosmos Data 支持的特定标准是 ArrayContainsCollectionContains,对于具有名为 tags 的数组或字符串字段列表的类,可以通过类似这样的自定义仓库方法来使用:

public abstract List<Family> findByTagsArrayContains(String tag);

或谓词规范:

public static PredicateSpecification<Family> tagsContain(String tag) {
return (root, criteriaBuilder) -> ((PersistentEntityCriteriaBuilder) criteriaBuilder).arrayContains(root.get("tags"), criteriaBuilder.literal(tag));
}

请注意,Microsoft Data Azure Cosmos Db 支持搜索仅包含单个元素的列表或数组。使用 ArrayContains 进行部分搜索时,不能使用通用仓库方法,而只能使用像这样的原始查询自定义方法:

@Query("SELECT DISTINCT VALUE f FROM family f WHERE ARRAY_CONTAINS(f.children, :gender, true)")
public abstract List<Family> childrenArrayContainsGender(Map.Entry<String, Object> gender);

然后传递以 "gender" 为键、"gender" 为值的 map 条目,基本上任何对象都可以序列化为 {"gender"."<gender_value>"}:"<gender_value>"} 的任何对象。这将仅使用性别字段对 Family 类中的子女数组执行搜索。这也可以通过使用谓词规范来实现:

public static PredicateSpecification<Family> childrenArrayContainsGender(GenderAware gender) {
return (root, criteriaBuilder) -> ((PersistentEntityCriteriaBuilder) criteriaBuilder).arrayContains(root.join("children"), criteriaBuilder.literal(gender));
}
注意

示例使用的是编译时已知的值,在这种情况下,最好创建自定义仓库方法,这样就能在编译时生成查询,并消除运行时的开销。建议仅在动态查询中使用标准,因为在构建时查询结构是未知的。

8.4.2 更新

要实现更新,您可以使用 JpaSpecificationExecutor 接口中的以下方法:

long updateAll(UpdateSpecification<Person> spec);

该方法期待 UpdateSpecification,它是规范接口的一个变体,包括对 jakarta.persistence.criteria.CriteriaUpdate 的访问:

import static jakarta.persistence.criteria.*;

public interface UpdateSpecification<T> {

(1)
@Nullable
Predicate toPredicate(@NonNull Root<T> root, (2)
@NonNull CriteriaUpdate<?> query, (3)
@NonNull CriteriaBuilder criteriaBuilder (4)
);

}
  1. 该规范正在生成一个查询限制谓词
  2. 实体根
  3. 标准更新实例
  4. 标准生成器

可使用 jakarta.persistence.criteria.CriteriaUpdate 接口更新特定属性:

query.set(root.get("name"), newName);

您可以定义标准规范方法,包括更新规范,这将有助于您创建更新查询:

class Specifications {

static PredicateSpecification<Person> nameEquals(String name) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get("name"), name);
}

static PredicateSpecification<Person> ageIsLessThan(int age) {
return (root, criteriaBuilder) -> criteriaBuilder.lessThan(root.get("age"), age);
}

static UpdateSpecification<Person> setNewName(String newName) {
return (root, query, criteriaBuilder) -> {
query.set(root.get("name"), newName);
return null;
};
}
}

然后,您可以使用更新规范与谓词规范相结合:

long recordsUpdated = personRepository.updateAll(setNewName("Steven").where(nameEquals("Denis")));

8.4.3 删除

要删除一个实体或多个实体,可以使用 JpaSpecificationExecutor 接口中的以下方法之一:

long deleteAll(PredicateSpecification<Person> spec);

long deleteAll(DeleteSpecification<Person> spec);

与查询一样,deleteAll 方法也有两种变体。

第一个方法期待 PredicateSpecification,它与查询部分描述的接口相同。

第二种方法带有 DeleteSpecification,仅用于删除标准,因为它包含了对 jakarta.persistence.criteria.CriteriaDelete 的访问。

import static jakarta.persistence.criteria.*;

public interface DeleteSpecification<T> {

(1)
@Nullable
Predicate toPredicate(@NonNull Root<T> root, (2)
@NonNull CriteriaDelete<?> query, (3)
@NonNull CriteriaBuilder criteriaBuilder (4)
);

}
  1. 该规范正在生成一个查询限制谓词
  2. 实体根
  3. 标准删除实例
  4. 标准生成器

对于删除,您可以重复使用与查询和更新相同的谓词:

class Specifications {

static PredicateSpecification<Person> nameEquals(String name) {
return (root, criteriaBuilder) -> criteriaBuilder.equal(root.get("name"), name);
}

static PredicateSpecification<Person> ageIsLessThan(int age) {
return (root, criteriaBuilder) -> criteriaBuilder.lessThan(root.get("age"), age);
}

}

只需将谓词规范传递给 deleteAll 方法即可:

long recordsDeleted = personRepository.deleteAll(where(nameEquals("Denis")));

8.5 Azure Cosmos 规格

由于 Azure Cosmos 数据库不像 Micronaut Data 支持的大多数数据库那样是关系型数据库,它在某些具体方面确实有不同的实现方式。

关系映射

由于该数据库不是关系型数据库,不支持跨容器和跨文档连接,因此实体/容器之间的关系不可映射。唯一支持的关系类型是 @Relation(value=Relation.Kind.EMBEDDED)@Relation(value=Relation.Kind.ONE_TO_MANY),它们实际上是文档与其嵌入对象或数组之间的关系。

下面是此类映射的一个示例:

@MappedEntity
public class Family {

@Id
private String id;

@PartitionKey
private String lastName;

@Relation(value = Relation.Kind.EMBEDDED)
private Address address;

@Relation(value = Relation.Kind.ONE_TO_MANY)
private List<Child> children = new ArrayList<>();

在这种情况下,我们的查询生成器需要使用 Relation 映射,以便根据嵌入对象或数组中的字段生成投影、排序和筛选,这可以在 FamilyRepository 中声明的方法中看到。

    public abstract List<Family> findByAddressStateAndAddressCityOrderByAddressCity(String state, String city);

public abstract void updateByAddressCounty(String county, boolean registered, @Nullable Date registeredDate);

@Join(value = "children.pets", alias = "pets")
public abstract List<Family> findByChildrenPetsType(PetType type);

public abstract List<Child> findChildrenByChildrenPetsGivenName(String name);

由于数据库的性质和关系的实现,级联的意义也不大。文档中的嵌入对象和数组会在保存文档时自动保存。

身份

在 Azure Cosmos 数据库中,每个文档都有字符串类型的内部 id 属性。Micronaut Data Cosmos 希望 @Id 的类型为:ShortIntegerLongStringUUID。在保存和读取时,类型被序列化为字符串,并从存储在 id 属性中的字符串反序列化。使用不支持的类型声明带有 @Id 注解的属性将导致异常。生成 id 只适用于 StringUUID,其中 UUID 可通过使用 @GeneratedValue@AutoPopulated 注解生成。字符串 ID 只能通过 @GeneratedValue 注解生成。数字 id 不能自动生成,应由用户在保存前设置 id 值。不支持复合标识。

分区密钥

在 Azure Cosmos 数据库中,分区键是将数据有效分配到不同逻辑集和物理集的核心要素,以便尽快完成对数据库的查询。每个映射实体都应定义分区键。如上文所述,可以在适当的实体字段上使用 @PartitionKey 注解或通过配置来定义分区键,详见配置部分的说明。有效使用定义明确的分区密钥将提高操作性能并降低请求单位成本。Micronaut Data Cosmos 尽可能使用分区密钥。下面是一些在读取、更新或删除操作中使用分区密钥的仓库方法示例。

    public abstract Optional<Family> queryById(String id, PartitionKey partitionKey);

public abstract void updateRegistered(@Id String id, boolean registered, PartitionKey partitionKey);

public abstract void deleteByLastName(String lastName, PartitionKey partitionKey);

public abstract void deleteById(String id, PartitionKey partitionKey);

诊断

Azure Cosmos Db 提供操作诊断,这样用户就可以获得这些信息,或许还可以与他们的日志或度量系统集成。在 Micronaut Data Azure 中,我们公开了 CosmosDiagnosticsProcessor 接口。用户需要实现该接口并将其添加到上下文中,这样我们的操作类就可以使用该接口。它只有一个方法:

void processDiagnostics(String operationName, @Nullable CosmosDiagnostics cosmosDiagnostics, @Nullable String activityId, double requestCharge);

在每次对 Azure Cosmos Db 进行操作后都会调用。参数 operationName 是 Micronaut Data Azure 中的内部操作名称,它有这些已知值:

String CREATE_DATABASE_IF_NOT_EXISTS = "CreateDatabaseIfNotExists";
String REPLACE_DATABASE_THROUGHPUT = "ReplaceDatabaseThroughput";
String CREATE_CONTAINER_IF_NOT_EXISTS = "CreateContainerIfNotExists";
String REPLACE_CONTAINER_THROUGHPUT = "ReplaceContainerThroughput";
String REPLACE_CONTAINER = "ReplaceContainer";
String QUERY_ITEMS = "QueryItems";
String EXECUTE_BULK = "ExecuteBulk";
String CREATE_ITEM = "CreateItem";
String REPLACE_ITEM = "ReplaceItem";
String DELETE_ITEM = "DeleteItem";

以便用户了解正在处理哪个操作的诊断结果。

8.6 使用属性转换器

在某些情况下,您希望以不同于实体的方式在数据库中表示属性。

请看下面的实体示例:

@MappedEntity
public class Book {
@Id
@GeneratedValue
@PartitionKey
private String id;
private String title;
private int pages;
@MappedProperty(converter = ItemPriceAttributeConverter.class)
@Nullable
private ItemPrice itemPrice;
@DateCreated
private Date createdDate;
@DateUpdated
private Date updatedDate;

public Book(String title, int pages) {
this.title = title;
this.pages = pages;
}
// ...
}

Book 类引用了 ItemPrice 类型。ItemPrice 类型定义如下:

package example;

import io.micronaut.core.annotation.Introspected;

@Introspected
public class ItemPrice {

private double price;

public ItemPrice(double price) {
this.price = price;
}

public double getPrice() {
return price;
}

public static ItemPrice valueOf(double price) {
return new ItemPrice(price);
}
}
  1. 属性转换器实现 @AttributeConverter 且必须是一个 Bean
  2. ItemPriceDouble 的转换器
  3. DoubleItemPrice 的转换器
注意

使用 @MappedProperty 可以定义转换器的结果类型:@MappedProperty(converterPersistedType = Double.class),在这种情况下,数据类型将被自动检测到。

8.7 优化锁定

乐观锁定是一种策略,即注意实际记录状态的版本,只有当版本相同时才修改记录。

与 Micronaut 中的其他数据库实现不同,Azure Cosmos 数据库依赖于每个文档中 _etag 字段的存在。我们不使用 @Version,因为 _etag 字段是字符串类型,为此我们引入了 @ETag 注解。

每次在 Azure Cosmos 数据库中更新文档时都会更新该字段,在下一次更新之前,它会检查正在更新的文档中的当前值是否与数据库中的当前值匹配。如果值不匹配,Micronaut 将抛出 OptimisticLockException

@ETag
private String documentVersion;

英文链接