Exercise - Create, read, update, and delete NoSQL data programmatically
You've made the connection to Azure Cosmos DB. In this unit, you'll create user documents in your WebCustomers
collection. Then, you'll retrieve the documents by ID, replace them, and delete them.
Working with documents programmatically
Data is stored in JSON documents in Azure Cosmos DB. Documents can be created, retrieved, replaced, or deleted in the portal or programmatically. This lab focuses on programmatic operations. Azure Cosmos DB provides client-side SDKs for .NET, .NET Core, Java, Node.js, and Python, each of which supports these operations. In this module, we'll use the Java SDK to perform CRUD (create, retrieve, update, and delete) operations on the NoSQL data stored in Azure Cosmos DB.
The main operations for Azure Cosmos DB documents are part of the CosmosAsyncContainer class:
Upsert performs a create or replace operation, depending on whether the document already exists.
To perform any of these operations, you'll need helper classes (Java POJO classes) that represent the objects stored in the database. Because we're working with a database of users, you'll want to have a User
class that represents user entities. This class will store primary data such as their first name, last name, and user ID. (The ID is required because that's the partition key to enable horizontal scaling.)
Each user has some associated shipping preferences and coupons, so you'll want ShippingPreference
and CouponsUsed
datatypes to represent those entities. Finally, each user might have some order history that is potentially unbounded, so you'll want to have separate OrderHistory
entities with a corresponding Java POJO class.
Go to src/main/java/com/azure/azure-cosmos-java-sql-app-mslearn and look in the datatypes folder. You'll see several POJOs: User
, ShippingPreference
, OrderHistory
, and CouponsUsed
. So we've provided all of the entity POJOs and their helper classes!
Next we'll create some entities and perform some basic CRUD operations on the Azure Cosmos DB container and the documents that it contains. You can pass Azure Cosmos DB a Jackson ObjectNode
instance that directly specifies the JSON document. But Azure Cosmos DB is also capable of serializing Java POJOs into JSON, and we recommend this approach as the simplest option (all else being equal).
Create documents
Open User.java and examine its contents. It should look something like this:
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class User { /** Document ID (required by Azure Cosmos DB). */ private String id; /** User ID. */ private String userId; /** User last name. */ private String lastName; /** User first name. */ private String firstName; /** User email address. */ private String email; /** User dividend setting. */ private String dividend; /** User shipping preferences. */ private ShippingPreference shippingPreference; /** User order history. */ private List<OrderHistory> orderHistory; /** Coupons recorded by the user. */ private List<CouponsUsed> coupons; }
Observe that the access methods for the
id
,userId
, and other fields are implicit (not defined in code). This behavior is possible because we use Project Lombok@Data
annotation to create them automatically.The
@NoArgsConstructor
annotation will generate a constructor with no arguments that sets default field values. The@AllArgsConstructor
annotation will generate another constructor with a full set of arguments to specify all field values directly.Note that
User
has anid
property. All Azure Cosmos DB documents require anid
property, so all POJOs we intend to serialize into JSON documents must have anid
field.Add the following method to CosmosApp.java:
/** * Take in list of Java POJOs, check if each exists, and if not insert it. * @param users List of User POJOs to insert. */ private static void createUserDocumentsIfNotExist(final List<User> users) { Flux.fromIterable(users).flatMap(user -> { try { container.readItem(user.getId(), new PartitionKey(user.getUserId()), User.class).block(); logger.info("User {} already exists in the database", user.getId()); return Mono.empty(); } catch (Exception err) { logger.info("Creating User {}", user.getId()); return container.createItem(user, new PartitionKey(user.getUserId()), new CosmosItemRequestOptions()); } }).blockLast(); }
Return to the
basicOperations
method and add the following to the end of that method, before theclient.close()
call.User maxaxam = new User( "1", "maxaxam", "Axam", "Max", "maxaxam@contoso.com", "2.0", new ShippingPreference( 1, "90 W 8th St", "", "New York", "NY", "10001", "USA" ), new ArrayList<OrderHistory>(Arrays.asList( new OrderHistory( "3", "1000", "08/17/2018", "52.49" ) )), new ArrayList<CouponsUsed>(Arrays.asList( new CouponsUsed( "A7B89F" ) )) ); User nelapin = new User( "2", "nelapin", "Pindakova", "Nela", "nelapin@contoso.com", "8.50", new ShippingPreference( 1, "505 NW 5th St", "", "New York", "NY", "10001", "USA" ), new ArrayList<OrderHistory>(Arrays.asList( new OrderHistory( "4", "1001", "08/17/2018", "105.89" ) )), new ArrayList<CouponsUsed>(Arrays.asList( new CouponsUsed( "Fall 2018" ) )) ); createUserDocumentsIfNotExist(new ArrayList<User>(Arrays.asList(maxaxam, nelapin)));
Build and run CosmosApp.java in the IDE, or run the program in the terminal by using:
mvn clean package mvn exec:java -Dexec.mainClass="com.azure.cosmos.examples.mslearnbasicapp.CosmosApp"
The terminal will display output as the application creates each new user document.
INFO: Database and container validation complete INFO: Creating User 1 INFO: Creating User 2
You might see some additional text emitted by the logger, such as timestamps.
Congratulations! You've created your first data in Azure Cosmos DB from a Java application. Let's pause and evaluate what you did here.
In basicOperations
, there are three new actions:
- Create the maxaxam
User
instance. - Create the nelapin
User
instance. - Call
createUserDocumentsIfNotExist
, passing in maxaxam and nelapin in a list.
Calling createUserDocumentsIfNotExist
inserts both User
instances as items/documents in Azure Cosmos DB. In having you pass the User
instances as a list, our intent is to model a performant method for ingesting POJOs quickly into Azure Cosmos DB, using minimum compute resources. createUserDocumentsIfNotExist
implements efficient async insertion of a list of POJOs.
Suppose that our goal is to maximize requests/second per thread. For comparison, the sync approach to writing createUserDocumentsIfNotExist
- ignoring for a moment the readItem
check - would be to iterate over each User
instance in users
. For each User
u
, we'd make a blocking call to createItem
:
container.createItem(u, new PartitionKey(u.getUserId()), new CosmosItemRequestOptions()).block(); // <= Note the .block() which loops until request response.
This sync style implements an intuitive process: issue request, wait for response, issue next request. However, createUserDocumentsIfNotExist
doesn't use this approach because blocking calls will essentially waste CPU cycles during the request response time, causing low requests/second.
You might get around this requests/second issue by spawning multiple threads to make parallel blocking request calls. The multiple threads will bring an execution-time improvement. But if your goal is to be thrifty with thread resources, then this is still wasteful. Each thread loops during request response time when it could instead be multitasking to something else, giving you low requests/second per thread.
For this reason, and for the purpose of showing you thread-efficient insertion of Java POJOs, we have instead provided an async example of document insertion. Azure Cosmos DB Java SDK v4 async support comes from Project Reactor, a Java application framework that provides a stream-based, declarative dataflow programming model for async event-driven programming. createDocumentsIfNotExist
implements Project Reactor async programming.
In createUserDocumentsIfNotExist
, Flux.fromIterable(users)
is a Project Reactor factory method. It creates a Flux
instance that's a source of async events. In this case, each async "event" includes a User
instance argument. The Flux
instance contains two such events, one for maxaxam and one for nelapin. The code inside .flatMap( ... ).blockLast();
defines a pipeline of sequential operations to be performed on the events emitted by the Flux
instance.
One of those operations is createItem
. The idea is that this pipeline is almost identical to the synchronous implementation except that we don't block on the createItem
call. Specifically, the call to blockLast()
subscribes to the assembled pipeline, causing the Flux
to asynchronously emit its two events. Then the pipeline inside .flatMap( ... ).blockLast();
processes each of those events in a pseudo-parallel fashion. When one request is issued and waits for a response, Project Reactor will process other requests in the background, which is the critical factor in maxing out requests/second per thread.
Now that we've demonstrated efficient async database requests with Project Reactor, the rest of this lab will use blocking (sync) calls for simplicity. To learn more about Project Reactor, see the Azure Cosmos DB Reactor Pattern Guide.
Read documents
To read documents from the database, add the following method to
CosmosApp
:/** * Take in a Java POJO argument, extract ID and partition key, and read the corresponding document from the container. * In this case the ID is the partition key. * @param user User POJO to pull ID and partition key from. */ private static CosmosItemResponse<User> readUserDocument(final User user) { CosmosItemResponse<User> userReadResponse = null; try { userReadResponse = container.readItem(user.getId(), new PartitionKey(user.getUserId()), User.class).block(); logger.info("Read user {}", user.getId()); } catch (CosmosException de) { logger.error("Failed to read user {}", user.getId(), de); } return userReadResponse; }
Copy and paste the following code to the end of the
basicOperations
method, after the document creation code:readUserDocument(maxaxam);
Build and run CosmosApp.java in the IDE, or run the program in the terminal by using:
mvn clean package mvn exec:java -Dexec.mainClass="com.azure.cosmos.examples.mslearnbasicapp.CosmosApp"
The terminal displays the following output, where "Read user 1" indicates that the document was retrieved.
INFO: Database and container validation complete INFO: User 1 already exists in the database INFO: User 2 already exists in the database INFO: Read user 1
You might also see some additional text emitted by the logger.
Replace documents
Azure Cosmos DB supports replacing JSON documents. In this case, we'll update a user record to account for a change to their last name.
Add the
replaceUserDocument
method after thereadUserDocument
method in the CosmosApp.java file./** * Take in a Java POJO argument, extract ID and partition key, * and replace the existing document with the same ID and partition key to match. * @param user User POJO representing the document update. */ private static void replaceUserDocument(final User user) { try { CosmosItemResponse<User> userReplaceResponse = container.replaceItem(user, user.getId(), new PartitionKey(user.getUserId())).block(); logger.info("Replaced User {}", user.getId()); } catch (CosmosException de) { logger.error("Failed to replace User {}", user.getUserId()); } }
Copy and paste the following code to the end of the
basicOperations
method, after the document read code.maxaxam.setLastName("Suh"); replaceUserDocument(maxaxam);
Build and run CosmosApp.java in the IDE, or run the program in the terminal by using:
mvn clean package mvn exec:java -Dexec.mainClass="com.azure.cosmos.examples.mslearnbasicapp.CosmosApp"
The terminal displays the following output, where "Replaced last name for Suh" indicates that the document was replaced.
INFO: Database and container validation complete INFO: User 1 already exists in the database INFO: User 2 already exists in the database INFO: Read user 1 INFO: Replaced last name for Suh
Delete documents
Copy and paste the
deleteUserDocument
method under yourreplaceUserDocument
method./** * Take in a Java POJO argument, extract ID and partition key, * and delete the corresponding document. * @param user User POJO representing the document update. */ private static void deleteUserDocument(final User user) { try { container.deleteItem(user.getId(), new PartitionKey(user.getUserId())).block(); logger.info("Deleted user {}", user.getId()); } catch (CosmosException de) { logger.error("User {} could not be deleted.", user.getId()); } }
Copy and paste the following code to the end of the
basicOperations
method.deleteUserDocument(maxaxam);
Build and run CosmosApp.java in the IDE, or run the program in the terminal by using:
mvn clean package mvn exec:java -Dexec.mainClass="com.azure.cosmos.examples.mslearnbasicapp.CosmosApp"
The terminal displays the following output, where "Deleted user 1" indicates that the document was deleted.
INFO: Database and container validation complete INFO: User 1 already exists in the database INFO: User 2 already exists in the database INFO: Read User 1 INFO: Replaced last name for Suh INFO: Deleted User 1
Working with documents programmatically
Data is stored in JSON documents in Azure Cosmos DB. Documents can be created, retrieved, replaced, or deleted in the portal or programmatically. This lab focuses on programmatic operations. All of these operations are available in Azure Cosmos DB Java SDK and are also accessible through the Spring Data programming model. In this module, we'll use Spring Data Azure Cosmos DB to perform CRUD (create, retrieve, update, and delete) operations on the NoSQL data stored in Azure Cosmos DB.
The main operations for Spring Data Azure Cosmos DB documents are basic operations in the Spring Data programming model:
save
- point-write or update a document, depending on whether the document already exists.view
- point-read a documentdelete
- point-delete a document
To perform any of these operations, you'll need helper classes (Java POJO classes) that represent the objects stored in the database. Because we're working with a database of online customers, you'll want to use the WebCustomer
class to represent user entities. This class will store primary data such as their first name, last name, and user ID. (The ID is required because that's the partition key to enable horizontal scaling.)
Each web customer has some associated shipping preferences and coupons, so you'll want ShippingPreference
and CouponsUsed
datatypes to represent those entities. Finally, each web customer might have some order history that is potentially unbounded, so you'll want to have separate OrderHistory
entities with a corresponding Java POJO class.
Go to src/main/java/com/azure/cosmos/examples/springexamples. You will see the WebCustomer
POJO. Now look in the common folder. You'll see several POJOs: ShippingPreference
, OrderHistory
, and CouponsUsed
. So we've provided all of the entity POJOs and their helper classes!
Next we'll create some entities and perform some basic CRUD operations on the Azure Cosmos DB container and the documents that it contains. You can pass Azure Cosmos DB a Jackson ObjectNode
instance that directly specifies the JSON document. But Azure Cosmos DB is also capable of serializing Java POJOs into JSON, and we recommend this approach as the simplest option (all else being equal).
Create and update documents
Open WebCustomer.java and examine its contents. It should look something like this:
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package com.azure.cosmos.examples.springexamples; import com.azure.cosmos.examples.springexamples.common.CouponsUsed; import com.azure.cosmos.examples.springexamples.common.OrderHistory; import com.azure.cosmos.examples.springexamples.common.ShippingPreference; import com.azure.spring.data.cosmos.core.mapping.Container; import com.azure.spring.data.cosmos.core.mapping.PartitionKey; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor @Container(containerName = "WebCustomer", ru = "400") public class WebCustomer { /** Document ID (required by Azure Cosmos DB). */ private String id; /** WebCustomer ID. */ private String userId; /** WebCustomer last name. */ @PartitionKey private String lastName; /** WebCustomer first name. */ private String firstName; /** WebCustomer email address. */ private String email; /** WebCustomer dividend setting. */ private String dividend; /** WebCustomer shipping preferences. */ private ShippingPreference shippingPreference; /** WebCustomer order history. */ private List<OrderHistory> orderHistory; /** Coupons recorded by the user. */ private List<CouponsUsed> coupons; }
Observe that the access methods for the
id
,userId
, and other fields are implicit (not defined in code). This behavior is possible because we use Project Lombok@Data
annotation to create them automatically.The
@NoArgsConstructor
annotation will generate a constructor with no arguments that sets default field values. The@AllArgsConstructor
annotation will generate another constructor with a full set of arguments to specify all field values directly.Note that
WebCustomer
has anid
property. All Azure Cosmos DB documents require anid
property, so all POJOs we intend to serialize into JSON documents must have anid
field.Add the following method to CosmosSample.java:
/** * Take in list of Java POJOs and insert them into the database. * @param webCustomers List of WebCustomer POJOs to insert. */ private void createWebCustomerDocumentsIfNotExist(final List<WebCustomer> webCustomers) { Flux.fromIterable(webCustomers).flatMap(webCustomer -> { logger.info("Creating WebCustomer {}", webCustomer.getId()); return this.reactiveWebCustomerRepository.save(webCustomer); }).blockLast(); }
Find the
run
method and add the following code to the end of that method.WebCustomer maxaxam = new WebCustomer( "1", "maxaxam", "Axam", "Max", "maxaxam@contoso.com", "2.0", new ShippingPreference( 1, "90 W 8th St", "", "New York", "NY", "10001", "USA" ), new ArrayList<OrderHistory>(Arrays.asList( new OrderHistory( "3", "1000", "08/17/2018", "52.49" ) )), new ArrayList<CouponsUsed>(Arrays.asList( new CouponsUsed( "A7B89F" ) )) ); WebCustomer nelapin = new WebCustomer( "2", "nelapin", "Pindakova", "Nela", "nelapin@contoso.com", "8.50", new ShippingPreference( 1, "505 NW 5th St", "", "New York", "NY", "10001", "USA" ), new ArrayList<OrderHistory>(Arrays.asList( new OrderHistory( "4", "1001", "08/17/2018", "105.89" ) )), new ArrayList<CouponsUsed>(Arrays.asList( new CouponsUsed( "Fall 2018" ) )) ); createWebCustomerDocumentsIfNotExist(new ArrayList(Arrays.asList(maxaxam, nelapin)));
Build and run CosmosSample.java in the IDE, or run the program in the terminal by using:
mvn clean package mvn spring-boot:run
Amidst the terminal output, you should see
INFO: Creating WebCustomer 1 INFO: Creating WebCustomer 2
Congratulations! You've created and/or updated your first data in Azure Cosmos DB from a Java application. Let's pause and evaluate what you did here.
In run
, there are three new actions:
- Create/update the maxaxam
WebCustomer
instance. - Create/update the nelapin
WebCustomer
instance. - Call
createWebCustomerDocumentsIfNotExist
, passing in maxaxam and nelapin in a list.
Calling createWebCustomerDocumentsIfNotExist
inserts both WebCustomer
instances as items/documents in Azure Cosmos DB. In having you pass the WebCustomer
instances as a list, our intent is to model a performant method for ingesting POJOs quickly into Azure Cosmos DB, using minimum compute resources. createWebCustomerDocumentsIfNotExist
implements efficient async insertion of a list of POJOs. If either document already exists, then save
will accomplish an update instead of a document create.
Suppose that our goal is to maximize requests/second per thread. For comparison, the sync approach to writing createWebCustomerDocumentsIfNotExist
would be to iterate over each WebCustomer
instance in webCustomers
. For each WebCustomer
webCustomer
, we'd make a blocking call to save
:
this.reactiveWebCustomerRepository.save(webCustomer).block(); // <= Note the .block() which loops until request response.
This sync style implements an intuitive process: issue request, wait for response, issue next request. However, createWebCustomerDocumentsIfNotExist
doesn't use this approach because blocking calls will essentially waste CPU cycles during the request response time, causing low requests/second.
You might get around this requests/second issue by spawning multiple threads to make parallel blocking request calls. The multiple threads will bring an execution-time improvement. But if your goal is to be thrifty with thread resources, then this is still wasteful. Each thread loops during request response time when it could instead be multitasking to something else, giving you low requests/second per thread.
For this reason, and for the purpose of showing you thread-efficient insertion of Java POJOs, we have instead provided an async example of document insertion. Spring Data async support comes from Project Reactor, a Java application framework that provides a stream-based, declarative dataflow programming model for async event-driven programming. createWebCustomerDocumentsIfNotExist
implements Project Reactor async programming.
In createWebCustomerDocumentsIfNotExist
, Flux.fromIterable(webCustomers)
is a Project Reactor factory method. It creates a Flux
instance that's a source of async events. In this case, each async "event" includes a WebCustomer
instance argument. The Flux
instance contains two such events, one for maxaxam and one for nelapin. The code inside .flatMap( ... ).blockLast();
defines a pipeline of sequential operations to be performed on the events emitted by the Flux
instance.
In this case, the two operations in the pipeline are save
calls. The idea is that this pipeline is almost identical to the synchronous implementation except that we don't block on the save
call. Specifically, the call to blockLast()
subscribes to the assembled pipeline, causing the Flux
to asynchronously emit its two events. Then the pipeline inside .flatMap( ... ).blockLast();
processes each of those events in a pseudo-parallel fashion. When one request is issued and waits for a response, Project Reactor will process other requests in the background, which is the critical factor in maxing out requests/second per thread.
Now that we've demonstrated efficient async database requests with Project Reactor, the rest of this lab will use blocking async calls (effectively sync calls) for simplicity. To learn more about Project Reactor, see the Azure Cosmos DB Reactor Pattern Guide.
Read documents
To read documents from the database, add the following method to
CosmosSample
:/** * Take in a Java POJO argument, extract ID and partition key, and read the corresponding document from the container. * In this case the ID is the partition key. * @param webCustomer User POJO to pull ID and partition key from. */ private WebCustomer readWebCustomerDocument(final WebCustomer webCustomer) { WebCustomer webCustomerResult = null; try { logger.info("Read webCustomer {}", webCustomer.getId()); webCustomerResult = this.reactiveWebCustomerRepository.findById(webCustomer.getId(), new PartitionKey(webCustomer.getLastName())).block(); } catch (CosmosException de) { logger.error("Failed to read webCustomer {}", webCustomer.getId(), de); } return webCustomer; }
Copy and paste the following code to the end of the
run
method, after the document creation code:readWebCustomerDocument(maxaxam);
Build and run CosmosSample.java in the IDE, or run the program in the terminal by using:
mvn clean package mvn spring-boot:run
Amidst the terminal output, you should see the following. "Read user 1" indicates that the document was retrieved.
INFO: Read webCustomer 1
Delete documents
Copy and paste the
deleteWebCustomerDocument
method under yourreadWebCustomerDocument
method./** * Take in a Java POJO argument, extract ID and partition key, * and delete the corresponding document. * @param webCustomer User POJO representing the document update. */ private void deleteWebCustomerDocument(final WebCustomer webCustomer) { try { this.reactiveWebCustomerRepository.deleteById(webCustomer.getId(),new PartitionKey(webCustomer.getLastName())).block(); logger.info("Deleted webCustomer {}", webCustomer.getId()); } catch (CosmosException de) { logger.error("User {} could not be deleted.", webCustomer.getId()); } }
Copy and paste the following code to the end of the
run
method.deleteWebCustomerDocument(maxaxam);
Build and run CosmosSample.java in the IDE, or run the program in the terminal by using:
mvn clean package mvn spring-boot:run
Amidst the terminal output, you should see the following. "Deleted user 1" indicates that the document was deleted.
INFO: Deleted webCustomer 1
In this unit, you created, updated, read and deleted documents in your Azure Cosmos DB database.