Paweł Łukasiewicz
2024-04-15
Paweł Łukasiewicz
2024-04-15
Udostępnij Udostępnij Kontakt
Wprowadzenie

W tym wpisie wykorzystamy AWS SDK dla .NET w celu dodania nowej tabeli z jednym (lub więcej) globalnym indeksem wtórnym oraz wykonamy zapytania przy wykorzystaniu tych indeksów.

Utworzenie tabeli z globalnym indeksem wtórnym

Jak doskonale pamiętacie z poprzednich wpisów, indeks GSI, musimy utworzyć w tym samym czasie w którym tworzona jest tabela. W tym celu wykorzystamy metodę CreateTable podając specyfikację jednego (lub więcej) globalnego indeksu wtórnego. W ramach przykładu w języku C# przygotujemy tabelę, która będzie przechowywała informacje o danych pogodowych. Kluczem partycji jest Location (lokalizacja) a kluczem sortownia Date (data). Globalny indeks wtórny o nazwie PrecipIndex pozwala na szybki dostęp do danych o opadach w różnej lokalizacji.

Kolejne kroki, które musimy wykonać dla powyższego przykładu to:

  • utworzenie instancji klasy AmazonDynamoDBClient;
  • utworzenie instancji klasy CreateTableRequest w celu przekazania informacji o żądaniu;
  • zdefiniowanie nazwy tabeli, jej klucza głównego oraz wartości przepustowości. Dla globalnego indeksu wtórnego musimy również podać nazwę indeksu, jego ustawienia przepustowości, definicje atrybutów dla klucza sortowania indeksu, schemat klucza dla indeksu oraz wskazać, które atrybuty należy rzutować;
  • ostatni krok to uruchomienie metody CreateTable podając powyższy obiekt żądania jako parametr.

Spójrzcie na poniższy przykład, który obrazuje wszystkie powyższe kroki. W pierwszym z nich tworzymy tabelę WeatherData z globalnym indeksem wtórnym PrecipIndex. Kluczem partycji jest Data a jego kluczem sortowania Precipitation (czyli opad, np. deszczu). Wszystkie atrybuty tabeli są rzutowane na indeks. Użytkownicy mogą zapytać ten indeks, aby uzyskać dane pogodowe dla konkretnej daty, opcjonalnie sortując dane według ilości opadów:

public async Task<ActionResult<string>> TableWithGSI()
{
    string tableName = "WeatherData";

    // definicja atrybutów
    var attributeDefintion = new List<AttributeDefinition>()
    {
        {new AttributeDefinition {AttributeName = "Location", AttributeType = "S"} },
        {new AttributeDefinition {AttributeName = "Date", AttributeType = "S"} },
        {new AttributeDefinition {AttributeName = "Precipitation", AttributeType = "N"} },
    };

    // schemat klucza tabeli
    var tableKeySchema = new List<KeySchemaElement>()
    {
        {new KeySchemaElement {AttributeName = "Location", KeyType = "HASH"} }, // partition key
        {new KeySchemaElement {AttributeName = "Date", KeyType = "RANGE"} },    // sort key
    };

    // definicja indexu, tj. PrecipIndex
    var precipIndex = new GlobalSecondaryIndex
    {
        IndexName = "PrecipIndex",
        ProvisionedThroughput = new ProvisionedThroughput
        {
            ReadCapacityUnits = 10,
            WriteCapacityUnits = 1
        },
        Projection = new Projection { ProjectionType = "ALL" }
    };

    // schemat klucza indeksu
    var indexKeySchema = new List<KeySchemaElement>()
    {
        {new KeySchemaElement { AttributeName = "Date", KeyType = "HASH"}},        // partition key
        {new KeySchemaElement{AttributeName = "Precipitation",KeyType = "RANGE"}}  // sort key
    };

    precipIndex.KeySchema = indexKeySchema;

    var createTableRequest = new CreateTableRequest()
    {
        TableName = tableName,
        ProvisionedThroughput = new ProvisionedThroughput
        {
            ReadCapacityUnits = 5,
            WriteCapacityUnits = 1
        },
        AttributeDefinitions = attributeDefintion,
        KeySchema = tableKeySchema,
        GlobalSecondaryIndexes = { precipIndex }
    };

    var createTableResponse = _amazonDynamoDB.CreateTableAsync(createTableRequest);

    StringBuilder sb = new StringBuilder();
    sb.AppendLine($"TableName: {createTableResponse.Result.TableDescription.TableName}");
    sb.AppendLine($"TableStatus: {createTableResponse.Result.TableDescription.TableStatus}");

    return sb.ToString();
}

Po poprawnym wykonaniu powyższego kodu zobaczycie poniższy status: DynamoDB: tabela z GSI

Informacje o tabeli

