Front-End Web & Mobile

DynamoDB on Mobile – Part 6: Global Secondary Indexes

Version 2 of the AWS Mobile SDK

  • This article and sample apply to Version 1 of the AWS Mobile SDK. If you are building new apps, we recommend you use Version 2. For details, please visit the AWS Mobile SDK page.
  • This content is being maintained for historical reference.

In Part 4 of our series, we discussed how to leverage local secondary indexes in an Amazon DynamoDB table while using the AWS Mobile SDKs in order to extend the ways we could query our users’ data. In this post, we will extend our example further to show additional ways a mobile app could query.

A User Data Table Revisited

In Part 1, we discussed a user data table in which multiple users’ data was stored. Our model looked like the following:

  • UserId – our Hash Key to uniquely identify the user, stored as a numeric value.
  • RecordId – our Range Key to identify a given record for the user, stored as a string.
  • Data – the data for the given record and user. As it is not being indexed, we can use any type supported by DynamoDB.

In Part 5, we discussed how we could use fine-grained access control for DynamoDB to limit access to only the logged-in user’s data. This is extremely useful to allow users to modify and see their own data, but what if we want to show some portion of all users’ data publicly? For instance, if our user data included a “high score” for a game, we may want to display their score as well as their name in a list for all users. We can accomplish this by using global secondary indexes.

Creating a Global Secondary Index

As with local secondary indexes, global secondary indexes can only be created along with the table and not added or removed afterward. If we add an index that reverses our table’s hash and range keys as shown below:

Creating the index

We’ll end up with an index that we can query by just RecordId:

Index summary

It’s important to note that global secondary indexes have their own provisioned throughput which, like the throughput for the table itself, can be modified to meet our use case:

Index throughput

Querying a Global Secondary Index

Once we have created our global secondary index, we can query it just as we could our table or local secondary index. If we want to get all the “highscore” records, we simply need to specify this in our query conditions:

iOS

// Create our dictionary of values
NSDictionary *conditions = [NSMutableDictionary new];

// Specify our key conditions (RecordId == "highscore")
DynamoDBCondition *recordIdCondition = [DynamoDBCondition new];
condition.comparisonOperator = @"EQ";
DynamoDBAttributeValue *recordId = [[DynamoDBAttributeValue alloc] initWithS:@"highscore"];
[recordIdCondition addAttributeValueList:recordId];
[conditions setObject:recordIdCondition forKey:@"RecordId"];

NSMutableDictionary *queryStartKey = nil;
do {
    DynamoDBQueryRequest *queryRequest = [DynamoDBQueryRequest new];
    queryRequest.tableName = @"UserData";
    queryRequest.exclusiveStartKey = queryStartKey;

    // Supply our conditions and specify the index to use
    queryRequest.keyConditions = conditions;
    queryRequest.indexName = @"Record-UserId-Index";

    // Process the query as normal
    DynamoDBQueryResponse *queryResponse = [[Constants ddb] query:queryRequest];

    // Each item in the result set is a NSDictionary of DynamoDBAttributeValue
    for (NSDictionary *item in queryResponse.items) {
        DynamoDBAttributeValue *userId = [item objectForKey:@"UserId"];
        DynamoDBAttributeValue *highScore = [item objectForKey:@"highscore"];
        NSLog(@"used id '%@' scored %@", userId.n, highScore.n);
    }
    // If the response lastEvaluatedKey has contents, 
    // that means there are more results
    queryStartKey = queryResponse.lastEvaluatedKey;

} while ([queryStartKey count] != 0);

Android

// Create our map of values
Map keyConditions = new HashMap();

// Specify our key conditions (RecordId == "highscore")
Condition hashKeyCondition = new Condition()
    .withComparisonOperator(ComparisonOperator.EQ.toString())
    .withAttributeValueList(new AttributeValue().withS("highscore"));
keyConditions.put("RecordId", hashKeyCondition);

Map lastEvaluatedKey = null;
do {
    QueryRequest queryRequest = new QueryRequest()
        .withTableName("UserData")
        .withKeyConditions(keyConditions)
        .withExclusiveStartKey(lastEvaluatedKey)
        .withIndexName("Record-UserId-Index");

    QueryResult queryResult = client.query(queryRequest);

    for (Map item : queryResult.getItems()) {
        Log.i(LOG_TAG, user id '" + item.get("UserId").getN() + "' scored " 
                + item.get("Data").getN());
    }

    // If the response lastEvaluatedKey has contents, 
    // that means there are more results
    lastEvaluatedKey = queryResult.getLastEvaluatedKey();

} while (lastEvaluatedKey != null);

We can repeat the same code to also retrieve our users’ display names if we choose to do so. If you want to see a different way to model a high scores table that allows for sorting of the results server side, see the DynamoDB documentation.

Integrating with Fine-Grained Access Control

Now that we have a view of our table that allows users to access all other users’ records by name, we need to be able to restrict access to that data in a way that makes sense for our app. By adding an additional statement to the policy we created in Part 5, we can restrict global access to only those records we want users to see.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:DeleteItem",
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query"
      ],
      "Resource": ["arn:aws:dynamodb:REGION:123456789012:table/UserData"],
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["${graph.facebook.com:id}"]}
      }
    },
    "Effect": "Allow",
      "Action": [
        "dynamodb:Query"
      ],
      "Resource": ["arn:aws:dynamodb:REGION:123456789012:table/UserData/index/RecordId-UserId-Index"],
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["highscore","name"]}
      }
    }
  ]
}

This additional statement will allow users to query against only our new global secondary index for the “highscore” and “name” records while disallowing all others.

Conclusion

We hope this shows how you can use global secondary indexes in your mobile apps. As more features are added to Amazon DynamoDB and/or the AWS Mobile SDKs, we will continue to post how you can leverage these features in your mobile apps. If you have suggestions for new features or posts you’d like to see, please consider leaving a comment here or in our forum.

Further Reading