DEV Community

loading...

DynamoDB with Go #12 - Condition on other item from item collection

Jędrzej Szczepaniak
・3 min read

Item collection is a set of attributes that share the same Partition Key. A common pattern is to model one to many relationships with item collections where PK represents commonality among items and SK distinguishes types of items.

As an example we can take information about user and orders placed by the user.

PK SK
1234 USERINFO
1234 ORDER#2017-03-04 00:00:00 +0000 UTC
1234 ORDER#2017-02-04 00:00:00 +0000 UTC

This is Single Table Design. We put different types of things into single table so that we can satisfy access patterns with minimal interactions with DynamoDB table. In this example we can display on a single page information about user with the latest orders while doing single query. PK represents ID of the user, and SK depending on the case represents basic user information or the order.

Now, that we've established that querying data gets simpler with Single Table Design, let us consider inserting data.

We need to be able to register a user. This use case is fairly simple. We need to make sure, that user with given ID doesn't exist in our system yet. The tool to do just that is to use expression like this one.

expr, err := expression.NewBuilder().
  WithCondition(expression.AttributeNotExists(expression.Name("pk"))).
  Build()
Enter fullscreen mode Exit fullscreen mode

When we have a user, we can place orders. Here is the thing. I would like to be able to place orders only for existing users. I would like to do something so that test passes. We want to get error when placing an order for the user that doesn't exist yet.

func TestInsertingOrderFailsBecauseUserDoesNotExist(t *testing.T) {
  ctx := context.Background()
  tableName := "ATable"
  db, cleanup := dynamo.SetupTable(t, ctx, tableName, "./template.yml")
  defer cleanup()

  _, err := db.PutItemWithContext(ctx, &dynamodb.PutItemInput{
    Item: map[string]*dynamodb.AttributeValue{
      "pk": dynamo.StringAttr("1234"),
      "sk": dynamo.StringAttr("ORDER#2017-03-04 00:00:00 +0000 UTC"),
    },
    TableName: aws.String(tableName),
  })
  assert.Error(t, err)
}
Enter fullscreen mode Exit fullscreen mode

Don't consider this a real world unit test. I am using _test.go file merely for executing queries. Anyway, this test fails. There is no error. Can we write simple condition that makes sure that user for which we want to place order exists?

Unfortunately, we can't do this easily. Writing conditions that relate to other items in the Item Collection is impossible. In order to make sure that user exists while introducing new order we need to use the transaction.

func TestInsertingOrderFailsBecauseUserDoesNotExist(t *testing.T) {
  ctx := context.Background()
  tableName := "ATable"
  db, cleanup := dynamo.SetupTable(t, ctx, tableName, "./template.yml")
  defer cleanup()

  expr, err := expression.NewBuilder().
    WithCondition(expression.AttributeExists(expression.Name("pk"))).
    WithUpdate(expression.Add(expression.Name("orders_count"), expression.Value(1))).
    Build()
  assert.NoError(t, err)

  _, err = db.TransactWriteItemsWithContext(ctx, &dynamodb.TransactWriteItemsInput{
    TransactItems: []*dynamodb.TransactWriteItem{
      {
        Put: &dynamodb.Put{
          Item: map[string]*dynamodb.AttributeValue{
            "pk": {S: aws.String("1234")},
            "sk": {S: aws.String("ORDER#2017-03-04 00:00:00 +0000 UTC")},
          },
          TableName: aws.String(tableName),
        },
      },
      {
        Update: &dynamodb.Update{
          ConditionExpression:       expr.Condition(),
          ExpressionAttributeValues: expr.Values(),
          ExpressionAttributeNames:  expr.Names(),
          UpdateExpression:          expr.Update(),
          Key: map[string]*dynamodb.AttributeValue{
            "pk": {S: aws.String("1234")},
            "sk": {S: aws.String("USERINFO")},
          },
          TableName: aws.String(tableName),
        },
      },
    },
  })

  assert.Error(t, err)
  _, ok := err.(*dynamodb.TransactionCanceledException)
  assert.True(t, ok)
}
Enter fullscreen mode Exit fullscreen mode

Transaction has two items. First one (Put) is what we really want to do here which is placing new order. Additionally, there is an update for item that represents information about the user. I did it so that I can put condition on that update operation. The condition says exactly what we wanted. Transaction will succeed only for existing user.

You can notice however that I am increasing orders_count which wasn't talked about as a requirement for this example. It doesn't really matter what gets updated, but you cannot perform empty update. We could choose to update some helper attribute to "" but I wanted to make it useful. We are killing two birds with one stone. We get orders count for free.

Takeaway from this episode is the following one.

If you want to base the condition of your operation on other item - you need to use transaction.

Discussion (0)