W celu uzyskania informacji o tabeli (w tym o indeksie) możemy posłużyć się metodą DescribeTable. Dla każdego indeksu możemy uzyskać dostęp do jego nazwy, schematu klucza oraz rzutowanych atrybutów.

Pomijam podstawowe kroki znane już doskonale każdemu z Was. My musimy skupić się na wykorzystaniu metody DescribeTable oraz przekazaniu jako parametru obiektu żądania wykorzystując DescribeTableRequest. Spójrzcie na poniższy przykład:

public async Task<ActionResult<string>> DescribeTableWithGSI()
{
    StringBuilder sb = new StringBuilder();
    string tableName = "WeatherData";

    var response = _amazonDynamoDB.DescribeTableAsync(tableName);

    List<GlobalSecondaryIndexDescription> globalSecondaryIndexes = response.Result.Table.GlobalSecondaryIndexes;


    // W naszym przypadku jest tylko jeden globalny indeks wtórny
    // Poniższy kod jednak będzie działał również dla większej liczby indeksów
    foreach (var gsiDescription in globalSecondaryIndexes)
    {
        sb.AppendLine($"Informacja o indeksie dla: {gsiDescription.IndexName}");

        foreach (KeySchemaElement kse in gsiDescription.KeySchema)
        {
            sb.AppendLine($"Nazwa atrybutu: {kse.AttributeName} - Typ atrybutu: {kse.KeyType}");
        }

        var projection = gsiDescription.Projection;
        sb.AppendLine($"Rodzaj rzutowania: {projection.ProjectionType}");

        if (projection.ProjectionType.Equals("Include"))
        {
            sb.AppendLine($"Atrybuty nie wchodzące w skład rzutowania: {projection.NonKeyAttributes}");
        }
    }

    return sb.ToString();
}

Po poprawnym wykonaniu powyższego kodu zobaczycie poniższe podsumowanie: DynamoDB: DescribeTable na tabeli z GSI

Zapytanie o globalny indeks wtórny

Operację Scan do globalnego indeksu wtórnego możemy przygotować w taki sam sposób jak dla tabeli. Tym razem musimy jednak określić nazwę indeksu, kryteria zapytania dla klucza partycji i klucz sortowania indeksu (jeżeli są obecne) oraz atrybuty, które chcemy zwrócić. W naszym przykładzie indeksem jest PrecipIndex, który ma klucz partycji Date i klucz sortowania Precipation. Zapytanie o indeks zwraca wszystkie dane pogodowe dla konkretnej daty, gdzie opady deszczu są większe niż zero. Pomijam oczywiście kroki standardowe ale musimy pamiętać o jednej ważnej rzeczy: atrybut Date jest słowem zastrzeżonym/kluczowym w DynamoDB. W związku z tym musimy użyć nazwy atrybutu jako symbol zastępczy (tzw. placeholder) we właściwości KeyConditionExpression. Spójrzmy na poniższy przykład:

public async Task<ActionResult<string>> QueryOnGSI()
{
    // Dodajcie dane do tabeli jezeli chcecie zobaczyc zwrocone rezultaty
    // Mozecie zmodyfikowac dwie metody z poprzednich wpisów:
    // FillDataIntoCarPartsCatalog oraz AddItem lub podmienić dane w metodzie: LoadReplyData()
    await LoadWeatherData();
    StringBuilder sb = new StringBuilder();
    string tableName = "WeatherData";
    string indexName = "PrecipIndex";


    var request = new ScanRequest
    {
        TableName = tableName
    };

    var response = await _amazonDynamoDB.ScanAsync(request);

    var queryRequest = new QueryRequest()
    {
        TableName = tableName,
        IndexName = indexName,
        KeyConditionExpression = "#dt = :v_date and Precipitation > :v_precip", // spójrzcie na wspomniany atrybut Date
        ExpressionAttributeNames = new Dictionary<string, string>               // i jego definicję
        {
            {"#dt", "Date" }
        },
        ExpressionAttributeValues = new Dictionary<string, AttributeValue>
        {
            { ":v_date", new AttributeValue { S = "2022-18-11"} },
            { ":v_precip", new AttributeValue { N = "0"} }
        },
        ScanIndexForward = true
    };

    var result = await _amazonDynamoDB.QueryAsync(queryRequest);

    var items = result.Items;

    foreach (var item in items)
    {
        foreach (string attribute in item.Keys)
        {
            // Spójrzcie na definicje atrybutów w poprzednim wpisie
            // Wtedy zobaczycie dlaczego takie podejście do 'wypisywania' wartości atrybutów
            if (attribute == "Precipitation")
                sb.AppendLine($"Wartośc opadu: {item[attribute].N}");
            else
                sb.AppendLine($"Data: {item[attribute].S}");
        }

        sb.AppendLine();
    }

    return sb.ToString();
}

W moim przypadku zostało zwróconych kilka elementów: DynamoDB: query na tabeli z GSI