tldr; https://github.com/ChristopherBiscardi/dynamo-netlify-graphql-single-table-example
Alex Debrie wrote a fantastic piece on single and multi table data modeling with DynamoDB: The What, Why, and When of Single-Table Design with DynamoDB. I highly suggest you go read it. Great. Now that you've read it, in the post he makes a claim about DynamoDB single table design and its application to GraphQL.
The second place where you may want to avoid single-table design with DynamoDB is in GraphQL applications.
He supports this claim by claiming that resolvers are sequential, and therefore a query with multiple sub-selections would require multiple serial database requests.
The problem is that resolvers are essentially independent from each other. In the example above, the root resolver would execute first to find the User with ID 112233. This would involve a query to the database. Then, once that data is available, it would be passed to the Order resolver in order to fetch the relevant Orders for this User. This would make subsequent requests to the database to resolve those entities.
For this example I'll use Netlify functions and apollo-server-lambda, a JS implementation of a GraphQL server. The query in question fetches a user
of id "112233" and gets the firstName
, lastName
, addresses
, and orders
.
query {user( id: "112233" ){firstNamelastNameaddressesorders {orderDateamountstatus}}}
which requires a schema that looks something like this:
type Query {user: User}type User {firstName: StringlastName: Stringaddresses: Stringorders: Order}type Order {orderDate: Stringamount: Stringstatus: String}
So we'll say that we have Users, Addresses, and Orders. Our primary data type for access in this scenario is the User. Our user has some metadata of it's own, a number of addresses, and a number of orders. We can represent our data in the following single table design.
pk | sk | data |
---|---|---|
user#1 | metadata | { firstName, lastName } |
user#1 | address#1 | { street: "1 sesame st" } |
user#1 | address#2 | { street: "2 euclid ave" } |
user#1 | order#1 | { orderDate: "Jan", amount: 5, status: "delivered" } |
user#1 | order#2 | { orderDate: "Feb", amount: 3, status: "shipped" } |
user#1 | order#3 | { orderDate: "Mar", amount: 2, status: "pending" } |
In rest form, we could get all of the data with the following DynamoDB query.
const params = {TableName: "dynamo-single-table-graphql",KeyConditionExpression: "pk = :user",ExpressionAttributeValues: {":user": `user#112233`}};const { Items } = await docClient.query(params).promise();
It turns out that because we can't perform OR in a KeyConditionExpression, that we can't limit the data we're querying for. We can use FilterExpressions, but that's probably not what we want here.
Does this refute Alex's point. Well, not nearly as strongly as I wanted to. It does do away with the notion that resolvers need to fire sequentially, but we still need to fetch all of the data almost indiscriminately from the database anyway. So we get some UI side wins for using GraphQL (1 request to the backend) and we get some potential wins from API to DB (in this case one request again) but we don't get full control and have to spend all of the RCUs we would normally have spent anyway.