MS Certificering: AZ-204, overnieuw beginnen, deel 04

Ingediend door Dirk Hornstra op 21-dec-2021 15:15

Na het 1e blok: Azure App Service web apps (link), het 2e blok: Implement Azure Functions (link) en het 3e blok: Develop solutions that use Blob Storage (link) is het nu tijd voor het 4e learning-block: Develop solutions that use Azure Cosmos DB, link.

Module 1: Explore Azure Cosmos DB, link.

Azure Cosmos DB is gebouwd om een database-oplossing te bieden die lage latency, elastische schaalbaarheid, consistente data en hoge beschikbaarheid heeft.

99.999% read en write beschikbaarheid, wereldwijd!
read en writes binnen 10 milliseconden "at the 99th percentile"

Op dit moment kun je maximaal 50 Azure Cosmos accounts onder een abonnement plaatsen (via support te verhogen).
Je hebt verschillende API's. Een Azure Cosmos database wordt als database beschouwd door SQL API, Azure Cosmos DB API for MongoDB en de Gremlin API. Cassandra API beschouwt het als een Keyspace, Table API kan er schijnbaar niets mee. Als je met Table API een tabel maakt wordt automatisch een database voor je aangemaakt.

Een item in Cosmos DB is een item in SQL API en Table API, Row in Cassandra API, Document in API for MongoDB en Node of Edge in Gremlin API.

Je hebt verschillende "consistentie-modes" om te kiezen. Dat loopt van strong - bounded staleness, session, consistent prefix naar eventual.

Welke keuze moet je maken?

SQL API en Table API

  • voor de meeste gevallen is session consistency de juiste optie
  • als er sterke consistentie nodig is, dan bounded staleness
  • als "eventual consistency"nog is, gebruik dan consistent prefix consistency
  • als er iets minder strikte eisen zijn dan session, gebruik prefix consistency
  • als je de hoogste beschikbaarheid wilt en laagste latency, dan eventual consistency
  • nog recentere data, maar zonder performance te offeren, dan kun je custom consistency level op de applicatielaag doorvoeren


Cassandra, MongoDB en Gremlin

Bij Gremlin wordt de default consistency level gebruikt.

Instellingen van Cassanda kun je hier nalezen: link en MongoDB hier: link.

Als bounded staleness gekozen is, dan garandeert Cosmos dat de clients altijd de waarde van een vorige write lezen.

Als het consistency level op "strong" staat, dan is het staleness window ingesteld op 0, dus ziet een client altijd de laatst-geschreven waarde.

Bij de andere levels is het afhankelijk van de workload.

Voor alle andere opties dan "strong" kun je de waarschijnlijkheid van het uitlezen van de data baseren op de Probabilisticaly Bounded Staleness (PBS) metriek.

