Paweł Łukasiewicz
2024-03-10
Paweł Łukasiewicz
2024-03-10
Udostępnij Udostępnij Kontakt
Wprowadzenie

Operacja Query pozwala na wykonywanie zapytań do tabeli lub indeksu wtórnego. Musimy podać wartość klucza partycji i warunek równości. Jeżeli tabela lub indeks ma klucz sortowania możemy zawęzić/udoskonalić nasze zapytanie podając wartość klucza sortowania i warunek.

Po sporej dawce teorii spójrzmy jakie kroki należy wykonać, aby przygotować zapytanie przy użyciu niskopoziomowego interfejsu API AWS SDK dla .NET:

  • utworzenie instancji klasy AmazonDynamoDBClient;
  • utworzenie instancji klasy QueryRequest i podanie parametrów operacji zapytania;
  • uruchomienie metody Query i przekazanie obiektu QueryRequest, który przygotowaliśmy w poprzednim kroku. Odpowiedź zawiera obiekt QueryResult, który dostarcza wszystkie elementy zwrócone przez zapytanie.

Poniższy przykład będzie bazował na tym dostępnym w oficjalnej dokumentacji AWS. Dodatkowym benefitem będzie możliwość załadowania danych przez nich przygotowanych (wszelkie kroki możecie znaleźć tutaj: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SampleData.html

My oczywiście przejdziemy przez kolejne etapy i nie będziemy niczego zakładać, np. istnienia tabeli. Przygotujemy tę tabelę, wypełnimy danymi a dopiero na ostatnim etapie wykonamy zapytanie. Tabelę, która utworzymy nazwiemy Reply (odpowiedzi danego wątku na forum). Każdy wątek będzie miał unikalny identyfikator i może mieć zero lub więcej odpowiedzi. Definicja tabeli będzie się składała zarówno z Id (partition key) jak i ReplyDateTime (sort key).

W pierszym kroku wykorzystamy metodę pomocniczą z jednego z poprzenich wpisów, która utworzy nam tabelę oraz dodamy metodę, która poczeka aż tabela zostanie utworzona – dopiero wtedy zajmiemy się wypełnieniem jej danym. Spójrzcie na przykładową implementację:

#region Useful methods

private async Task<ActionResult<string>> CreateSampleTable()
{
    var request = new CreateTableRequest
    {
        TableName = TableName3, // TableName3 = "Reply"
        AttributeDefinitions = new List<AttributeDefinition>()
        {
            new AttributeDefinition
            {
                AttributeName = "Id",
                AttributeType = "S"
            },
            new AttributeDefinition
            {
            AttributeName = "ReplyDateTime",
            AttributeType = "S"
            }
        },
        KeySchema = new List<KeySchemaElement>
        {
            new KeySchemaElement
            {
                AttributeName = "Id",
                KeyType = "HASH" // partition key
            },
            new KeySchemaElement
            {
                AttributeName = "ReplyDateTime",
                KeyType = "RANGE" //sort key
            }
        },
        ProvisionedThroughput = new ProvisionedThroughput
        {
            ReadCapacityUnits = 5,
            WriteCapacityUnits = 10
        }
    };

    var response = await _amazonDynamoDB.CreateTableAsync(request);

    var tableDescription = response.TableDescription;

    StringBuilder sb = new StringBuilder(); 
    sb.AppendLine($"Tabela: {tableDescription.TableName} | Status: {tableDescription.TableStatus}"); 
    sb.AppendLine($"ReadCapacityUnit: {tableDescription.ProvisionedThroughput.ReadCapacityUnits}"); 
    sb.AppendLine($"WriteCapacityUnit: {tableDescription.ProvisionedThroughput.WriteCapacityUnits}");
    
    WaitUntilTableReady(TableName2);
    
    return sb.ToString();
}

private async void WaitUntilTableReady(string tableName)
{
    string status = null;

    // Czekamy na utworzenie tabeli. Wywołujemy metodę DescribeTable.
    do
    {
        Thread.Sleep(5000); // czekamy 5 sekund
        try
        {
            var request = await _amazonDynamoDB.DescribeTableAsync(tableName);
            status = request.Table.TableStatus;
        }
        catch (ResourceNotFoundException)
        {
            // Metoda DescribeTable jest ‘w końcu spójna’ (eventually consistent).
            // Może się więc zdarzyć, że otrzymamy zwrotkę, że ‘dana tabela nie została (jeszcze) znaleziona’.
        }
    } while (status != "ACTIVE");
}

#endregion
Po pomyślnym wykonaniu powyższego kodu powinniście zobaczyć poniższy komunikat: DynamoDB: tworzenie nowej tabeli

Dane w tabeli Reply

Nasza tabela została utworzona, dostaliśmy również informacje, że jest aktywna. Możemy teraz dodać kilka elementów. Wykorzystamy zawartość pliku Reply.json, który możecie znaleźć w przykładach udostępnionych przez AWS pod tym adresem https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SampleData.html. Ja przygotuje żądanie wykorzystując wiedzę z poprzednich wpisów:

private async Task<ActionResult<HttpStatusCode>> LoadReplyData()
{
    var request = new BatchWriteItemRequest()
    {
        RequestItems = new Dictionary<string, List<WriteRequest>>
        {
            {
                TableName3, new List<WriteRequest>
                {
                    new WriteRequest
                    {
                        PutRequest = new PutRequest
                        {
                            Item = new Dictionary<string, AttributeValue>
                            {
                                { "Id", new AttributeValue {S = "Amazon DynamoDB#DynamoDB Thread 1"} },
                                { "ReplyDateTime", new AttributeValue {S = "2015-09-15T19:58:22.947Z"} },
                                { "Message", new AttributeValue{S = "DynamoDB Thread 1 Reply 1 text"} },
                                { "PostedBy", new AttributeValue {S = "User A"} },
                            }
                        }
                    },
                    new WriteRequest
                    {
                        PutRequest = new PutRequest
                        {
                            Item = new Dictionary<string, AttributeValue>
                            {
                                { "Id", new AttributeValue {S = "Amazon DynamoDB#DynamoDB Thread 1"} },
                                { "ReplyDateTime", new AttributeValue {S = "2015-09-22T19:58:22.947Z"} },
                                { "Message", new AttributeValue{S = "DynamoDB Thread 1 Reply 2 text"} },
                                { "PostedBy", new AttributeValue {S = "User B"} },
                            }
                        }
                    },
                    new WriteRequest
                    {
                        PutRequest = new PutRequest
                        {
                            Item = new Dictionary<string, AttributeValue>
                            {
                                { "Id", new AttributeValue {S = "Amazon DynamoDB#DynamoDB Thread 2"} },
                                { "ReplyDateTime", new AttributeValue {S = "2015-09-29T19:58:22.947Z"} },
                                { "Message", new AttributeValue{S = "DynamoDB Thread 2 Reply 1 text"} },
                                { "PostedBy", new AttributeValue {S = "User A"} },
                            }
                        }
                    },
                    new WriteRequest
                    {
                        PutRequest = new PutRequest
                        {
                            Item = new Dictionary<string, AttributeValue>
                            {
                                { "Id", new AttributeValue {S = "Amazon DynamoDB#DynamoDB Thread 2"} },
                                { "ReplyDateTime", new AttributeValue {S = "2015-10-05T19:58:22.947Z"} },
                                { "Message", new AttributeValue{S = "DynamoDB Thread 2 Reply 2 text"} },
                                { "PostedBy", new AttributeValue {S = "User A"} },
                            }
                        }
                    },
                }
            }
        }
    };

    var response = await _amazonDynamoDB.BatchWriteItemAsync(request);

    return response.HttpStatusCode;

}

Pamiętajcie, żeby rozszerzyć implementacje, którą przygotowaliśmy w poprzedniej części wpisu o wykonanie metody dodającej dane do świeżo utworzonej tabeli. W moim przypadku cała operacja przebiegła pomyślnie: DynamoDB: dodawanie elementów do tabeli

Możemy przejść do kolejnej części wpisu, w której przygotujemy Query.

Query

W dwóch powyższych częściach wpisu przygotowaliśmy tabelę oraz wypełniliśmy ją danymi. W naszym przypadku każdy wątek na forum ma unikalny identyfikator (Id) i może mieć zero lub więcej odpowiedzi (replies). Jest to również powodem, dlaczego nasz klucz główny składa się zarówno z Id (partion key) jak i ReplyDateTime (sort key).

Poniższe zapytanie pobiera wszystkie odpowiedzi dla określonego tematu. Zapytanie wymaga podania nazwy tabeli jak i wartości dla atrybutu Subject:

public async Task<ActionResult<string>> QueryOnOrdersTable()
{
    // Setup
    var response = await CreateSampleTable();
    var loadResponse = LoadReplyData();
    StringBuilder sb = new StringBuilder();

    // Query
    var request = new QueryRequest
    {
        TableName = TableName3,
        KeyConditionExpression = "Id = :v_id",
        ExpressionAttributeValues = new Dictionary<string, AttributeValue>
        { { ":v_id", new AttributeValue { S = "Amazon DynamoDB#DynamoDB Thread 1" } } },
    };

    var queryResponse = await _amazonDynamoDB.QueryAsync(request);

    foreach (Dictionary<string, AttributeValue> item in queryResponse.Items)
    {
        // Przetwarzanie wyników
        // Zerknicie tutaj: https://www.plukasiewicz.net/AwsLambda/DynamoDbLambda
        // jeżeli checie pobrać wartości danych atrybutów w dokładniejszy sposób
        string json = JsonConvert.SerializeObject(item, Formatting.Indented);

        sb.AppendLine(json);
    }

    return sb.ToString();
}

Parametry opcjonalne

Query obsługuje również kilka parametrów opcjonalnych. Możemy np. zawęzić wyniki wyszukiwania w poprzednim zapytaniu, aby zwrócić odpowiedzi jedynie z dwóch ostatnich tygodni poprzez określenie warunku. Warunek ten nazywany jest warunkiem klucza sortowania, ponieważ DynamoDB określa warunek zapytania względem klucza sortowania klucza głównego. Możemy również określić inne opcjonalne parametry, aby pobrać tylko określną listę atrybutów z elementów w wyniku zapytania.

Spójrzmy zatem na poniższy przykład pobierający odpowiedzi opublikowane w przeciągu ostatnich 15 dni. W poniższym przykładzie określamy 3 parametry opcjonalne:

  • KeyConditionExpression - w celu pobrania odpowiedzi z ostatnich 15 dni;
  • ProjectionExpression - w celu określenia listy atrybutów do pobrania dla elementów z wyniku zapytania;
  • ConsistentRead - w cely wykonania silnie spójnego odczytu
public async Task<ActionResult<string>> QueryOnOrdersTableAdditionalParameters()
{
    // Setup
    StringBuilder sb = new StringBuilder();
    DateTime twoWeeksAgoDate = DateTime.UtcNow - TimeSpan.FromDays(15);
    string twoWeeksAgoString = twoWeeksAgoDate.ToString(AWSSDKUtils.ISO8601DateFormat);

    var request = new QueryRequest
    {
        TableName = TableName3,
        KeyConditionExpression = "Id = :v_id and ReplyDateTime > :v_twoWeeksAgo",
        ExpressionAttributeValues = new Dictionary<string, AttributeValue>
        {
            { ":v_id", new AttributeValue { S = "Amazon DynamoDB#DynamoDB Thread 1" } },
            { ":v_twoWeeksAgo", new AttributeValue { S = twoWeeksAgoString  } }
        },
        ProjectionExpression = "Subject, ReplyDateTime, PostedBy",
        ConsistentRead = true
    };

    var queryResponse = await _amazonDynamoDB.QueryAsync(request);
    foreach (Dictionary<string, AttributeValue> item in queryResponse.Items)
    {
        // Przetwarzanie wyników
        // Zerknicie tutaj: https://www.plukasiewicz.net/AwsLambda/DynamoDbLambda
        // jeżeli checie pobrać wartości danych atrybutów w dokładniejszy sposób
        string json = JsonConvert.SerializeObject(item, Formatting.Indented);

        sb.AppendLine(json);
    }

    return sb.ToString();
}

Nic również nie stoi na przeszkodzie w ograniczeniu rozmiaru strony lub ilości elementów na stronie – odbywa się to przy wykorzystaniu parametru Limit. Za każdym razem, gdy uruchomimy Query otrzymujemy jedną stronę wyników, która ma określoną liczbę elementów. Aby pobrać następną stronę należy ponownie uruchomić zapytanie podając wartość klucza głównego ostatniego elementu na poprzedniej stronie tak, aby metoda mogła zwrócić następny zestaw elementów. Przekazujemy tę informację w żądaniu poprzez ustawienie właściwości ExclusiveStartKey. Początkowo wartość tego atrybutu może mieć wartość null. Aby poprawnie pobrać kolejne strony musimy zaktualizować wartość tej właściwości do klucza głównego ostatniego elementu na poprzedniej stronie.

Spójrzmy na kolejny przykład, który tym razem określa parametry opcjonalne Limit oraz ExclusiveStartKey. Pętla do/while kontynuuje zbieranie wyników z danej strony, aż parametr LastEvaluatedKey zwróci wartość null:

public async Task<ActionResult<string>> QueryOnReplyLimit()
{
    // Setup
    Dictionary<string, AttributeValue> lastKeyEvaluated = null;
    StringBuilder sb = new StringBuilder();

    do
    {
        var request = new QueryRequest
        {
            TableName = TableName3,
            KeyConditionExpression = "Id = :v_id",
            ExpressionAttributeValues = new Dictionary<string, AttributeValue>
            {{ ":v_id", new AttributeValue { S = "Amazon DynamoDB#DynamoDB Thread 2" } }},
            // parametr opcjonalny.
            Limit = 1,
            ExclusiveStartKey = lastKeyEvaluated
        };

        var queryResponse = await _amazonDynamoDB.QueryAsync(request);
        foreach (Dictionary<string, AttributeValue> item in queryResponse.Items)
        {
            // Przetwarzanie wyników
            // Zerknicie tutaj: https://www.plukasiewicz.net/AwsLambda/DynamoDbLambda
            // jeżeli checie pobrać wartości danych atrybutów w dokładniejszy sposób
            string json = JsonConvert.SerializeObject(item, Formatting.Indented);

            sb.AppendLine(json);
        }

        return sb.ToString();
    } while (lastKeyEvaluated != null && lastKeyEvaluated.Count != 0);
}