快速入门:使用 Java SDK 和 Azure Cosmos DB 生成表 API 应用
适用对象: 表
本快速入门演示如何从 Java 应用程序访问 Azure Cosmos DB 表 API。 Azure Cosmos DB 表 API 是一种无架构数据存储,允许应用程序在云中存储结构化 NoSQL 数据。 由于数据存储在无架构设计中,因此将具有新特性的对象添加到表中时,系统会自动向表中添加新属性(列)。
Java 应用程序可以使用 azure-data-tables 客户端库访问 Azure Cosmos DB 表 API。
先决条件
示例应用程序是使用 Spring Boot 2.6.4 编写的,可以使用 Visual Studio Code 或 IntelliJ IDEA 作为 IDE。
如果没有 Azure 试用版订阅,请在开始前创建一个试用版订阅。
示例应用程序
可以从存储库 https://github.com/Azure-Samples/msdocs-azure-data-tables-sdk-java 克隆或下载本教程的示例应用程序。 入门应用和完整应用都包含在示例存储库中。
git clone https://github.com/Azure-Samples/msdocs-azure-data-tables-sdk-java
示例应用程序使用天气数据作为示例来演示表 API 的功能。 表示天气观察值的对象使用表 API 进行存储和检索,包括存储具有附加属性的对象以演示表 API 的无架构功能。
1 - 创建 Azure Cosmos DB 帐户
首先需要创建一个 Azure Cosmos DB Tables API 帐户,该帐户将包含应用程序中使用的表。 可以使用 Azure 门户、Azure CLI 或 Azure PowerShell 执行此操作。
登录到 Azure 门户,并按照以下步骤创建 Azure Cosmos DB 帐户。
2 - 创建表
接下来,需要在 Azure Cosmos DB 帐户中创建表,以供应用程序使用。 与传统数据库不同,只需指定表的名称,无需指定表中的属性(列)。 将数据加载到表中时,属性(列)会根据需要自动创建。
在 Azure 门户中完成以下步骤,以在 Azure Cosmos DB 帐户中创建表。
3 - 获取 Azure Cosmos DB 连接字符串
若要访问 Azure Cosmos DB 中的表,你的应用需要 CosmosDB 存储帐户的表连接字符串。 可以使用 Azure 门户、Azure CLI 或 Azure PowerShell 检索该连接字符串。
说明 | 屏幕快照 |
---|---|
在 Azure Cosmos DB 帐户页左侧的“设置”标题下找到名为“连接字符串”的菜单项,并将其选中。 你将进入一个页面,可以在其中检索存储帐户的连接字符串。 | |
复制“主连接字符串”值以在应用程序中使用。 |
Azure Cosmos DB 帐户的连接字符串被视为应用机密,必须像其他应用机密或密码一样加以保护。 此示例使用 POM 在开发过程中存储连接字符串,并提供给应用程序使用。
<profiles>
<profile>
<id>local</id>
<properties>
<azure.tables.connection.string>
<![CDATA[YOUR-DATA-TABLES-SERVICE-CONNECTION-STRING]]>
</azure.tables.connection.string>
<azure.tables.tableName>WeatherData</azure.tables.tableName>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
4 - 加入 azure-data-tables 包
若要从 Java 应用程序访问 Azure Cosmos DB 表 API,请加入 azure-data-tables 包。
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-data-tables</artifactId>
<version>12.2.1</version>
</dependency>
5 - 在 TableServiceConfig.java 中配置表客户端
Azure SDK 使用客户端对象与 Azure 通信,以对 Azure 执行不同的操作。 TableClient 对象是用于与 Azure Cosmos DB 表 API 通信的对象。
应用程序通常会为每个表创建单个 TableClient 对象,以在整个应用程序中使用。 建议指示方法生成由 Spring 容器管理的 TableClient 对象 bean,并作为单一实例来实现此目的。
在应用程序的 TableServiceConfig.java
文件中,编辑 tableClientConfiguration()
方法以匹配以下代码片段:
@Configuration
public class TableServiceConfiguration {
private static String TABLE_NAME;
private static String CONNECTION_STRING;
@Value("${azure.tables.connection.string}")
public void setConnectionStringStatic(String connectionString) {
TableServiceConfiguration.CONNECTION_STRING = connectionString;
}
@Value("${azure.tables.tableName}")
public void setTableNameStatic(String tableName) {
TableServiceConfiguration.TABLE_NAME = tableName;
}
@Bean
public TableClient tableClientConfiguration() {
return new TableClientBuilder()
.connectionString(CONNECTION_STRING)
.tableName(TABLE_NAME)
.buildClient();
}
}
还需要将下面的 using 语句添加到 TableServiceConfig.java
文件顶部。
import com.azure.data.tables.TableClient;
import com.azure.data.tables.TableClientBuilder;
6 - 实现 Azure Cosmos DB 表操作
示例应用的所有 Azure Cosmos DB 表操作都在位于 Services 目录中的 TablesServiceImpl
类中实现。 需要导入 com.azure.data.tables
SDK 包。
import com.azure.data.tables.TableClient;
import com.azure.data.tables.models.ListEntitiesOptions;
import com.azure.data.tables.models.TableEntity;
import com.azure.data.tables.models.TableTransactionAction;
import com.azure.data.tables.models.TableTransactionActionType;
在 TableServiceImpl
类的开头,添加用于 TableClient 对象的成员变量以及一个构造函数,以允许将 TableClient 对象注入该类。
@Autowired
private TableClient tableClient;
从表中获取行
TableClient 类包含名为 listEntities 的方法,可用于从表中选择行。 在此示例中,由于未向该方法传递任何参数,因此会从表中选择所有行。
该方法还采用类型为 TableEntity 的泛型参数,该参数指定返回的模型类数据。 在此例中,将使用内置类 TableEntity,这意味着 listEntities
方法会返回 PagedIterable<TableEntity>
集合作为其结果。
public List<WeatherDataModel> retrieveAllEntities() {
List<WeatherDataModel> modelList = tableClient.listEntities().stream()
.map(WeatherDataUtils::mapTableEntityToWeatherDataModel)
.collect(Collectors.toList());
return Collections.unmodifiableList(WeatherDataUtils.filledValue(modelList));
}
com.azure.data.tables.models
包中定义的 TableEntity 类具有适用于表中的分区键和行键值的属性。 这两个值共同表示表中行的唯一键。 在此示例应用程序中,气象站的名称(城市)存储在分区键中,观测日期/时间存储在行键中。 所有其他属性(温度、湿度、风速)存储在 TableEntity
对象中的字典内。
常见做法是将 TableEntity 对象映射到具有你自己定义的对象。 示例应用程序在 Models 目录中定义了一个类 WeatherDataModel
来实现此目的。 此类具有分区键和行键将映射到的气象站名称和观察日期属性,可为这些值提供更有意义的属性名称。 它随后使用字典将所有其他属性存储在对象中。 这是使用表存储时的常见模式,因为行可以具有任意数量的任意属性,并且我们希望模型对象能够捕获所有这些属性。 此类还包含列出类中的属性的方法。
public class WeatherDataModel {
public WeatherDataModel(String stationName, String observationDate, OffsetDateTime timestamp, String etag) {
this.stationName = stationName;
this.observationDate = observationDate;
this.timestamp = timestamp;
this.etag = etag;
}
private String stationName;
private String observationDate;
private OffsetDateTime timestamp;
private String etag;
private Map<String, Object> propertyMap = new HashMap<String, Object>();
public String getStationName() {
return stationName;
}
public void setStationName(String stationName) {
this.stationName = stationName;
}
public String getObservationDate() {
return observationDate;
}
public void setObservationDate(String observationDate) {
this.observationDate = observationDate;
}
public OffsetDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(OffsetDateTime timestamp) {
this.timestamp = timestamp;
}
public String getEtag() {
return etag;
}
public void setEtag(String etag) {
this.etag = etag;
}
public Map<String, Object> getPropertyMap() {
return propertyMap;
}
public void setPropertyMap(Map<String, Object> propertyMap) {
this.propertyMap = propertyMap;
}
}
mapTableEntityToWeatherDataModel
方法用于将 TableEntity 对象映射到 WeatherDataModel
对象。 mapTableEntityToWeatherDataModel
方法直接映射 PartitionKey
、RowKey
、Timestamp
和 Etag
属性,然后使用 properties.keySet
循环访问 TableEntity
对象中的其他属性,并将这些属性映射到 WeatherDataModel
对象(不包括已直接映射的属性)。
编辑 mapTableEntityToWeatherDataModel
方法中的代码以匹配以下代码块。
public static WeatherDataModel mapTableEntityToWeatherDataModel(TableEntity entity) {
WeatherDataModel observation = new WeatherDataModel(
entity.getPartitionKey(), entity.getRowKey(),
entity.getTimestamp(), entity.getETag());
rearrangeEntityProperties(observation.getPropertyMap(), entity.getProperties());
return observation;
}
private static void rearrangeEntityProperties(Map<String, Object> target, Map<String, Object> source) {
Constants.DEFAULT_LIST_OF_KEYS.forEach(key -> {
if (source.containsKey(key)) {
target.put(key, source.get(key));
}
});
source.keySet().forEach(key -> {
if (Constants.DEFAULT_LIST_OF_KEYS.parallelStream().noneMatch(defaultKey -> defaultKey.equals(key))
&& Constants.EXCLUDE_TABLE_ENTITY_KEYS.parallelStream().noneMatch(defaultKey -> defaultKey.equals(key))) {
target.put(key, source.get(key));
}
});
}
筛选从表返回的行
若要筛选从表返回的行,可以将 OData 样式筛选器字符串传递到 listEntities 方法。 例如,如果你想要获取 2021 年 7 月 1 日午夜到 2021 年 7 月 2 日午夜(含)的所有上海天气读数,可以传入以下筛选字符串。
PartitionKey eq 'Shanghai' and RowKey ge '2021-07-01 12:00 AM' and RowKey le '2021-07-02 12:00 AM'
可以在筛选器系统查询选项部分中查看 OData 网站上的所有 OData 筛选器运算符
在示例应用程序中,FilterResultsInputModel
对象旨在捕获用户提供的任何筛选条件。
public class FilterResultsInputModel implements Serializable {
private String partitionKey;
private String rowKeyDateStart;
private String rowKeyTimeStart;
private String rowKeyDateEnd;
private String rowKeyTimeEnd;
private Double minTemperature;
private Double maxTemperature;
private Double minPrecipitation;
private Double maxPrecipitation;
public String getPartitionKey() {
return partitionKey;
}
public void setPartitionKey(String partitionKey) {
this.partitionKey = partitionKey;
}
public String getRowKeyDateStart() {
return rowKeyDateStart;
}
public void setRowKeyDateStart(String rowKeyDateStart) {
this.rowKeyDateStart = rowKeyDateStart;
}
public String getRowKeyTimeStart() {
return rowKeyTimeStart;
}
public void setRowKeyTimeStart(String rowKeyTimeStart) {
this.rowKeyTimeStart = rowKeyTimeStart;
}
public String getRowKeyDateEnd() {
return rowKeyDateEnd;
}
public void setRowKeyDateEnd(String rowKeyDateEnd) {
this.rowKeyDateEnd = rowKeyDateEnd;
}
public String getRowKeyTimeEnd() {
return rowKeyTimeEnd;
}
public void setRowKeyTimeEnd(String rowKeyTimeEnd) {
this.rowKeyTimeEnd = rowKeyTimeEnd;
}
public Double getMinTemperature() {
return minTemperature;
}
public void setMinTemperature(Double minTemperature) {
this.minTemperature = minTemperature;
}
public Double getMaxTemperature() {
return maxTemperature;
}
public void setMaxTemperature(Double maxTemperature) {
this.maxTemperature = maxTemperature;
}
public Double getMinPrecipitation() {
return minPrecipitation;
}
public void setMinPrecipitation(Double minPrecipitation) {
this.minPrecipitation = minPrecipitation;
}
public Double getMaxPrecipitation() {
return maxPrecipitation;
}
public void setMaxPrecipitation(Double maxPrecipitation) {
this.maxPrecipitation = maxPrecipitation;
}
}
将此对象传递到 TableServiceImpl
类中的 retrieveEntitiesByFilter
方法时,它会为每个非 null 属性值创建一个筛选器字符串。 它随后会使用“and”子句将所有值联接在一起,以创建合并筛选器字符串。 此合并筛选器字符串会传递到 TableClient 对象上的 listEntities 方法,仅返回与筛选器字符串匹配的行。 可以在代码中使用类似方法,根据应用程序的要求构造合适的筛选器字符串。
public List<WeatherDataModel> retrieveEntitiesByFilter(FilterResultsInputModel model) {
List<String> filters = new ArrayList<>();
if (!StringUtils.isEmptyOrWhitespace(model.getPartitionKey())) {
filters.add(String.format("PartitionKey eq '%s'", model.getPartitionKey()));
}
if (!StringUtils.isEmptyOrWhitespace(model.getRowKeyDateStart())
&& !StringUtils.isEmptyOrWhitespace(model.getRowKeyTimeStart())) {
filters.add(String.format("RowKey ge '%s %s'", model.getRowKeyDateStart(), model.getRowKeyTimeStart()));
}
if (!StringUtils.isEmptyOrWhitespace(model.getRowKeyDateEnd())
&& !StringUtils.isEmptyOrWhitespace(model.getRowKeyTimeEnd())) {
filters.add(String.format("RowKey le '%s %s'", model.getRowKeyDateEnd(), model.getRowKeyTimeEnd()));
}
if (model.getMinTemperature() != null) {
filters.add(String.format("Temperature ge %f", model.getMinTemperature()));
}
if (model.getMaxTemperature() != null) {
filters.add(String.format("Temperature le %f", model.getMaxTemperature()));
}
if (model.getMinPrecipitation() != null) {
filters.add(String.format("Precipitation ge %f", model.getMinPrecipitation()));
}
if (model.getMaxPrecipitation() != null) {
filters.add(String.format("Precipitation le %f", model.getMaxPrecipitation()));
}
List<WeatherDataModel> modelList = tableClient.listEntities(new ListEntitiesOptions()
.setFilter(String.join(" and ", filters)), null, null).stream()
.map(WeatherDataUtils::mapTableEntityToWeatherDataModel)
.collect(Collectors.toList());
return Collections.unmodifiableList(WeatherDataUtils.filledValue(modelList));
}
使用 TableEntity 对象插入数据
将数据添加到表的最简单方法是使用 TableEntity 对象。 在此示例中,数据会从输入模型对象映射到 TableEntity 对象。 输入对象中表示气象站名称和观察日期/时间的属性分别映射到 PartitionKey
和 RowKey
属性,这些属性共同构成表中行的唯一键。 输入模型对象上的其他属性随后会映射到 TableClient 对象上的字典属性。 最后,TableClient 对象上的 createEntity 方法用于将数据插入表中。
修改示例应用程序中的 insertEntity
类,以包含以下代码。
public void insertEntity(WeatherInputModel model) {
tableClient.createEntity(WeatherDataUtils.createTableEntity(model));
}
使用 TableEntity 对象更新插入数据
如果尝试向表中插入的行具有该表中已存在的分区键/行键组合,则会收到错误。 因此,在向表添加行时,通常最好使用 upsertEntity 而不是 insertEntity
方法。 如果表中已存在给定分区键/行键组合,则 upsertEntity 方法会更新现有行。 否则,行会添加到表中。
public void upsertEntity(WeatherInputModel model) {
tableClient.upsertEntity(WeatherDataUtils.createTableEntity(model));
}
使用变量属性插入或更新插入数据
使用 Azure Cosmos DB 表 API 的一个好处是,如果要加载到表的对象包含任何新属性,那这些属性会自动添加到表中并且值存储在 Azure Cosmos DB 中。 无需如同传统数据库中一样,运行 ALTER TABLE
等 DDL 语句来添加列。
在处理可能会随着时间推移添加或修改需要捕获的数据的数据源时,或者在不同的输入向应用程序提供不同的数据时,此模型可使应用程序具有灵活性。 在示例应用程序中,我们可以模拟一个不仅发送基本天气数据,而且还发送一些附加值的气象站。 首次将具有这些新属性的对象存储在表中时,对应属性(列)会自动添加到表中。
在示例应用程序中,ExpandableWeatherObject
类围绕内部字典进行构建,以支持对象上的任何属性集。 此类表示对象需要包含任意属性集时的典型模式。
public class ExpandableWeatherObject {
private String stationName;
private String observationDate;
private Map<String, Object> propertyMap = new HashMap<String, Object>();
public String getStationName() {
return stationName;
}
public void setStationName(String stationName) {
this.stationName = stationName;
}
public String getObservationDate() {
return observationDate;
}
public void setObservationDate(String observationDate) {
this.observationDate = observationDate;
}
public Map<String, Object> getPropertyMap() {
return propertyMap;
}
public void setPropertyMap(Map<String, Object> propertyMap) {
this.propertyMap = propertyMap;
}
public boolean containsProperty(String key) {
return this.propertyMap.containsKey(key);
}
public Object getPropertyValue(String key) {
return containsProperty(key) ? this.propertyMap.get(key) : null;
}
public void putProperty(String key, Object value) {
this.propertyMap.put(key, value);
}
public List<String> getPropertyKeys() {
List<String> list = Collections.synchronizedList(new ArrayList<String>());
Iterator<String> iterators = this.propertyMap.keySet().iterator();
while (iterators.hasNext()) {
list.add(iterators.next());
}
return Collections.unmodifiableList(list);
}
public Integer getPropertyCount() {
return this.propertyMap.size();
}
}
若要使用表 API 插入或更新插入这种对象,请将可扩展对象的属性映射到 TableEntity 对象,并相应地使用 TableClient 对象上的 createEntity 或 upsertEntity 方法。
public void insertExpandableEntity(ExpandableWeatherObject model) {
tableClient.createEntity(WeatherDataUtils.createTableEntity(model));
}
public void upsertExpandableEntity(ExpandableWeatherObject model) {
tableClient.upsertEntity(WeatherDataUtils.createTableEntity(model));
}
更新条目
可以通过对 TableClient 对象调用 updateEntity 方法来更新实体。 由于使用表 API 存储的实体(行)可以包含任意属性集,因此创建基于字典对象的更新对象(类似于前面讨论的 ExpandableWeatherObject
)通常很有用。 在此情况下,唯一的区别是添加 etag
属性,该属性用于在更新期间进行并发控制。
public class UpdateWeatherObject {
private String stationName;
private String observationDate;
private String etag;
private Map<String, Object> propertyMap = new HashMap<String, Object>();
public String getStationName() {
return stationName;
}
public void setStationName(String stationName) {
this.stationName = stationName;
}
public String getObservationDate() {
return observationDate;
}
public void setObservationDate(String observationDate) {
this.observationDate = observationDate;
}
public String getEtag() {
return etag;
}
public void setEtag(String etag) {
this.etag = etag;
}
public Map<String, Object> getPropertyMap() {
return propertyMap;
}
public void setPropertyMap(Map<String, Object> propertyMap) {
this.propertyMap = propertyMap;
}
}
在示例应用中,此对象会传递到 TableServiceImpl
类中的 updateEntity
方法。 此方法首先对 TableClient 使用 getEntity 方法,从表 API 加载现有实体。 它随后更新该实体对象,并使用 updateEntity
方法将更新保存到数据库。 请注意 updateEntity 方法如何获取对象的当前 Etag,以确保对象自最初加载以来未发生更改。 如果希望无论如何都更新实体,则可以将 etag
值传递到 updateEntity
方法。
public void updateEntity(UpdateWeatherObject model) {
TableEntity tableEntity = tableClient.getEntity(model.getStationName(), model.getObservationDate());
Map<String, Object> propertiesMap = model.getPropertyMap();
propertiesMap.keySet().forEach(key -> tableEntity.getProperties().put(key, propertiesMap.get(key)));
tableClient.updateEntity(tableEntity);
}
删除实体
若要从表中删除实体,请使用对象的分区键和行键对 TableClient 对象调用 deleteEntity 方法。
public void deleteEntity(WeatherInputModel model) {
tableClient.deleteEntity(model.getStationName(),
WeatherDataUtils.formatRowKey(model.getObservationDate(), model.getObservationTime()));
}
7 - 运行代码
运行示例应用程序以与 Azure Cosmos DB 表 API 交互。 首次运行应用程序时没有数据,因为表为空。 使用应用程序顶部的任何按钮将数据添加到表。
选择“使用表实体插入”按钮会打开一个对话框,使你可以使用 TableEntity
对象插入或更新插入新行。
选择“使用可扩展数据插入”按钮后,将会打开一个对话框,在其中可以插入具有自定义属性的对象,并演示 Azure Cosmos DB 表 API 如何根据需要自动将属性(列)添加到表中。 使用“添加自定义字段”按钮添加一个或多个新属性并演示此功能。
使用“插入示例数据”按钮将一些示例数据加载到 Azure Cosmos DB 表中。
选择顶部菜单中的“筛选结果”项以进入“筛选结果”页面。 在此页面上,填写筛选条件以演示如何构建筛选子句并传递到 Azure Cosmos DB 表 API。
清理资源
完成示例应用程序后,应从 Azure 帐户中删除与本文相关的所有 Azure 资源。 可以通过删除资源组来进行这种清理。
可以在 Azure 门户中执行以下操作来删除资源组。
说明 | 屏幕快照 |
---|---|
若要转到资源组,在搜索栏中键入资源组的名称。 然后在“资源组”选项卡上,选择资源组的名称。 | |
从“资源组”页顶部的工具栏中选择“删除资源组”。 | |
屏幕右侧将弹出一个对话框,要求确认删除资源组。
|
后续步骤
在本快速入门教程中,已了解如何创建 Azure Cosmos DB 帐户、使用数据资源管理器创建表和运行应用。 现在可以使用 API for Table 进行数据查询了。