Início Rápido: Criar uma API para a aplicação Tabela com o SDK Java e o Azure Cosmos DB
APLICA-SE A: Tabela
Este início rápido mostra como aceder à API de Tabelas do Azure Cosmos DB a partir de uma aplicação Java. A API de Tabelas do Azure Cosmos DB é um arquivo de dados sem esquemas que permite que as aplicações armazenem dados NoSQL estruturados na cloud. Uma vez que os dados são armazenados num design sem esquema, são adicionadas automaticamente novas propriedades (colunas) à tabela quando um objeto com um novo atributo é adicionado à tabela.
As aplicações Java podem aceder à API de Tabelas do Azure Cosmos DB com a biblioteca de cliente azure-data-tables .
Pré-requisitos
A aplicação de exemplo é escrita no Spring Boot 2.6.4, pode utilizar o Visual Studio Code ou o 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
A aplicação de exemplo para este tutorial pode ser clonada ou transferida a partir do repositório https://github.com/Azure-Samples/msdocs-azure-data-tables-sdk-java. Tanto uma aplicação inicial como uma aplicação concluída estão incluídas no repositório de exemplo.
git clone https://github.com/Azure-Samples/msdocs-azure-data-tables-sdk-java
A aplicação de exemplo utiliza dados meteorológicos como exemplo para demonstrar as capacidades da API de Tabelas. Os objetos que representam observações meteorológicas são armazenados e obtidos com a API para Tabela, incluindo o armazenamento de objetos com propriedades adicionais para demonstrar as capacidades sem esquema da API de Tabelas.
1 - Criar uma conta do Azure Cosmos DB
Primeiro, tem de criar uma conta da API de Tabelas do Azure Cosmos DB que irá conter as tabelas utilizadas na sua aplicação. Isto pode ser feito com o portal do Azure, a CLI do Azure ou Azure PowerShell.
Inicie sessão no portal do Azure e siga estes passos para criar uma conta do Azure Cosmos DB.
2 - Criar uma tabela
Em seguida, tem de criar uma tabela na sua conta do Azure Cosmos DB para a sua aplicação utilizar. Ao contrário de uma base de dados tradicional, só precisa de especificar o nome da tabela e não as propriedades (colunas) na tabela. À medida que os dados são carregados para a tabela, as propriedades (colunas) serão criadas automaticamente conforme necessário.
Na portal do Azure, conclua os seguintes passos para criar uma tabela dentro da sua conta do Azure Cosmos DB.
3 - Obter a cadeia de ligação do Azure Cosmos DB
Para aceder às tabelas no Azure Cosmos DB, a aplicação precisará da cadeia de ligação da tabela para a conta de Armazenamento do CosmosDB. A cadeia de ligação pode ser obtida com o portal do Azure, a CLI do Azure ou Azure PowerShell.
A cadeia de ligação da sua conta do Azure Cosmos DB é considerada um segredo da aplicação e tem de ser protegida como qualquer outro segredo ou palavra-passe da aplicação. Este exemplo utiliza o POM para armazenar a cadeia de ligação durante o desenvolvimento e disponibilizá-la à aplicação.
<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 aceder à API de Tabelas do Azure Cosmos DB a partir de uma aplicação Java, inclua o pacote azure-data-tables .
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-data-tables</artifactId>
<version>12.2.1</version>
</dependency>
5 - Configurar o cliente tabela em TableServiceConfig.java
O SDK do Azure comunica com o Azure com objetos de cliente para executar operações diferentes no Azure. O objeto TableClient é o objeto utilizado para comunicar com a API de Tabelas do Azure Cosmos DB.
Normalmente, uma aplicação cria um único objeto TableClient por tabela para ser utilizada em toda a aplicação. Recomenda-se que indique que um método produz um objeto TableClient bean para ser gerido pelo contentor Spring e como singleton para o conseguir.
TableServiceConfig.java
No ficheiro da aplicação, edite o tableClientConfiguration()
método para corresponder ao seguinte fragmento 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();
}
}
Também terá de adicionar a seguinte instrução using na parte superior do TableServiceConfig.java
ficheiro.
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 a aplicação de exemplo são implementadas na TablesServiceImpl
classe localizada no diretório Serviços . Terá de importar o com.azure.data.tables
pacote do 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 denominado listEntities que lhe permite selecionar linhas da tabela. Neste exemplo, uma vez que não estão a ser transmitidos parâmetros para o método , todas as linhas serão selecionadas na tabela.
O método também utiliza um parâmetro genérico do tipo TableEntity que especifica que os dados da classe de modelo serão devolvidos como. Neste caso, é utilizada a classe incorporada TableEntity , o que significa que o listEntities
método devolverá uma PagedIterable<TableEntity>
coleção como 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 os valores da chave de partição e da chave de linha na tabela. Em conjunto, estes 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, humidade, velocidade do vento) são armazenadas num dicionário no TableEntity
objeto.
É prática comum mapear um objeto TableEntity para um objeto da sua própria definição. A aplicação de exemplo define uma classe WeatherDataModel
no diretório Modelos para esta finalidade. Esta classe tem propriedades para o nome da estação e a data de observação a que a chave de partição e a chave de linha serão mapeados, fornecendo nomes de propriedade mais significativos para estes valores. Em seguida, utiliza um dicionário para armazenar todas as outras propriedades no objeto. Este é um padrão comum ao trabalhar com o Armazenamento de tabelas, uma vez que uma linha pode ter qualquer número de propriedades arbitrárias e queremos que os nossos objetos de modelo sejam capazes de capturar todas elas. Esta 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 é utilizado para mapear um objeto TableEntity para um WeatherDataModel
objeto. O mapTableEntityToWeatherDataModel
método mapeia diretamente as PartitionKey
propriedades , RowKey
, Timestamp
e e Etag
, em seguida, utiliza 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 código no mapTableEntityToWeatherDataModel
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 devolvidas de uma tabela
Para filtrar as linhas devolvidas de uma tabela, pode transmitir uma cadeia de filtro de estilo OData para o método listEntities . Por exemplo, se quisesse obter todas as leituras meteorológicas de Chicago entre a meia-noite de 1 de julho de 2021 e a meia-noite de 2 de julho de 2021 (inclusive), passaria a seguinte cadeia de filtro.
PartitionKey eq 'Chicago' and RowKey ge '2021-07-01 12:00 AM' and RowKey le '2021-07-02 12:00 AM'
Pode ver todos os operadores de filtro OData no site OData na secção Opção Filtrar Consulta do Sistema
Na aplicação de exemplo, o FilterResultsInputModel
objeto foi concebido para capturar os critérios de filtro fornecidos pelo utilizador.
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 este objeto é transmitido para o retrieveEntitiesByFilter
método na TableServiceImpl
classe , cria uma cadeia de filtro para cada valor de propriedade não nulo. Em seguida, cria uma cadeia de filtro combinada ao associar todos os valores com uma cláusula "e". Esta cadeia de filtro combinada é transmitida para o método listEntities no objeto TableClient e só serão devolvidas linhas que correspondam à cadeia de filtro. Pode utilizar um método semelhante no seu código para construir cadeias de filtro adequadas, conforme exigido pela sua aplicação.
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 com um objeto TableEntity
A forma mais simples de adicionar dados a uma tabela é através de 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 representa o nome da estação meteorológica e a data/hora de observação são mapeadas para as PartitionKey
propriedades e RowKey
), respetivamente, que, em conjunto, formam uma chave exclusiva para a linha na tabela. Em seguida, as propriedades adicionais no objeto de modelo de entrada são mapeadas para as propriedades do dicionário no objeto TableClient . Por fim, o método createEntity no objeto TableClient é utilizado para inserir dados na tabela.
Modifique a insertEntity
classe na aplicação de exemplo para conter o seguinte código.
public void insertEntity(WeatherInputModel model) {
tableClient.createEntity(WeatherDataUtils.createTableEntity(model));
}
Upsert data using a TableEntity object (Upsert data using a TableEntity object)
Se tentar inserir uma linha numa tabela com uma combinação de teclas de chave de partição/linha que já existe nessa tabela, receberá um erro. Por este motivo, muitas vezes é preferível utilizar upsertEntity em vez do insertEntity
método ao adicionar linhas a uma tabela. Se a combinação de teclas de linha/chave de partição especificada 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 upsertar dados com propriedades variáveis
Uma das vantagens de utilizar a API de Tabelas do Azure Cosmos DB é que, se um objeto a ser carregado para uma tabela contiver novas propriedades, essas propriedades são adicionadas automaticamente à tabela e aos valores armazenados no Azure Cosmos DB. Não é necessário executar instruções DDL como ALTER TABLE
adicionar colunas como numa base de dados tradicional.
Este modelo dá flexibilidade à sua aplicação ao lidar com origens de dados que podem adicionar ou modificar os dados que precisam de ser capturados ao longo do tempo ou quando entradas diferentes fornecem dados diferentes à sua aplicação. Na aplicação de exemplo, podemos simular uma estação meteorológica que envia não só os dados meteorológicos de base, mas também alguns valores adicionais. Quando um objeto com estas novas propriedades é armazenado na tabela pela primeira vez, as propriedades correspondentes (colunas) serão automaticamente adicionadas à tabela.
Na aplicação de exemplo, a ExpandableWeatherObject
classe é criada em torno de um dicionário interno para suportar qualquer conjunto de propriedades no objeto. Esta classe representa um padrão típico para quando um objeto precisa de 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 upser tal objeto com a API para Tabela, mapeie as propriedades do objeto expansível para um objeto TableEntity e utilize os métodos createEntity ou upsertEntity no objeto TableClient conforme adequado.
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 ao chamar o método updateEntity no objeto TableClient . Uma vez que uma entidade (linha) armazenada com a API de Tabelas pode conter qualquer conjunto arbitrário de propriedades, muitas vezes é útil criar um objeto de atualização baseado num objeto de dicionário semelhante ao ExpandableWeatherObject
abordado anteriormente. Neste caso, a única diferença é a adição de uma etag
propriedade que é utilizada para o controlo 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;
}
}
Na aplicação de exemplo, este objeto é transmitido para o updateEntity
método na TableServiceImpl
classe . Primeiro, este método carrega a entidade existente a partir da API de Tabelas com o método getEntity no TableClient. Em seguida, atualiza esse objeto de entidade e utiliza o updateEntity
método para guardar as atualizações na base de dados. Tenha em atenção como o método updateEntity utiliza o Etag atual do objeto para garantir que o objeto não foi alterado desde que foi inicialmente carregado. Se quiser atualizar a entidade independentemente disso, poderá transmitir 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 - Executar o código
Execute a aplicação de exemplo para interagir com a API de Tabelas do Azure Cosmos DB. Quando executar a aplicação pela primeira vez, não haverá dados porque a tabela está vazia. Utilize qualquer um dos botões na parte superior da aplicação para adicionar dados à tabela.
Selecionar o botão Inserir com a Entidade de Tabela abre uma caixa de diálogo que lhe permite inserir ou inserir uma nova linha com um TableEntity
objeto.
Selecionar o botão Inserir com Dados Expansíveis apresenta uma caixa de diálogo que lhe 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. Utilize o botão Adicionar Campo Personalizado para adicionar uma ou mais novas propriedades e demonstrar esta capacidade.
Utilize o botão Inserir Dados de Exemplo para carregar alguns dados de exemplo para a tabela do Azure Cosmos DB.
Selecione o item Filtrar Resultados no menu superior a ser levado para a página Filtrar Resultados. Nesta página, preencha os critérios de filtro para demonstrar como uma cláusula de filtro pode ser criada e transmitida à API de Tabelas do Azure Cosmos DB.
Limpar os recursos
Quando tiver terminado a aplicação de exemplo, deverá remover todos os recursos do Azure relacionados com este artigo da sua conta do Azure. Pode fazê-lo ao eliminar o grupo de recursos.
Um grupo de recursos pode ser eliminado com a portal do Azure ao fazer o seguinte.
Passos seguintes
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, pode consultar os seus dados com a API para Tabela.