Guia de início rápido: criar uma API para o aplicativo Table com o Java SDK e o Azure Cosmos DB
APLICA-SE A: Tabela
Este guia de início rápido mostra como acessar a API de Tabelas do Azure Cosmos DB a partir de um aplicativo Java. A API de Tabelas do Azure Cosmos DB é um armazenamento de dados sem esquema que permite que os aplicativos armazenem dados NoSQL estruturados na nuvem. Como os dados são armazenados em um design sem esquema, novas propriedades (colunas) são adicionadas automaticamente à tabela quando um objeto com um novo atributo é adicionado à tabela.
Os aplicativos Java podem acessar a API de Tabelas do Azure Cosmos DB usando a biblioteca de cliente azure-data-tables .
Pré-requisitos
O aplicativo de exemplo é escrito em Spring Boot 2.6.4, Você pode usar o Visual Studio Code ou IntelliJ IDEA como um IDE.
Se não tiver uma subscrição do Azure, crie uma conta gratuita antes de começar.
Aplicação de exemplo
O aplicativo de exemplo para este tutorial pode ser clonado ou baixado do repositório https://github.com/Azure-Samples/msdocs-azure-data-tables-sdk-java. Tanto um aplicativo inicial quanto um aplicativo concluído são incluídos no repositório de exemplo.
git clone https://github.com/Azure-Samples/msdocs-azure-data-tables-sdk-java
O aplicativo de exemplo usa dados meteorológicos como exemplo para demonstrar os recursos da API de tabelas. Os objetos que representam observações meteorológicas são armazenados e recuperados usando a API para Tabela, incluindo o armazenamento de objetos com propriedades adicionais para demonstrar os recursos sem esquema da API Tabelas.
1 - Criar uma conta do Azure Cosmos DB
Primeiro, você precisa criar uma conta da API de Tabelas do Azure Cosmos DB que conterá a(s) tabela(s) usada(s) em seu aplicativo. Isso pode ser feito usando o portal do Azure, a CLI do Azure ou o Azure PowerShell.
Entre no portal do Azure e siga estas etapas para criar uma conta do Azure Cosmos DB.
2 - Criar uma tabela
Em seguida, você precisa criar uma tabela em sua conta do Azure Cosmos DB para seu aplicativo usar. Ao contrário de um banco de dados tradicional, você só precisa especificar o nome da tabela, não as propriedades (colunas) na tabela. À medida que os dados são carregados na tabela, as propriedades (colunas) serão criadas automaticamente conforme necessário.
No portal do Azure, conclua as etapas a seguir para criar uma tabela dentro de sua conta do Azure Cosmos DB.
3 - Obter a cadeia de conexão do Azure Cosmos DB
Para acessar sua(s) tabela(s) no Azure Cosmos DB, seu aplicativo precisará da cadeia de conexão de tabela para a conta de Armazenamento do CosmosDB. A cadeia de conexão pode ser recuperada usando o portal do Azure, a CLI do Azure ou o Azure PowerShell.
A cadeia de conexão para sua conta do Azure Cosmos DB é considerada um segredo de aplicativo e deve ser protegida como qualquer outro segredo ou senha de aplicativo. Este exemplo usa o POM para armazenar a cadeia de conexão durante o desenvolvimento e disponibilizá-la para o aplicativo.
<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 - Incluir o pacote azure-data-tables
Para acessar a API de Tabelas do Azure Cosmos DB a partir de um aplicativo Java, inclua o pacote azure-data-tables .
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-data-tables</artifactId>
<version>12.2.1</version>
</dependency>
5 - Configure o cliente Table no TableServiceConfig.java
O SDK do Azure se comunica com o Azure usando objetos de cliente para executar operações diferentes no Azure. O objeto TableClient é o objeto usado para se comunicar com a API de Tabelas do Azure Cosmos DB.
Um aplicativo normalmente criará um único objeto TableClient por tabela para ser usado em todo o aplicativo. É recomendável indicar que um método produz um bean de objeto TableClient para ser gerenciado pelo contêiner Spring e como um singleton para fazer isso.
TableServiceConfig.java
No arquivo do aplicativo, edite o tableClientConfiguration()
método para corresponder ao seguinte trecho de código:
@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();
}
}
Você também precisará adicionar a seguinte instrução using na parte superior do TableServiceConfig.java
arquivo.
import com.azure.data.tables.TableClient;
import com.azure.data.tables.TableClientBuilder;
6 - Implementar operações de tabela do Azure Cosmos DB
Todas as operações de tabela do Azure Cosmos DB para o aplicativo de exemplo são implementadas na TablesServiceImpl
classe localizada no diretório Serviços . Você precisará importar o com.azure.data.tables
pacote 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;
No início da TableServiceImpl
classe, adicione uma variável de membro para o objeto TableClient e um construtor para permitir que o objeto TableClient seja injetado na classe.
@Autowired
private TableClient tableClient;
Obter linhas de uma tabela
A classe TableClient contém um método chamado listEntities que permite selecionar linhas da tabela. Neste exemplo, como nenhum parâmetro está sendo passado para o método, todas as linhas serão selecionadas na tabela.
O método também usa um parâmetro genérico do tipo TableEntity que especifica que os dados da classe do modelo serão retornados como. Nesse caso, a classe interna TableEntity é usada, o que significa que o listEntities
método retornará uma PagedIterable<TableEntity>
coleção como seus resultados.
public List<WeatherDataModel> retrieveAllEntities() {
List<WeatherDataModel> modelList = tableClient.listEntities().stream()
.map(WeatherDataUtils::mapTableEntityToWeatherDataModel)
.collect(Collectors.toList());
return Collections.unmodifiableList(WeatherDataUtils.filledValue(modelList));
}
A classe TableEntity definida no com.azure.data.tables.models
pacote tem propriedades para a chave de partição e valores de chave de linha na tabela. Juntos, esses dois valores para uma chave exclusiva para a linha na tabela. Neste exemplo de aplicação, o nome da estação meteorológica (cidade) é armazenado na chave de partição e a data/hora da observação é armazenada na chave de linha. Todas as outras propriedades (temperatura, umidade, velocidade do vento) são armazenadas em um dicionário no TableEntity
objeto.
É prática comum mapear um objeto TableEntity para um objeto de sua própria definição. O aplicativo de exemplo define uma classe WeatherDataModel
no diretório Models para essa finalidade. Essa classe tem propriedades para o nome da estação e a data de observação para as quais a chave de partição e a chave de linha serão mapeadas, fornecendo nomes de propriedade mais significativos para esses valores. Em seguida, ele usa um dicionário para armazenar todas as outras propriedades no objeto. Esse é um padrão comum ao trabalhar com o armazenamento de tabela, uma vez que uma linha pode ter qualquer número de propriedades arbitrárias e queremos que nossos objetos de modelo sejam capazes de capturar todas elas. Essa classe também contém métodos para listar as propriedades na classe.
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;
}
}
O mapTableEntityToWeatherDataModel
método é usado para mapear um objeto TableEntity para um WeatherDataModel
objeto. O mapTableEntityToWeatherDataModel
método mapeia diretamente as PartitionKey
propriedades , RowKey
, Timestamp
e Etag
e, em seguida, usa o properties.keySet
para iterar sobre as outras propriedades no TableEntity
objeto e mapeá-las para o WeatherDataModel
objeto, menos as propriedades que já foram mapeadas diretamente.
Edite o mapTableEntityToWeatherDataModel
código no método para corresponder ao seguinte bloco de código.
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));
}
});
}
Filtrar linhas retornadas de uma tabela
Para filtrar as linhas retornadas de uma tabela, você pode passar uma cadeia de caracteres de filtro de estilo OData para o método listEntities . Por exemplo, se você quisesse obter todas as leituras do tempo para Chicago entre a meia-noite de 1º de julho de 2021 e a meia-noite de 2 de julho de 2021 (inclusive), você passaria na seguinte cadeia de filtros.
PartitionKey eq 'Chicago' and RowKey ge '2021-07-01 12:00 AM' and RowKey le '2021-07-02 12:00 AM'
Você pode exibir todos os operadores de filtro OData no site OData na seção Opção de consulta do sistema de filtro
No aplicativo de exemplo, o FilterResultsInputModel
objeto é projetado para capturar qualquer critério de filtro fornecido pelo usuário.
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;
}
}
Quando esse objeto é passado para o retrieveEntitiesByFilter
método na TableServiceImpl
classe, ele cria uma cadeia de caracteres de filtro para cada valor de propriedade não nula. Em seguida, ele cria uma cadeia de caracteres de filtro combinada unindo todos os valores com uma cláusula "e". Essa cadeia de caracteres de filtro combinada é passada para o método listEntities no objeto TableClient e somente as linhas correspondentes à cadeia de caracteres de filtro serão retornadas. Você pode usar um método semelhante em seu código para construir cadeias de caracteres de filtro adequadas, conforme exigido pelo seu aplicativo.
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));
}
Inserir dados usando um objeto TableEntity
A maneira mais simples de adicionar dados a uma tabela é usando um objeto TableEntity . Neste exemplo, os dados são mapeados de um objeto de modelo de entrada para um objeto TableEntity . As propriedades no objeto de entrada que representam o nome da estação meteorológica e a data/hora de observação são mapeadas para as PartitionKey
propriedades e RowKey
), respectivamente, que juntas formam uma chave exclusiva para a linha na tabela. Em seguida, as propriedades adicionais no objeto de modelo de entrada são mapeadas para propriedades de dicionário no objeto TableClient . Finalmente, o método createEntity no objeto TableClient é usado para inserir dados na tabela.
Modifique a insertEntity
classe no aplicativo de exemplo para conter o código a seguir.
public void insertEntity(WeatherInputModel model) {
tableClient.createEntity(WeatherDataUtils.createTableEntity(model));
}
Dados de upsert usando um objeto TableEntity
Se tentar inserir uma linha numa tabela com uma combinação de chave de partição/chave de linha que já existe nessa tabela, receberá um erro. Por esse motivo, geralmente é preferível usar upsertEntity em vez do insertEntity
método ao adicionar linhas a uma tabela. Se a combinação de chave de partição/chave de linha já existir na tabela, o método upsertEntity atualizará a linha existente. Caso contrário, a linha será adicionada à tabela.
public void upsertEntity(WeatherInputModel model) {
tableClient.upsertEntity(WeatherDataUtils.createTableEntity(model));
}
Inserir ou atualizar dados com propriedades variáveis
Uma das vantagens de usar a API de Tabelas do Azure Cosmos DB é que, se um objeto que está sendo carregado em uma tabela contiver novas propriedades, essas propriedades serão adicionadas automaticamente à tabela e aos valores armazenados no Azure Cosmos DB. Não há necessidade de executar instruções DDL como ALTER TABLE
adicionar colunas como em um banco de dados tradicional.
Esse modelo dá flexibilidade ao seu aplicativo ao lidar com fontes de dados que podem adicionar ou modificar quais dados precisam ser capturados ao longo do tempo ou quando entradas diferentes fornecem dados diferentes para seu aplicativo. Na aplicação de exemplo, podemos simular uma estação meteorológica que envia não apenas os dados meteorológicos base, mas também alguns valores adicionais. Quando um objeto com essas novas propriedades é armazenado na tabela pela primeira vez, as propriedades correspondentes (colunas) serão adicionadas automaticamente à tabela.
No aplicativo de exemplo, a ExpandableWeatherObject
classe é criada em torno de um dicionário interno para oferecer suporte a qualquer conjunto de propriedades no objeto. Essa classe representa um padrão típico para quando um objeto precisa conter um conjunto arbitrário de propriedades.
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();
}
}
Para inserir ou atualizar esse objeto usando a API para Table, mapeie as propriedades do objeto expansível em um objeto TableEntity e use os métodos createEntity ou upsertEntity no objeto TableClient conforme apropriado.
public void insertExpandableEntity(ExpandableWeatherObject model) {
tableClient.createEntity(WeatherDataUtils.createTableEntity(model));
}
public void upsertExpandableEntity(ExpandableWeatherObject model) {
tableClient.upsertEntity(WeatherDataUtils.createTableEntity(model));
}
Atualizar uma entidade
As entidades podem ser atualizadas chamando o método updateEntity no objeto TableClient. Como uma entidade (linha) armazenada usando a API Tables pode conter qualquer conjunto arbitrário de propriedades, geralmente é útil criar um objeto de atualização com base em um objeto de dicionário semelhante ao discutido ExpandableWeatherObject
anteriormente. Nesse caso, a única diferença é a adição de uma etag
propriedade que é usada para controle de simultaneidade durante as atualizações.
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;
}
}
No aplicativo de exemplo, esse objeto é passado para o updateEntity
método na TableServiceImpl
classe. Esse método primeiro carrega a entidade existente da API Tables usando o método getEntity no TableClient. Em seguida, ele atualiza esse objeto de entidade e usa o updateEntity
método para salvar as atualizações no banco de dados. Observe como o método updateEntity usa o Etag atual do objeto para garantir que o objeto não tenha sido alterado desde que foi carregado inicialmente. Se você quiser atualizar a entidade independentemente disso, você pode passar um valor de etag
para o updateEntity
método.
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);
}
Remover uma entidade
Para remover uma entidade de uma tabela, chame o método deleteEntity no objeto TableClient com a chave de partição e a chave de linha do objeto.
public void deleteEntity(WeatherInputModel model) {
tableClient.deleteEntity(model.getStationName(),
WeatherDataUtils.formatRowKey(model.getObservationDate(), model.getObservationTime()));
}
7 - Execute o código
Execute o aplicativo de exemplo para interagir com a API de Tabelas do Azure Cosmos DB. Na primeira vez que você executar o aplicativo, não haverá dados porque a tabela está vazia. Use qualquer um dos botões na parte superior do aplicativo para adicionar dados à tabela.
Selecionar o botão Inserir usando entidade da tabela abre uma caixa de diálogo que permite inserir ou atualizar uma nova linha usando um TableEntity
objeto.
Selecionar o botão Inserir usando dados expansíveis exibe uma caixa de diálogo que permite inserir um objeto com propriedades personalizadas, demonstrando como a API de Tabelas do Azure Cosmos DB adiciona automaticamente propriedades (colunas) à tabela quando necessário. Use o botão Adicionar campo personalizado para adicionar uma ou mais novas propriedades e demonstrar esse recurso.
Use o botão Inserir Dados de Exemplo para carregar alguns dados de exemplo em sua tabela do Azure Cosmos DB.
Selecione o item Filtrar Resultados no menu superior para ser direcionado para a página Resultados do Filtro. Nesta página, preencha os critérios de filtro para demonstrar como uma cláusula de filtro pode ser criada e passada para a API de Tabelas do Azure Cosmos DB.
Clean up resources (Limpar recursos)
Quando terminar o aplicativo de exemplo, remova todos os recursos do Azure relacionados a este artigo da sua conta do Azure. Você pode fazer isso excluindo o grupo de recursos.
Um grupo de recursos pode ser excluído usando o portal do Azure fazendo o seguinte.
Próximos passos
Neste guia de introdução, aprendeu a criar uma conta do Azure Cosmos DB, a criar uma tabela com o Data Explorer e a executar uma aplicação. Agora você pode consultar seus dados usando a API para Tabela.