guregu / dynamo

expressive DynamoDB library for Go
Other
1.3k stars 179 forks source link

Querying for multiple item types in a single query #201

Closed chris closed 2 years ago

chris commented 2 years ago

I'm wondering what advice folks have for using this package to handle queries that produce multiple item/model types in a single query. This is the single table design pattern where you might do something like query for a user AND their orders (or some other item that is not represented by the same model as User)? This would be a query where say you only specify the PK, or maybe the PK and then an SK with some less than or greater than or other case, and that would then return something like:

PK SK
USER#123 USER#12345
USER#123 ORDER#789
USER#123 ORDER#312

Obviously this would need more complex marshalling and maybe just doesn't fit the general use of this package? The benefit here is avoiding doing two DynamoDB queries to get this data, so just wondering if others have done this using this Go package, or what suggestions folks have.

guregu commented 2 years ago

It's not well documented, but this package's equivalent of json.RawMessage is map[string]*dynamodb.AttributeValue. You can unmarshal your stuff to that type first and then unmarshal it again when you know the proper type.

Something like this (untested, but should illustrate the technique):

func getOrders(ctx context.Context, userID int) (user User, orders []Order, err error) {
    pk := fmt.Sprintf("USER#%d", userID)
    iter := db.Table("...").Get("PK", pk).Iter()

    var item map[string]*dynamodb.AttributeValue
    for iter.NextWithContext(ctx, &item) {
        sk := item["SK"].S
        switch {
        case sk == nil:
            err = fmt.Errorf("invalid sort key")
            return
        case strings.HasPrefix(*sk, "USER#"):
            err = dynamo.UnmarshalItem(item, &user)
            if err != nil {
                return
            }
        case strings.HasPrefix(*sk, "ORDER#"):
            var order Order
            err = dynamo.UnmarshalItem(item, &order)
            if err != nil {
                return
            }
            orders = append(orders, order)
        default:
            err = fmt.Errorf("get orders: unknown type: %s", *sk)
            return
        }
    }
    err = iter.Err()
    return
}

It'd be nice to have something built-in to deal with this. Maybe generics can help? 🤔

chris commented 2 years ago

Thank you @guregu ! That makes sense. Something built in may work, but this also seems fairly simple and there are uses cases of this technique where you might have different add-on types (e.g. depending on how you sort and filter, you might have one that is user+orders, and another that is user+friends or some such thing). But, as you said, maybe with generics you might be able to do that. Anyway, I appreciate the super fast response and info!

chris commented 2 years ago

Just a quick followup - I've implemented this and it works great.