Met Core (SQL) API kun je altijd via SQL je data opvragen (je doet query's op JSON-data).

API voor MongoDB, hierbij wordt je data opgeslagen in BSON formaat, compatibel met MongoDB, maar gebruikt niet deze code. API voor MongoDB is compatibel met versie 4.0, 3.6 en 3.2 van MongoDB server.

Cassandra API, data wordt opgeslagen in een kolom-gebaseerde structuur. Je kunt Cassandra Query Language (CQL) gebruiken en tools als CQL shell. Cassandra API ondersteunt alleen de OLTP scenario's.

Table API, hierbij wordt data opgeslagen als key-value. Als je Azure Table storage gebruikt zie je grenzen bij latency, schalen, doorvoer. Table API ondersteunt alleen OLTP scenario's.

Gremlin API, je kunt graph query's uitvoeren en data opslaan als "edges" en "vertices". Ook Gremlin ondersteunt alleen OLTP scenario's.

Met Azure Cosmos DB betaal je voor de doorvoer van data en de opslag die je gebruikt, per uur. De kosten van database acties worden uitgedrukt in request units (RU's). CPU, IOPS, geheugen, deze zaken die nodig zijn. De kosten om een read te doen is voor een 1KB item 1RU.

  • Op basis van het type account wat je aanmaakt worden deze in rekening gebracht.
  • Provisioned throughput mode: je stelt eeen aantal RU's per applicatie per seconde in, deze nemen met 10 per seconde toe. De aanpassingen (verhogen of verlagen per 100) kun je via de portal of code doen. Je kunt dit op container en database niveau instellen.
  • Serverless mode: geen provisioning. Je betaalt "per gebruik".
  • Autoscale mode: hiermee laat je de boel automatisch schalen, als de load op je data onvoorspelbaar is.


Hierna volgt een voorbeeld waarbij je een Cosmos DB aanmaakt.
In het voorbeeld wordt voor "Serverless" gekozen. Je voegt een container toe, je voegt items toe.
Ook hier, je moet een eigen of free account gebruiken, het kan niet meer met een sandbox.

Module 2: Implement partitioning in Azure Cosmos DB, link.

Dit hoofdstuk gaat over partitionering. Je deelt de containers in op basis van een partitie-sleutel. Elke subset is dan een "logical partition". Alle items in een partitie hebben voor de partition key dezelfde waarde. Zou je gebruikers op basis van UserID gaan indelen en er zijn 1.000 gebruikers, dan heb je ook 1.000 partities. Naast die partition key heeft elk item in de container een unieke item ID waarde. Combineren van die 2 is de index, waarmee je een item uniek maakt. Bij "personeel" zou je een partitie sleutel in kunnen stellen op "afdeling".

De logische partitie geeft ook aan waar je een database transactie kunt doorvoeren. Je kunt dat daarop doen met snapshot isolatie.

Naast logische partities heb je ook de fysieke partities: link. Een fysieke partitie kan één of meerdere logische partities bevatten (net zoals je dat op schijf doet). Azure Cosmos DB beheert dat zelf.
Elke fysieke partitie kan 10.000 request units per seconde verwerken. Dat impliceert dat logische partities ook die waarde hebben.
De totale hoeveelheid data in een fysieke partitie kan maximaal 50 GB bedragen.

De keuze voor de partition key is belangrijk, je kunt deze na de tijd niet meer aanpassen. Partition key accepteert alfanumerieke tekens en underscores.
Het moet een waarde zijn die nooit verandert. De mogelijke waardes moeten groot zijn. En graag een evenredige verdeling, zodat de RU en opslag gelijkmatig verdeeld wordt.

Als je grote containers hebt waar veel uit gelezen wordt, is het mogelijk dat je een partitie sleutel wilt gebruiken die vaak als filter in query's gebruikt wordt. Door deze in het filter predicate mee te nemen kunnen query's geoptimaliseerd worden: link.

Als je een kleine container hebt (weinig data dus), dan heb je waarschijnlijk nog niet veel fysieke partities en is het nog niet iets om over na te denken/je zorgen over te maken. Als er meer dan een paar fysieke partities gebruikt worden, dan moet je nadenken over cross-partition query's.

Je krijg meer fysieke partities als: je container meer dan 30.000 RU's heeft, je container meer dan 100 GB aan data heeft.

De site geeft nog aan dat je ook item ID als partitie sleutel kunt gebruiken. Je moet er wel rekening mee houden dat als dit de key is, hij uniek is binnen je container en dat je dus geen ander item met dezelfde item ID kunt hebben, item ID is goed bij een heavy-read container als er veel fysieke partities zijn en je kunt geen stored procedures en triggers op meerdere logische partities uitvoeren.

De beste invulling is een veld met tig verschillende waardes, honderdduizenden. Hierdoor wordt je data beter gedistribueerd. Als dat veld er niet is, kun je zelf een veld maken die dit gaat doen, een synthetic partition key.

Je kunt dat doen door een partitionKey veld te maken en die te vullen door bijvoorbeeld van 2 velden de waardes achter elkaar te plakken. Misschien is 1 veld niet uniek, maar een combinatie dat wel. Of gebruik een partition key met een willekeurige toevoeging. We zien het voorbeeld van een datum, waar een willekeurig cijfer achter geplakt wordt (tussen 1 en 400). Hiermee komen niet alle records van 1 dag in 1 partitie. En we hebben het voorbeeld van een soort RDW-achtige database waarbij we nummerborden opslaan, partition key is de datum. Vaak wordt er gezocht op datum en nummerbord. We berekenen een hash van het nummerbord en voegen dat toe achter de datum.

Module 3: Work with Azure Cosmos DB, link.

We gaan kijken naar de .NET SDK v3 voor Cosmos DB.


// je hebt eerst de client
CosmosClient client = new CosmosClient(endpoint, key);

// aanmaken database

// An object containing relevant information about the response
DatabaseResponse databaseResponse = await client.CreateDatabaseIfNotExistsAsync(databaseId, 10000);

// A client side reference object that allows additional operations like ReadAsync
Database database = databaseResponse;

// database response lezen

DatabaseResponse readResponse = await database.ReadAsync();

// database verwijderen

await database.DeleteAsync();

// container aanmaken

// Set throughput to the minimum value of 400 RU/s
ContainerResponse simpleContainer = await database.CreateContainerIfNotExistsAsync(
    id: containerId,
    partitionKeyPath: partitionKey,
    throughput: 400);

// container opvragen

Container container = database.GetContainer(containerId);
ContainerProperties containerProperties = await container.ReadContainerAsync();

// container verwijderen

await database.GetContainer(containerId).DeleteContainerAsync();

// item aanmaken

ItemResponse<SalesOrder> response = await container.CreateItemAsync(salesOrder, new PartitionKey(salesOrder.AccountNumber));

// item lezen

string id = "[id]";
string accountNumber = "[partition-key]";
ItemResponse<SalesOrder> response = await container.ReadItemAsync(id, new PartitionKey(accountNumber));

// item query-en

QueryDefinition query = new QueryDefinition(
    "select * from sales s where s.AccountNumber = @AccountInput ")
    .WithParameter("@AccountInput", "Account1");

FeedIterator<SalesOrder> resultSet = container.GetItemQueryIterator<SalesOrder>(
    query,
    requestOptions: new QueryRequestOptions()
    {
        PartitionKey = new PartitionKey("Account1"),
        MaxItemCount = 1
    });

Meer info in de Github-repo: link en hier kun je voorbeelden vinden: link.
We gaan vervolgens (met een eigen Azure account) de voorbeelden doorlopen:

 

// inloggen

az login

// resource group aanmaken

az group create --location <myLocation> --name az204-cosmos-rg

// db account aanmaken
// in de response die je krijgt, noteer het documentEndpoint

az cosmosdb create --name <myCosmosDBacct> --resource-group az204-cosmos-rg

// vraag de primaire sleutel op:

# Retrieve the primary key
az cosmosdb keys list --name <myCosmosDBacct> --resource-group az204-cosmos-rg

// ga lokaal aan de slag:
md az204-cosmos
cd az204-cosmos
dotnet new console
code . -r

dotnet add package Microsoft.Azure.Cosmos

// include dit in Program.cs

using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;

// voorbeeld code om te gebruiken

public class Program
{
    // Replace <documentEndpoint> with the information created earlier
    private static readonly string EndpointUri = "<documentEndpoint>";

    // Set variable to the Primary Key from earlier.
    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 names of the database and container we will create
    private string databaseId = "az204Database";
    private string containerId = "az204Container";

    public static async Task Main(string[] args)
    {
        try
        {
            Console.WriteLine("Beginning operations...\n");
            Program p = new Program();
            await p.CosmosAsync();
        }
        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 program, press any key to exit.");
            Console.ReadKey();
        }
    }
}

// voeg ook toe

public async Task CosmosAsync()
{
    // Create a new instance of the Cosmos Client
    this.cosmosClient = new CosmosClient(EndpointUri, PrimaryKey);
// Runs the CreateDatabaseAsync method
            await this.CreateDatabaseAsync();
// Run the CreateContainerAsync method
await this.CreateContainerAsync();
}

// db aanmaken

private async Task CreateDatabaseAsync()
{
    // Create a new database using the cosmosClient
    this.database = await this.cosmosClient.CreateDatabaseIfNotExistsAsync(databaseId);
    Console.WriteLine("Created Database: {0}\n", this.database.Id);
}

// voeg ook toe

private async Task CreateContainerAsync()
{
    // Create a new container
    this.container = await this.database.CreateContainerIfNotExistsAsync(containerId, "/LastName");
    Console.WriteLine("Created Container: {0}\n", this.container.Id);
}

// voer het programma uit en ruim daarna weer op:
az group delete --name az204-cosmos-rg --no-wait

Je kunt in Cosmos werken met stored procedures, triggers en user-defined-functions, zoals we die ook van SQL Server gewend zijn.
Hier kun je meer informatie vinden: link.

Stored procedures maak je in Javascript:


var helloWorldStoredProc = {
    id: "helloWorld",
    serverScript: function () {
        var context = getContext();
        var response = context.getResponse();

        response.setBody("Hello, World");
    }
}

We gaan een SP maken waarbij een item wordt aangemaakt en we het ID daarvan geven we terug. Die actie kan mislukken en dat moeten we dus ook afvangen.


function createSampleDocument(documentToCreate) {
    var context = getContext();
    var collection = context.getCollection();
    var accepted = collection.createDocument(
        collection.getSelfLink(),
        documentToCreate,
        function (error, documentCreated) {                 
            context.getResponse().setBody(documentCreated.id)
        }
    );
    if (!accepted) return;
}

Je krijgt data altijd binnen als een string, ook als het een array is. Dat kun je met onderstaande code zelf filteren en vervolgens parsen naar een array:


function sample(arr) {
    if (typeof arr === "string") arr = JSON.parse(arr);

    arr.forEach(function(a) {
        // do something here
        console.log(a);
    });
}

Acties op de DB zijn gebonden aan tijd. SP hebben een beperkte tijd om uitgevoerd te worden. Alle functies sturen een boolean terug dat weergeeft of de actie wel of niet uitgevoerd is.

Je kunt transacties implementeren met SP's. Javascript functies kunnen een continuation-based model implementeren of de uitvoering vervolgen.

Cosmos DB ondersteunt pre-triggers en post-triggers. Triggers worden niet automatisch uitgevoed, ze moet gespecificeerd zijn bij elke database-actie waarop je wilt dat ze uitgevoerd worden. Voorbeelden van pre-triggers: link en post-triggers: link.

Pre-trigger om datum-veld te vullen:


function validateToDoItemTimestamp() {
    var context = getContext();
    var request = context.getRequest();

    // item to be created in the current operation
    var itemToCreate = request.getBody();

    // validate properties
    if (!("timestamp" in itemToCreate)) {
        var ts = new Date();
        itemToCreate["timestamp"] = ts.getTime();
    }

    // update the item that will be created
    request.setBody(itemToCreate);
}

Pre-triggers hebben geen input parameters. Je moet aangeven wanneer een trigger uitgevoerd wordt. In dit geval zou bij TriggerOperation de waarde TriggerOperation.Create gekozen moeten worden.

Voorbeeld van een post-trigger:


function updateMetadata() {
var context = getContext();
var container = context.getCollection();
var response = context.getResponse();

// item that was created
var createdItem = response.getBody();

// query for metadata document
var filterQuery = 'SELECT * FROM root r WHERE r.id = "_metadata"';
var accept = container.queryDocuments(container.getSelfLink(), filterQuery,
    updateMetadataCallback);
if(!accept) throw "Unable to update metadata, abort";

function updateMetadataCallback(err, items, responseOptions) {
    if(err) throw new Error("Error" + err.message);
        if(items.length != 1) throw 'Unable to find metadata document';

        var metadataItem = items[0];

        // update metadata
        metadataItem.createdItems += 1;
        metadataItem.createdNames += " " + createdItem.id;
        var accept = container.replaceDocument(metadataItem._self,
            metadataItem, function(err, itemReplaced) {
                    if(err) throw "Unable to update metadata, abort";
            });
        if(!accept) throw "Unable to update metadata, abort";
        return;
    }
}

Het uitvoeren van triggers zit binnen de transactie. Dus als er een fout optreedt bij post-trigger uitvoering, dan faalt de hele transactie, wordt er een rollback gedaan en krijg je de error terug.

User-defined functions, een voorbeeld om de BTW te berekenen:


function tax(income) {

        if(income == undefined)
            throw 'no input';

        if (income < 1000)
            return income * 0.1;
        else if (income < 10000)
            return income * 0.2;
        else
            return income * 0.4;
    }