dyntastic icon indicating copy to clipboard operation
dyntastic copied to clipboard

Document working with single-table design tables

Open straygar opened this issue 9 months ago • 2 comments

Assuming I have table following a single-table design principle and composite keys, and records like:

# User records
{ pk: 'User#<userId>', sk: 'User', ... }
{ pk: 'User#<userId>', sk: 'Review#<reviewId>', ... }
# Post records
{ pk: 'Post#<postId>, sk: 'Post', ... }
{ pk: 'Post#<postId>, sk: 'Comment#<commentId>', ...}

Based on the docs, it's unclear how I'd define the following models:

  • User
  • UserReview
  • Post
  • PostComment

I assume I could override all the CRUD methods to do something like:

class UserReview(Dyntastic):
  __table_name__: ...
  __hash_key__: 'pk'
  __range_key__: 'sk'

  @classmethod
  def get(cls, user_id: str, review_id: str) -> Self:
    pk = f'User#{user_id}'
    sk = f'Review#{review_id}'
    return cls.get(pk, sk)

Although this feels a little cumbersome.

straygar avatar Mar 12 '25 16:03 straygar

Hey! Unfortunately, there's not a whole lot of lift that this library currently does for a single-table design (though it's something that I'd love to eventually support well). Something like you wrote is probably about as good as you can get right now.

One thing that is supported is overriding get_model on a parent class, which may your life a little easier by reusing a single parent table definition that loads the appropriate model based on the key prefixes:

class MyTable(Dyntastic):
    __table_name__ = ...
    __hash_key__ = "pk"
    __range_key__ = "sk"

    pk: str
    sk: str

    @classmethod
    def get_model(cls, item):
        # Whatever logic needed from the pk/sk to determine which sub-model is relevant
        pk_label = item["pk"].split("#")[0]
        sk_label = item["sk"].split("#")[0]

        match (pk_label, sk_label):
            case ("User", "User"): return User
            case ("User", "Review"): return UserReview
            case ("Post", "Post"): return Post
            case ("Post", "Comment"): return Comment

class User(MyTable):
    # ... additional fields relevant for user here

class UserReview(MyTable):
    # ... additional fields relevant for user review here

...

This unfortunately still requires adding logic somewhere to inject the key prefixes when getting / constructing models, based on which model is being accessed. There's probably some clever way to do that without having to duplicate get and __init__ in all the subclasses, but I'd have to play around with it a bit to give a concrete suggestion.

nayaverdier avatar Mar 12 '25 17:03 nayaverdier

That's cool! I don't mind this solution that much and it definitely avoids a lot of copy-pasting. Thanks!

straygar avatar Mar 12 '25 17:03 straygar