In the previous chapter, we covered the last part of the Implementing Solutions for Apps objective by learning how to implement authentication for web apps, APIs, and more.
In this chapter, we will be introducing the final objective: Implementing and Managing Data Platforms. We are going to cover what a NoSQL database is, learn about the features of Azure's NoSQL offering known as Cosmos DB, and then develop a solution that uses it.
We are also going to learn how to create, read, update, and delete data by using the appropriate APIs, how to set the appropriate consistency level for operations, understand the different partition schemes, and implement a multi-region replica of our database.
The following topics will be covered in this chapter:
Let's get started!
This chapter's examples will use Visual Studio 2019 (https://visualstudio.microsoft.com/vs/).
The source code for our sample application can be downloaded from https://github.com/PacktPublishing/Microsoft-Azure-Architect-Technologies-Exam-Guide-AZ-303/tree/master/Ch15.
Azure Cosmos DB is a NoSQL database, so to get the best value from Cosmos DB, we need to understand what NoSQL is and how it differs from traditional SQL databases.
SQL databases are built around the concept of tables, and within each table, you have rows of data split into cells. Each cell contains an individual piece of data within a record, and each row is an entire record.
A key difference from SQL databases is that the table and cell definition, known as the schema, must be defined before you can enter data into it. So, if you want to store a customer record containing a name and address, first, you need to create a table with each of the columns defined for each data type, such as the first name, address line, city, country, and so on.
Traditionally, SQL tables are built so that they're relational. This means that rather than having large records containing all the required information, you split the records into multiple tables and then join them as part of a query. For example, you could store core personal information such as a user's first and last name in one table, but their address details in another table, with a link in each record to join the related tables together.
In a similar fashion, data elements that need to be consistent across records, such as a Category, would be defined in a separate table, with a CategoryId link in the person record rather than the category itself.
The following screenshot shows an example of how this might look:
Relational tables can also have one-to-many relationships – this means that rather than linking a single address record to a person, you can link multiple address records. They can be used to store separate addresses for billing and shipping, for example.
One of the benefits of a relational table is that any change that's made to a category name, for example, would automatically be reflected across all person records as they only store a link to the category ID, not the data itself.
NoSQL databases, as the name suggests, do not follow these patterns. NoSQL databases store data as JSON documents, which are hierarchical in structure. This means that related information, such as an address, can be stored alongside the main person record but is embedded as a child object, as opposed to being part of a flat structure. Embedding data can also be used for multiple related records; for example, multiple addresses can be stored within the main person record too.
With this in mind, a typical NoSQL JSON document might look like this:
{
"id": "001",
"firstName": "Brett",
"surname": "Hargreaves",
"category": "Active",
"addresses": [
{
"type": "billing",
"street": "999 Letsbe Avenue",
"city": "London"
},
{
"type": "shipping",
"street": "20 Docklands Road",
"city": "London"
}
]
}
For certain scenarios, NoSQL databases can perform much faster than their SQL counterparts. However, one of the biggest benefits is that you can easily mix the records of completely different schemas.
For example, a container, which is synonymous to a table, might contain the following records – and this would be perfectly valid:
[
{
"id": "001",
"firstName": "Brett",
"Surname": "Hargreaves",
"category": "Active",
},
{
"id": "002",
"firstName": "Sjoukje",
"Surname": "Zaal",
"profession": "CTO",
},
{
"id": "003",
"title": "Microsoft Azure Architect Technologies: Exam Guide AZ303",
"publisher": "Packt Publishing",
}
]
Now that you have an understanding of NoSQL, we will investigate Azure's implementation of a NoSQL database – Cosmos DB.
Cosmos DB storage is the premium offering for Azure Table storage. It's a multi-model and globally distributed database service that is designed to horizontally scale and replicate your data to any number of Azure regions. By replicating and scaling the data, Cosmos DB can guarantee low latency, high availability, and high performance anywhere in the world. You can replicate or scale data easily inside the Azure portal by selecting from the available regions on the map.
This high availability and low latency makes Cosmos DB most suitable for mobile applications, games, and applications that need to be globally distributed. The Azure portal also uses Cosmos DB for data storage. Cosmos DB is completely schemaless, and you can use a number of existing APIs with available SDKs to communicate with it. So, if you are using a specific API for your data and you want to move your data to Cosmos DB, all you need to do is change the connection string inside your application and the data will be stored in Cosmos DB automatically.
Cosmos DB offers the following key benefits:
Cosmos DB supports the following APIs for storing and interacting with your data:
In the future, new APIs will be added to Cosmos DB as well.
In the next section, we are going to create, read, update, and delete data using the appropriate APIs.
Before we can create, read, update, and delete data using the APIs, first, we need to create a Cosmos DB in Azure. We will do this in the following subsection.
You can create a Cosmos DB using the Azure portal, PowerShell, the CLI, or ARM templates. In this demonstration, we are going to create a Cosmos DB server, database, and container from the Azure portal. To do this, perform the following steps:
a) Subscription: Pick a subscription.
b) Resource group: Create a new one and call it PacktCosmosResourceGroup.
c) Account name: packtsqlapi.
d) API: Core (SQL).
e) Notebooks (preview): Off.
f) Location: East US.
g) Capacity mode: Provisioned throughput.
h) Apply Free Tier Discount: Apply (this is only available for one Cosmos DB per subscription).
i) Account Type: Non-Production.
j) Geo-Redundancy: Disable.
k) Multi-region Writes: Disable.
a) Database id: PacktToDoList
b) Throughput: 400 (provisioning the throughput at the database level means the throughput will be shared across all containers)
c) Container id: Items
d) Partition Key: /category
In this demonstration, we created a database server, a database, and a container from the Azure portal. In the next section, we are going to create an application that creates a database and container programmatically. Then, we will create, read, update, and delete data using the Cosmos DB APIs.
In this second part of the demonstration, we are going to create the sample application. For this, we are going to create a new console application in Visual Studio 2019. First, we will connect to our Cosmos DB account.
The first step is to connect to the Cosmos DB account that we created in the previous section, as follows:
using System;
using System.Threading.Tasks;
using System.Configuration;
using System.Collections.Generic;
using System.Net;
using Microsoft.Azure.Cosmos;
// Azure Cosmos DB endpoint.
private static readonly string EndpointUri = "<your endpoint here>";
// Primary key for the Azure Cosmos account.
private static readonly string PrimaryKey = "<your primary key>";
// The Cosmos client instance
private CosmosClient cosmosClient;
// The database we will create
private Database database;
// The container we will create.
private Container container;
// The name of the database and container we will create
private string databaseId = "FamilyDatabase";
private string containerId = "FamilyContainer";
Replace the Main() method with the following:
public static async Task Main(string[] args)
{
}
public async Task GetStartedDemoAsync()
{
// Create a new instance of the Cosmos Client
this.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
}
public static async Task Main(string[] args)
{
try
{
Console.WriteLine("Beginning operations... ");
Program p = new Program();
await p.GetStartedDemoAsync();
}
catch (CosmosException de)
{
Exception baseException = de.GetBaseException();
Console.WriteLine("{0} error occurred: {1}", de.StatusCode, de);
}
catch (Exception e)
{
Console.WriteLine("Error: {0}", e);
}
finally
{
Console.WriteLine("End of demo, press any key to exit.");
Console.ReadKey();
}
}
Now, if you run the application, the console will display a message stating End of demo, press any key to exit. This means that the application has successfully connected to the Cosmos DB account.
Now that we've successfully connected to the Cosmos DB account, we can create a new database.
To create a new database, perform the following steps:
private async Task CreateDatabaseAsync()
{
// Create a new database
this.database = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(databaseId);
Console.WriteLine("Created Database: {0} ", this.database.Id);
}
public async Task GetStartedDemoAsync()
{
// Create a new instance of the Cosmos Client
this.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
//ADD THIS PART TO YOUR CODE
await this.CreateDatabaseAsync();
}
In the next section, we will create the container.
To create the container, do the following:
private async Task CreateContainerAsync()
{
// Create a new container
this.container = await this.database.CreateContainerIfNotExistsAsync(containerId, "/LastName");
Console.WriteLine("Created Container: {0} ", this.container.Id);
}
public async Task GetStartedDemoAsync()
{
// Create a new instance of the Cosmos Client
this.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
await this.CreateDatabaseAsync();
//ADD THIS PART TO YOUR CODE
await this.CreateContainerAsync();
}
In the next section, we are going to add items to the container.
In this section, we are going to add items to the container using the API. First, we need to create a Family class that represents the objects that we are going to store in the container. You can create an item using the CreateItemAsync method. When you use the SQL API, items are created and stored as documents. These documents are user-defined arbitrary JSON content. You can then insert an item into your Azure Cosmos container.
Besides the Family class, we will also add some subclasses, such as Parent, Child, Pet, and Address, which are used in the Family class. To do this, perform the following steps:
using Newtonsoft.Json;
public class Family
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
public string LastName { get; set; }
public Parent[] Parents { get; set; }
public Child[] Children { get; set; }
public Address Address { get; set; }
public bool IsRegistered { get; set; }
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
public class Parent
{
public string FamilyName { get; set; }
public string FirstName { get; set; }
}
public class Child
{
public string FamilyName { get; set; }
public string FirstName { get; set; }
public string Gender { get; set; }
public int Grade { get; set; }
public Pet[] Pets { get; set; }
}
public class Pet
{
public string GivenName { get; set; }
}
public class Address
{
public string State { get; set; }
public string County { get; set; }
public string City { get; set; }
}
private async Task AddItemsToContainerAsync()
{
// Create a family object for the Zaal family
Family ZaalFamily = new Family
{
Id = "Zaal.1",
LastName = "Zaal",
Parents = new Parent[]
{
new Parent { FirstName = "Thomas" },
new Parent { FirstName = "Sjoukje" }
},
Children = new Child[]
{
new Child
{
FirstName = "Molly",
Gender = "female",
Grade = 5,
Pets = new Pet[]
{
new Pet { GivenName = "Fluffy" }
}
}
},
Address = new Address { State = "WA", County = "King", City = "Seattle" },
IsRegistered = false
};
try
{
// Read the item to see if it exists.
ItemResponse<Family> zaalFamilyResponse = await this.container.ReadItemAsync<Family>(ZaalFamily.Id, new PartitionKey(ZaalFamily.LastName));
Console.WriteLine("Item in database with id: {0} already exists ", zaalFamilyResponse.Resource.Id);
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
// Create an item in the container representing the Zaal family. Note we provide the value of the partition key for this item, which is "Zaal"
ItemResponse<Family> zaalFamilyResponse = await this.container.CreateItemAsync<Family>(ZaalFamily, new PartitionKey(ZaalFamily.LastName));
// Note that after creating the item, we can access the body of the item with the Resource property off the ItemResponse. We can also access the RequestCharge property to see the amount of RUs consumed on this request.
Console.WriteLine("Created item in database with id: {0} Operation consumed {1} RUs. ", zaalFamilyResponse.Resource.Id, zaalFamilyResponse.RequestCharge);
}
// Create a family object for the PacktPub family
Family PacktPubFamily = new Family
{
Id = "PacktPub.1",
LastName = "PacktPub",
Parents = new Parent[]
{
new Parent { FamilyName = "PacktPub", FirstName = "Robin" },
new Parent { FamilyName = "Zaal", FirstName = "Sjoukje" }
},
Children = new Child[]
{
new Child
{
FamilyName = "Merriam",
FirstName = "Jesse",
Gender = "female",
Grade = 8,
Pets = new Pet[]
{
new Pet { GivenName = "Goofy" },
new Pet { GivenName = "Shadow" }
}
},
new Child
{
FamilyName = "Miller",
FirstName = "Lisa",
Gender = "female",
Grade = 1
}
},
Address = new Address { State = "NY", County = "Manhattan", City = "NY" },
IsRegistered = true
};
try
{
// Read the item to see if it exists
ItemResponse<Family> packtPubFamilyResponse = await this.container.ReadItemAsync<Family>(PacktPubFamily.Id, new PartitionKey(PacktPubFamily.LastName));
Console.WriteLine("Item in database with id: {0} already exists ", packtPubFamilyResponse.Resource.Id);
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
// Create an item in the container representing the Wakefield family. Note we provide the value of the partition key for this item, which is "PacktPub"
ItemResponse<Family> packtPubFamilyResponse = await this.container.CreateItemAsync<Family>(PacktPubFamily, new PartitionKey(PacktPubFamily.LastName));
// Note that after creating the item, we can access the body of the item with the Resource property off the ItemResponse. We can also access the RequestCharge property to see the amount of RUs consumed on this request.
Console.WriteLine("Created item in database with id: {0} Operation consumed {1} RUs. ", packtPubFamilyResponse.Resource.Id, packtPubFamilyResponse.RequestCharge);
}
public async Task GetStartedDemoAsync()
{
// Create a new instance of the Cosmos Client
this.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
await this.CreateDatabaseAsync();
await this.CreateContainerAsync();
//ADD THIS PART TO YOUR CODE
await this.AddItemsToContainerAsync();
}
In this section, we added some items to the container. In the next section, we will query the resources.
In this part of the demonstration, we are going to query the resources. Azure Cosmos DB supports rich queries against JSON documents stored in each container. The following code will show you how to run a query against the items that we stored in the container in the previous section.
To do this, perform the following steps:
private async Task QueryItemsAsync()
{
var sqlQueryText = "SELECT * FROM c WHERE c.LastName = 'Zaal'";
Console.WriteLine("Running query: {0} ", sqlQueryText);
QueryDefinition queryDefinition = new QueryDefinition(sqlQueryText);
FeedIterator<Family> queryResultSetIterator = this.container.GetItemQueryIterator<Family>(queryDefinition);
List<Family> families = new List<Family>();
while (queryResultSetIterator.HasMoreResults)
{
FeedResponse<Family> currentResultSet = await queryResultSetIterator.ReadNextAsync();
foreach (Family family in currentResultSet)
{
families.Add(family);
Console.WriteLine(" Read {0} ", family);
}
}
}
public async Task GetStartedDemoAsync()
{
// Create a new instance of the Cosmos Client
this.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
await this.CreateDatabaseAsync();
await this.CreateContainerAsync();
await this.AddItemsToContainerAsync();
//ADD THIS PART TO YOUR CODE
await this.QueryItemsAsync();
}
With that, we have created a query to retrieve the data from the container. In the next section, we are going to update a Family item.
In this part of the demonstration, we are going to update a Family item. To do this, add the following code:
private async Task ReplaceFamilyItemAsync()
{
ItemResponse<Family> PacktPubFamilyResponse = await this.container.ReadItemAsync<Family>("PacktPub.1", new PartitionKey("PacktPub"));
var itemBody = PacktPubFamilyResponse.Resource;
// update registration status from false to true
itemBody.IsRegistered = true;
// update grade of child
itemBody.Children[0].Grade = 6;
// replace the item with the updated content
PacktPubFamilyResponse = await this.container.ReplaceItemAsync<Family>(itemBody, itemBody.Id, new PartitionKey(itemBody.LastName));
Console.WriteLine("Updated Family [{0},{1}]. Body is now: {2} ", itemBody.LastName, itemBody.Id, PacktPubFamilyResponse.Resource);
}
public async Task GetStartedDemoAsync()
{
// Create a new instance of the Cosmos Client
this.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
await this.CreateDatabaseAsync();
await this.CreateContainerAsync();
await this.AddItemsToContainerAsync();
await this.QueryItemsAsync();
//ADD THIS PART TO YOUR CODE
await this.ReplaceFamilyItemAsync();
}
In this part of the demonstration, we updated an item in the container. In the next section, we will delete an item from the container.
To delete a Family item, perform the following steps:
private async Task DeleteFamilyItemAsync()
{
var partitionKeyValue = "Zaal";
var familyId = "Zaal.1";
// Delete an item. Note we must provide the partition key value and id of the item to delete
_ = await this.container.DeleteItemAsync<Family>(familyId, new PartitionKey(partitionKeyValue));
Console.WriteLine("Deleted Family [{0},{1}] ", partitionKeyValue, familyId);
}
public async Task GetStartedDemoAsync()
{
// Create a new instance of the Cosmos Client
this.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
await this.CreateDatabaseAsync();
await this.CreateContainerAsync();
await this.AddItemsToContainerAsync();
await this.QueryItemsAsync();
await this.ReplaceFamilyItemAsync();
//ADD THIS PART TO YOUR CODE
await this.DeleteFamilyItemAsync();
}
In this demonstration, we created a Cosmos DB server, database, and container. We also added data to the container, ran a query on the data, updated the data, and then deleted the data.
In the next section, we are going to cover partitioning schemes.
To meet the performance needs of your application, Azure Cosmos DB uses partitioning to scale individual containers in a database. Cosmos DB partitions in a way that the items are divided into distinct subsets called logical partitions. These are formed based on the value of the partition key that is added to each item in the container. All of the items that are in a logical partition have the same partition key. Each item in a container has an item ID (which is unique within the logical partition). To create the item's index, the partition key and the item ID are combined. This uniquely identifies the item.
Tip
If you look at our sample application from the previous section, you will see that the partition key and item ID have been combined.
Besides logical partitions, Azure Cosmos DB also has physical partitions:
The following chart shows how logical partitions are mapped to physical partitions that are distributed globally over multiple regions:
In the next section, we are going to cover how to set the appropriate consistency level for operations.
When you use distributed databases that rely on high availability and low latency, you can choose between two different extreme consistency models: strong consistency and eventual consistency. The former is the standard for data programmability, but this will result in reduced availability during failures and higher latency. The latter offers higher availability and better performance, but makes it much harder to program applications.
Azure Cosmos DB offers more choices between the two extreme consistency models. Strong consistency and eventual consistency are at two different ends of the spectrum. This can help developers make a more precise choice with respect to high availability and performance. Azure Cosmos DB offers five consistency models, known as strong, bounded staleness, session, consistent prefix, and eventual consistency:
Tip
For more information on how to choose the right consistency level for the different APIs that Azure Cosmos DB has to offer, please refer to the article at https://docs.microsoft.com/en-us/azure/cosmos-db/consistency-levels-choosing.
The key lesson from this section is that you can control how fast data is replicated around all regions to best support your application requirements. In the next and final section, we will learn how to implement and place our database replicas.
Azure Cosmos DB is a globally distributed database, and it works by placing replicas of your data in the regions where you need them. In the previous section, we learned how to control the consistency of those replicas. In this section, we will create a replica of our database and set that consistency level. Follow these steps:
The replication process may take some time to complete. When it does, you will be able to see your new replica on the map in a darker colored pin. By default, the replica will use Session Consistency. To change this, perform the following steps:
The following screenshot shows an example of this:
As we have seen, Cosmos DB is a powerful, fully managed, and fully resilient NoSQL option that provides many options that can be uniquely tailored to your requirements.
In this chapter, we've learned about Azure's NoSQL offering, known as Cosmos DB, what the differences are between SQL and NoSQL, and how to build a simple solution.
We then covered how to use the different APIs from Visual Studio to create databases, containers, data, and more. We also covered the partition schemes and the different consistency levels that are provided for Azure Cosmos DB.
Finally, we walked through enabling a multi-region replication and configuring its consistency.
In the next chapter, we will continue with this objective by learning how to develop solutions that use a relational database.
Answer the following questions to test your knowledge of the information in this chapter. You can find the answers in the Assessments section at the end of this book:
a) True
b) False
a) True
b) False
a) True
b) False
Check out the following links for more information about the topics that were covered in this chapter:
18.118.145.114