Modeling Relational Data in DynamoDB#

DynamoDB offers various strategies for implementing relationships between entities. I have created a GitHub repository, dynamodb_modeler-project that thoroughly discusses the different options, comparing their pros and cons, and demonstrates an ultimate strategy that can accommodate arbitrary numbers of entities and relationships while allowing for smooth scalability. The pynamodb_mate.patterns.relationship module implements this ultimate strategy, making it easy to use in production environments.

To showcase the simplicity and effectiveness of this library, please refer to the following example and compare it with the implementation in the Reinvent YouTube in DynamoDB project without using the library. By doing so, you will gain a clear understanding of how this library streamlines the process of modeling relationships in DynamoDB.

The example provided demonstrates how the library abstracts away the complexities of managing relationships, allowing developers to focus on the core business logic. With just a few lines of code, you can define and establish relationships between entities, eliminating the need for manual implementation of complex data modeling patterns.

By leveraging the pynamodb_mate.patterns.relationship module, developers can save significant time and effort in designing and implementing scalable and maintainable data models in DynamoDB. The library provides a high-level, intuitive interface that encapsulates best practices and optimized strategies for handling relationships efficiently.

Whether you are building a new application from scratch or migrating an existing one to DynamoDB, the pynamodb_mate library offers a powerful toolset to simplify the process of modeling and managing relationships. It enables developers to create robust and scalable data models with ease, ensuring optimal performance and flexibility as the application grows.

I encourage you to explore the example provided and dive into the GitHub repository to learn more about the capabilities and benefits of the pynamodb_mate library. By adopting this library in your DynamoDB projects, you can streamline your development process, improve code maintainability, and unlock the full potential of DynamoDB’s scalability and performance.

[1]:
import typing as T
import pynamodb_mate.api as pm
from iterproxy import IterProxy

rl = pm.patterns.relationship # make the namespace shorter

Declare Entity Table and Lookup Index#

[2]:
class LookupIndex(rl.BaseLookupIndex):
    class Meta:
        index_name = "lookup-index"
        projection = pm.AllProjection()


class Entity(rl.BaseEntity):
    """
    :param pk: partition key can only has alpha letter and hyphen.
    """

    class Meta:
        table_name = "entity"
        region = "us-east-1"
        billing_mode = pm.constants.PAY_PER_REQUEST_BILLING_MODE

    lookup_index = LookupIndex()

    def print_vip_attrs(self):
        print(self.get_vip_attrs())

Declare Data Model for Each Entity#

In this ultimate data modeling strategy, all entities are stored within a single table. To enhance code organization, readability, and maintainability, it is highly recommended to define subclasses for each entity type. By creating separate subclasses, you can encapsulate entity-specific logic, attributes, and behavior within their respective classes.

This approach offers several benefits:

  1. Modularity: Each entity subclass can be developed, tested, and maintained independently, promoting a modular and loosely coupled codebase.

  2. Code Reusability: Common functionality and attributes can be defined in a base class, which can be inherited by the entity subclasses. This promotes code reuse and reduces duplication.

  3. Readability: By separating entity-specific logic into dedicated subclasses, the codebase becomes more readable and self-explanatory. Developers can quickly understand the purpose and behavior of each entity without sifting through a monolithic class.

  4. Maintainability: As the application evolves and new requirements emerge, having separate entity subclasses makes it easier to modify and extend the codebase. Changes can be made to specific entity subclasses without impacting the entire system.

  5. Scalability: When the number of entities grows, managing them within separate subclasses allows for better scalability. New entity types can be added seamlessly without modifying the existing codebase extensively.

By leveraging the power of inheritance and defining subclasses for each entity, you can create a well-structured and maintainable codebase that is easier to understand, modify, and scale as your application evolves.

Remember to keep the subclasses focused on entity-specific logic and attributes, while leveraging the base class for common functionality and behavior. This approach will help you build a robust and efficient DynamoDB data model that can accommodate future growth and changes.

[3]:
class User(Entity):
    lookup_index = LookupIndex()

    @property
    def user_id(self) -> str:
        return self.pk_id


class Video(Entity):
    lookup_index = LookupIndex()

    @property
    def video_id(self) -> str:
        return self.pk_id


class Channel(Entity):
    lookup_index = LookupIndex()

    @property
    def channel_id(self) -> str:
        return self.pk_id


class Playlist(Entity):
    lookup_index = LookupIndex()

    @property
    def playlist_id(self) -> str:
        return self.pk_id

Declare Data Model for Each Relationship#

Similarly, a relationship is just a DynamoDB item. You also need a class for each relationship.

[4]:
class VideoOwnership(Entity):
    lookup_index = LookupIndex()

    @property
    def video_id(self) -> str:
        return self.pk_id

    @property
    def user_id(self) -> str:
        return self.sk_id


class ChannelOwnership(Entity):
    lookup_index = LookupIndex()

    @property
    def channel_id(self) -> str:
        return self.pk_id

    @property
    def user_id(self) -> str:
        return self.sk_id


class PlaylistOwnership(Entity):
    lookup_index = LookupIndex()

    @property
    def playlist_id(self) -> str:
        return self.pk_id

    @property
    def user_id(self) -> str:
        return self.sk_id


class VideoChannelAssociation(Entity):
    lookup_index = LookupIndex()

    @property
    def video_id(self) -> str:
        return self.pk_id

    @property
    def channel_id(self) -> str:
        return self.sk_id


class VideoPlaylistAssociation(Entity):
    lookup_index = LookupIndex()

    @property
    def video_id(self) -> str:
        return self.pk_id

    @property
    def playlist_id(self) -> str:
        return self.sk_id


class ViewerSubscribeYoutuber(Entity):
    lookup_index = LookupIndex()

    @property
    def viewer_id(self) -> str:
        return self.pk_id

    @property
    def youtuber_id(self) -> str:
        return self.sk_id


class ViewerSubscribeChannel(Entity):
    lookup_index = LookupIndex()

    @property
    def viewer_id(self) -> str:
        return self.pk_id

    @property
    def channel_id(self) -> str:
        return self.sk_id

Declare Type Hint and Iterator Proxy Object#

This is recommended but optional.

[5]:
T_Entity = T.Union[
    Entity,
    User,
    Video,
    Channel,
    Playlist,
    VideoOwnership,
    ChannelOwnership,
    PlaylistOwnership,
    VideoChannelAssociation,
    VideoPlaylistAssociation,
    ViewerSubscribeYoutuber,
    ViewerSubscribeChannel,
]

T_Entity_Type = T.Union[
    T.Type[Entity],
    T.Type[User],
    T.Type[Video],
    T.Type[Channel],
    T.Type[Playlist],
    T.Type[VideoOwnership],
    T.Type[ChannelOwnership],
    T.Type[PlaylistOwnership],
    T.Type[VideoChannelAssociation],
    T.Type[VideoPlaylistAssociation],
    T.Type[ViewerSubscribeYoutuber],
    T.Type[ViewerSubscribeChannel],
]


class UserIterProxy(IterProxy[User]):
    pass


class VideoIterProxy(IterProxy[Video]):
    pass


class ChannelIterProxy(IterProxy[Channel]):
    pass


class PlaylistIterProxy(IterProxy[Playlist]):
    pass


class VideoOwnershipIterProxy(IterProxy[VideoOwnership]):
    pass


class ChannelOwnershipIterProxy(IterProxy[ChannelOwnership]):
    pass


class PlaylistOwnershipIterProxy(IterProxy[PlaylistOwnership]):
    pass


class VideoChannelAssociationIterProxy(IterProxy[VideoChannelAssociation]):
    pass


class VideoPlaylistAssociationIterProxy(IterProxy[VideoPlaylistAssociation]):
    pass


class ViewerSubscribeYoutuberIterProxy(IterProxy[ViewerSubscribeYoutuber]):
    pass


class ViewerSubscribeChannelIterProxy(IterProxy[ViewerSubscribeChannel]):
    pass

Declare Relationship Settings#

The RelationshipSetting object is a central component of the library that stores all the metadata related to entity relationships. It serves as a configuration object that defines the characteristics and behavior of entities and their relationships.

When using the library, you need to declare an EntityType for each entity in your data model. An EntityType represents a unique entity type and is associated with a specific entity class. This entity class serves as a blueprint for creating instances of the entity and encapsulates the entity’s attributes and behavior.

To define relationships between entities, you can use the OneToManyRelationshipType and ManyToManyRelationshipType objects. These objects represent the type of relationship between two entities. Each relationship type should have a unique name that identifies the relationship and specifies the associated entity classes involved in the relationship.

One of the key benefits of using this library is the ability to declare your business logic and queries as methods within the entity classes. Instead of writing raw DynamoDB queries directly, you can encapsulate the logic and queries inside methods. This approach provides several advantages:

  1. Clarity: By defining business logic and queries as methods, the code becomes more readable and self-explanatory. Each method can have a descriptive name that clearly conveys its purpose, making the codebase easier to understand and maintain.

  2. Encapsulation: Methods allow you to encapsulate the implementation details of business logic and queries within the entity classes. This encapsulation hides the complexity of the underlying DynamoDB operations and provides a higher-level abstraction for interacting with the data.

  3. Reusability: Methods promote code reuse by allowing you to define common logic and queries once and invoke them from multiple places in your application. This reduces code duplication and makes the codebase more maintainable.

  4. Testability: By encapsulating business logic and queries in methods, you can easily write unit tests to verify the correctness of your code. Testing individual methods is simpler and more focused compared to testing raw DynamoDB queries scattered throughout the codebase.

By leveraging the RelationshipSetting object, declaring entity types, defining relationship types, and encapsulating business logic and queries in methods, you can create a clean, maintainable, and expressive codebase for working with DynamoDB. This approach simplifies the development process, improves code organization, and enhances the overall readability and maintainability of your application.

[6]:
user_entity_type = rl.EntityType(name="User", klass=User)
video_entity_type = rl.EntityType(name="Video", klass=Video)
channel_entity_type = rl.EntityType(name="Channel", klass=Channel)
playlist_entity_type = rl.EntityType(name="Playlist", klass=Playlist)

video_ownership_relationship_type = rl.OneToManyRelationshipType(
    name="Video-Ownership",
    klass=VideoOwnership,
    one_type=user_entity_type,
    many_type=video_entity_type,
)
channel_ownership_relationship_type = rl.OneToManyRelationshipType(
    name="Channel-Ownership",
    klass=ChannelOwnership,
    one_type=user_entity_type,
    many_type=channel_entity_type,
)
playlist_ownership_relationship_type = rl.OneToManyRelationshipType(
    name="Playlist-Ownership",
    klass=PlaylistOwnership,
    one_type=user_entity_type,
    many_type=playlist_entity_type,
)

video_channel_association_relationship_type = rl.ManyToManyRelationshipType(
    name="Video-Channel-Association",
    klass=VideoChannelAssociation,
    left_type=video_entity_type,
    right_type=channel_entity_type,
)
video_playlist_association_relationship_type = rl.ManyToManyRelationshipType(
    name="Video-Playlist-Association",
    klass=VideoPlaylistAssociation,
    left_type=video_entity_type,
    right_type=playlist_entity_type,
)
viewer_subscribe_youtuber_relationship_type = rl.ManyToManyRelationshipType(
    name="Viewer-Subscribe-Youtuber",
    klass=ViewerSubscribeYoutuber,
    left_type=user_entity_type,
    right_type=user_entity_type,
)
viewer_subscribe_channel_relationship_type = rl.ManyToManyRelationshipType(
    name="Viewer-Subscribe-Channel",
    klass=ViewerSubscribeChannel,
    left_type=user_entity_type,
    right_type=channel_entity_type,
)


class RelationshipSetting(rl.RelationshipSetting):
    def new_user(
        self,
        id: str,
        name: str,
        save: bool = True,
    ) -> T.Optional[User]:
        return self.new_entity(e_type=user_entity_type, id=id, name=name, save=save)

    def new_video(
        self,
        id: str,
        name: str,
        save: bool = True,
    ) -> T.Optional[Video]:
        return self.new_entity(e_type=video_entity_type, id=id, name=name, save=save)

    def new_channel(
        self,
        id: str,
        name: str,
        save: bool = True,
    ) -> T.Optional[Channel]:
        return self.new_entity(e_type=channel_entity_type, id=id, name=name, save=save)

    def new_playlist(
        self,
        id: str,
        name: str,
        save: bool = True,
    ) -> T.Optional[Playlist]:
        return self.new_entity(e_type=playlist_entity_type, id=id, name=name, save=save)

    def list_users(self) -> UserIterProxy:
        return self.list_entities(entity_type=user_entity_type)

    def list_videos(self) -> UserIterProxy:
        return self.list_entities(entity_type=video_entity_type)

    def list_channels(self) -> UserIterProxy:
        return self.list_entities(entity_type=channel_entity_type)

    def list_playlists(self) -> UserIterProxy:
        return self.list_entities(entity_type=playlist_entity_type)

    def set_video_owner(
        self,
        conn: pm.Connection,
        video_id: str,
        user_id: str,
    ):
        self.set_one_to_many(
            conn=conn,
            one_to_many_r_type=video_ownership_relationship_type,
            many_entity_id=video_id,
            one_entity_id=user_id,
        )

    def set_channel_owner(
        self,
        conn: pm.Connection,
        channel_id: str,
        user_id: str,
    ):
        self.set_one_to_many(
            conn=conn,
            one_to_many_r_type=channel_ownership_relationship_type,
            many_entity_id=channel_id,
            one_entity_id=user_id,
        )

    def set_playlist_owner(
        self,
        conn: pm.Connection,
        playlist_id: str,
        user_id: str,
    ):
        self.set_one_to_many(
            conn=conn,
            one_to_many_r_type=playlist_ownership_relationship_type,
            many_entity_id=playlist_id,
            one_entity_id=user_id,
        )

    def find_videos_created_by_a_user(
        self,
        user_id: str,
    ) -> VideoIterProxy:
        return self.find_many_by_one(
            one_to_many_r_type=video_ownership_relationship_type,
            one_entity_id=user_id,
        )

    def find_channels_created_by_a_user(
        self,
        user_id: str,
    ) -> ChannelIterProxy:
        return self.find_many_by_one(
            one_to_many_r_type=channel_ownership_relationship_type,
            one_entity_id=user_id,
        )

    def find_playlists_created_by_a_user(
        self,
        user_id: str,
    ) -> PlaylistIterProxy:
        return self.find_many_by_one(
            one_to_many_r_type=playlist_ownership_relationship_type,
            one_entity_id=user_id,
        )

    def add_video_to_channel(
        self,
        video_id: str,
        channel_id: str,
    ):
        self.set_many_to_many(
            many_to_many_r_type=video_channel_association_relationship_type,
            left_entity_id=video_id,
            right_entity_id=channel_id,
        )

    def add_video_to_playlist(
        self,
        video_id: str,
        playlist_id: str,
    ):
        self.set_many_to_many(
            many_to_many_r_type=video_playlist_association_relationship_type,
            left_entity_id=video_id,
            right_entity_id=playlist_id,
        )

    def viewer_subscribe_youtuber(
        self,
        viewer_id: str,
        youtuber_id: str,
    ):
        self.set_many_to_many(
            many_to_many_r_type=viewer_subscribe_youtuber_relationship_type,
            left_entity_id=viewer_id,
            right_entity_id=youtuber_id,
        )

    def viewer_subscribe_channel(
        self,
        viewer_id: str,
        channel_id: str,
    ):
        self.set_many_to_many(
            many_to_many_r_type=viewer_subscribe_channel_relationship_type,
            left_entity_id=viewer_id,
            right_entity_id=channel_id,
        )

    def find_videos_in_channel(
        self,
        channel_id: str,
    ) -> VideoChannelAssociationIterProxy:
        return self.find_many_by_many(
            many_to_many_r_type=video_channel_association_relationship_type,
            entity_id=channel_id,
            lookup_by_left=False,
        )

    def find_channels_that_has_video(
        self,
        video_id: str,
    ) -> VideoChannelAssociationIterProxy:
        return self.find_many_by_many(
            many_to_many_r_type=video_channel_association_relationship_type,
            entity_id=video_id,
            lookup_by_left=True,
        )

    def find_videos_in_playlist(
        self,
        playlist_id: str,
    ) -> VideoPlaylistAssociationIterProxy:
        return self.find_many_by_many(
            many_to_many_r_type=video_playlist_association_relationship_type,
            entity_id=playlist_id,
            lookup_by_left=False,
        )

    def find_playlists_that_has_video(
        self,
        video_id: str,
    ) -> VideoPlaylistAssociationIterProxy:
        return self.find_many_by_many(
            many_to_many_r_type=video_playlist_association_relationship_type,
            entity_id=video_id,
            lookup_by_left=True,
        )

    def find_subscribers_for_youtuber(
        self,
        youtuber_id: str,
    ) -> ViewerSubscribeYoutuberIterProxy:
        return self.find_many_by_many(
            many_to_many_r_type=viewer_subscribe_youtuber_relationship_type,
            entity_id=youtuber_id,
            lookup_by_left=False,
        )

    def find_subscribed_youtubers(
        self,
        user_id: str,
    ) -> ViewerSubscribeYoutuberIterProxy:
        return self.find_many_by_many(
            many_to_many_r_type=viewer_subscribe_youtuber_relationship_type,
            entity_id=user_id,
            lookup_by_left=True,
        )

    def find_subscribers_for_channel(
        self,
        channel_id: str,
    ) -> ViewerSubscribeChannelIterProxy:
        return self.find_many_by_many(
            many_to_many_r_type=viewer_subscribe_channel_relationship_type,
            entity_id=channel_id,
            lookup_by_left=False,
        )

    def find_subscribed_channels(
        self,
        user_id: str,
    ) -> ViewerSubscribeChannelIterProxy:
        return self.find_many_by_many(
            many_to_many_r_type=viewer_subscribe_channel_relationship_type,
            entity_id=user_id,
            lookup_by_left=True,
        )

    def remove_video_from_channel(
        self,
        video_id: str,
        channel_id: str,
    ):
        self.unset_many_to_many(
            many_to_many_r_type=video_channel_association_relationship_type,
            left_entity_id=video_id,
            right_entity_id=channel_id,
        )

    def remove_video_from_playlist(
        self,
        video_id: str,
        playlist_id: str,
    ):
        self.unset_many_to_many(
            many_to_many_r_type=video_playlist_association_relationship_type,
            left_entity_id=video_id,
            right_entity_id=playlist_id,
        )

    def viewer_unsubscribe_youtuber(
        self,
        viewer_id: str,
        youtuber_id: str,
    ):
        self.unset_many_to_many(
            many_to_many_r_type=viewer_subscribe_youtuber_relationship_type,
            left_entity_id=viewer_id,
            right_entity_id=youtuber_id,
        )

    def viewer_unsubscribe_channel(
        self,
        viewer_id: str,
        channel_id: str,
    ):
        self.unset_many_to_many(
            many_to_many_r_type=viewer_subscribe_youtuber_relationship_type,
            left_entity_id=viewer_id,
            right_entity_id=channel_id,
        )

    def clear_channel(
        self,
        channel_id: str,
    ):
        self.clear_many_by_many(
            many_to_many_r_type=video_playlist_association_relationship_type,
            entity_id=channel_id,
            lookup_by_left=False,
        )

    def clear_playlist(
        self,
        playlist_id: str,
    ):
        self.clear_many_by_many(
            many_to_many_r_type=video_playlist_association_relationship_type,
            entity_id=playlist_id,
            lookup_by_left=False,
        )

    def unsubscribe_all_youtuber(
        self,
        viewer_id: str,
    ):
        self.clear_many_by_many(
            many_to_many_r_type=viewer_subscribe_youtuber_relationship_type,
            entity_id=viewer_id,
            lookup_by_left=True,
        )

    def unsubscribe_all_channel(
        self,
        viewer_id: str,
    ):
        self.clear_many_by_many(
            many_to_many_r_type=viewer_subscribe_channel_relationship_type,
            entity_id=viewer_id,
            lookup_by_left=True,
        )


rs = RelationshipSetting(
    main_model=Entity,
    entity_types=[
        user_entity_type,
        video_entity_type,
        channel_entity_type,
        playlist_entity_type,
    ],
    one_to_many_relationship_types=[
        video_ownership_relationship_type,
        channel_ownership_relationship_type,
        playlist_ownership_relationship_type,
    ],
    many_to_many_relationship_types=[
        video_channel_association_relationship_type,
        video_playlist_association_relationship_type,
        viewer_subscribe_youtuber_relationship_type,
        viewer_subscribe_channel_relationship_type,
    ],
)

Setup Local DynamoDB Mock#

[7]:
import moto
from pynamodb.connection import Connection

mock_dynamodb = moto.mock_dynamodb()
mock_dynamodb.start()
conn = Connection()

rs.main_model.create_table(wait=True)

Create Some Test Data#

[8]:
rs.delete_all()
[9]:
u_alice = rs.new_user(id="u-1", name="Alice")
u_bob = rs.new_user(id="u-2", name="Bob")
u_cathy = rs.new_user(id="u-3", name="Cathy")
u_david = rs.new_user(id="u-4", name="David")

v_alice_1 = rs.new_video(id="v-1-1", name="Alice's Video 1")
rs.set_video_owner(conn=conn, video_id="v-1-1", user_id="u-1")
v_alice_2 = rs.new_video(id="v-1-2", name="Alice's Video 2")
rs.set_video_owner(conn=conn, video_id="v-1-2", user_id="u-1")

v_bob_1 = rs.new_video(id="v-2-1", name="Bob's Video 1")
rs.set_video_owner(conn=conn, video_id="v-2-1", user_id="u-2")
v_bob_2 = rs.new_video(id="v-2-2", name="Bob's Video 2")
rs.set_video_owner(conn=conn, video_id="v-2-2", user_id="u-2")
v_bob_3 = rs.new_video(id="v-2-3", name="Bob's Video 3")
rs.set_video_owner(conn=conn, video_id="v-2-3", user_id="u-2")
v_bob_4 = rs.new_video(id="v-2-4", name="Bob's Video 4")
rs.set_video_owner(conn=conn, video_id="v-2-4", user_id="u-2")

c_alice_1 = rs.new_channel(id="c-1-1", name="Alice's Channel 1")
rs.set_channel_owner(conn=conn, channel_id="c-1-1", user_id="u-1")

c_bob_1 = rs.new_channel(id="c-2-1", name="Bob's Channel 1")
rs.set_channel_owner(conn=conn, channel_id="c-2-1", user_id="u-2")
c_bob_2 = rs.new_channel(id="c-2-2", name="Bob's Channel 2")
rs.set_channel_owner(conn=conn, channel_id="c-2-2", user_id="u-2")

p_cathy_1 = rs.new_playlist(id="p-3-1", name="Cathy's Playlist 1")
rs.set_playlist_owner(conn=conn, playlist_id="p-3-1", user_id="u-3")
p_cathy_2 = rs.new_playlist(id="p-3-2", name="Cathy's Playlist 2")
rs.set_playlist_owner(conn=conn, playlist_id="p-3-2", user_id="u-3")

rs.add_video_to_channel(video_id="v-2-1", channel_id="c-2-1")
rs.add_video_to_channel(video_id="v-2-2", channel_id="c-2-1")
rs.add_video_to_channel(video_id="v-2-3", channel_id="c-2-1")

rs.add_video_to_channel(video_id="v-2-2", channel_id="c-2-2")
rs.add_video_to_channel(video_id="v-2-3", channel_id="c-2-2")
rs.add_video_to_channel(video_id="v-2-4", channel_id="c-2-2")

rs.add_video_to_playlist(video_id="v-2-1", playlist_id="p-3-1")
rs.add_video_to_playlist(video_id="v-2-2", playlist_id="p-3-1")
rs.add_video_to_playlist(video_id="v-2-3", playlist_id="p-3-1")

rs.add_video_to_playlist(video_id="v-2-2", playlist_id="p-3-2")
rs.add_video_to_playlist(video_id="v-2-3", playlist_id="p-3-2")
rs.add_video_to_playlist(video_id="v-2-4", playlist_id="p-3-2")

rs.viewer_subscribe_youtuber(viewer_id="u-2", youtuber_id="u-1")
rs.viewer_subscribe_youtuber(viewer_id="u-3", youtuber_id="u-1")
rs.viewer_subscribe_youtuber(viewer_id="u-4", youtuber_id="u-1")
rs.viewer_subscribe_youtuber(viewer_id="u-1", youtuber_id="u-2")
rs.viewer_subscribe_youtuber(viewer_id="u-3", youtuber_id="u-2")
rs.viewer_subscribe_youtuber(viewer_id="u-4", youtuber_id="u-3")

rs.viewer_subscribe_channel(viewer_id="u-1", channel_id="c-2-1")
rs.viewer_subscribe_channel(viewer_id="u-1", channel_id="c-2-2")
rs.viewer_subscribe_channel(viewer_id="u-2", channel_id="c-1-1")
rs.viewer_subscribe_channel(viewer_id="u-3", channel_id="c-1-1")
rs.viewer_subscribe_channel(viewer_id="u-3", channel_id="c-2-1")
rs.viewer_subscribe_channel(viewer_id="u-4", channel_id="c-2-2")

Declare Some Helper Functions For Testing#

[10]:
# declare some helpers
def assert_pk(lst: T.Iterable[T_Entity], pks: T.List[str]):
    """
    A helper function to verify a list of items' partition key.
    """
    assert set(x.pk_id for x in lst) == set(pks)


def assert_sk(lst: T.Iterable[T_Entity], sks: T.List[str]):
    """
    A helper function to verify a list of items' sort key. Usually used
    for lookup in one-to-many and many-to-many relationship.
    """
    assert set(x.sk_id for x in lst) == set(sks)


def print_all(lst: T.Iterable[T_Entity]):
    for entity in lst:
        entity.print_vip_attrs()
[11]:
print("--- Scan entities and relationships ---")
res = rs.scan().all()
print_all(res)
--- Scan entities and relationships ---
{'type': 'User', 'pk': 'u-1', 'sk': 'User', 'name': 'Alice'}
{'type': 'User', 'pk': 'u-2', 'sk': 'User', 'name': 'Bob'}
{'type': 'User', 'pk': 'u-3', 'sk': 'User', 'name': 'Cathy'}
{'type': 'User', 'pk': 'u-4', 'sk': 'User', 'name': 'David'}
{'type': 'Video', 'pk': 'v-1-1', 'sk': 'Video', 'name': "Alice's Video 1"}
{'type': 'Video-Ownership', 'pk': 'v-1-1', 'sk': 'u-1'}
{'type': 'Video', 'pk': 'v-1-2', 'sk': 'Video', 'name': "Alice's Video 2"}
{'type': 'Video-Ownership', 'pk': 'v-1-2', 'sk': 'u-1'}
{'type': 'Video', 'pk': 'v-2-1', 'sk': 'Video', 'name': "Bob's Video 1"}
{'type': 'Video-Ownership', 'pk': 'v-2-1', 'sk': 'u-2'}
{'type': 'Video', 'pk': 'v-2-2', 'sk': 'Video', 'name': "Bob's Video 2"}
{'type': 'Video-Ownership', 'pk': 'v-2-2', 'sk': 'u-2'}
{'type': 'Video', 'pk': 'v-2-3', 'sk': 'Video', 'name': "Bob's Video 3"}
{'type': 'Video-Ownership', 'pk': 'v-2-3', 'sk': 'u-2'}
{'type': 'Video', 'pk': 'v-2-4', 'sk': 'Video', 'name': "Bob's Video 4"}
{'type': 'Video-Ownership', 'pk': 'v-2-4', 'sk': 'u-2'}
{'type': 'Channel', 'pk': 'c-1-1', 'sk': 'Channel', 'name': "Alice's Channel 1"}
{'type': 'Channel-Ownership', 'pk': 'c-1-1', 'sk': 'u-1'}
{'type': 'Channel', 'pk': 'c-2-1', 'sk': 'Channel', 'name': "Bob's Channel 1"}
{'type': 'Channel-Ownership', 'pk': 'c-2-1', 'sk': 'u-2'}
{'type': 'Channel', 'pk': 'c-2-2', 'sk': 'Channel', 'name': "Bob's Channel 2"}
{'type': 'Channel-Ownership', 'pk': 'c-2-2', 'sk': 'u-2'}
{'type': 'Playlist', 'pk': 'p-3-1', 'sk': 'Playlist', 'name': "Cathy's Playlist 1"}
{'type': 'Playlist-Ownership', 'pk': 'p-3-1', 'sk': 'u-3'}
{'type': 'Playlist', 'pk': 'p-3-2', 'sk': 'Playlist', 'name': "Cathy's Playlist 2"}
{'type': 'Playlist-Ownership', 'pk': 'p-3-2', 'sk': 'u-3'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-1', 'sk': 'c-2-1'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-2', 'sk': 'c-2-1'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-2', 'sk': 'c-2-2'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-3', 'sk': 'c-2-1'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-3', 'sk': 'c-2-2'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-4', 'sk': 'c-2-2'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-1', 'sk': 'p-3-1'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-2', 'sk': 'p-3-1'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-2', 'sk': 'p-3-2'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-3', 'sk': 'p-3-1'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-3', 'sk': 'p-3-2'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-4', 'sk': 'p-3-2'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-2', 'sk': 'u-1'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-3', 'sk': 'u-1'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-3', 'sk': 'u-2'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-4', 'sk': 'u-1'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-4', 'sk': 'u-3'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-1', 'sk': 'u-2'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-1', 'sk': 'c-2-1'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-1', 'sk': 'c-2-2'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-2', 'sk': 'c-1-1'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-3', 'sk': 'c-1-1'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-3', 'sk': 'c-2-1'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-4', 'sk': 'c-2-2'}

Test Video Ownership Relationship (One to Many)#

[12]:
print("--- Alice owned videos ---")
res = rs.find_videos_created_by_a_user(user_id="u-1").all()
assert_pk(res, ["v-1-1", "v-1-2"])
--- Alice owned videos ---
[13]:
print("--- Bob owned videos ---")
res = rs.find_videos_created_by_a_user(user_id="u-2").all()
print_all(res)
assert_pk(res, ["v-2-1", "v-2-2", "v-2-3", "v-2-4"])
--- Bob owned videos ---
{'type': 'Video-Ownership', 'pk': 'v-2-1', 'sk': 'u-2'}
{'type': 'Video-Ownership', 'pk': 'v-2-2', 'sk': 'u-2'}
{'type': 'Video-Ownership', 'pk': 'v-2-3', 'sk': 'u-2'}
{'type': 'Video-Ownership', 'pk': 'v-2-4', 'sk': 'u-2'}

Test Channel Ownership Relationship (One to Many)#

[14]:
print("--- Alice owned channels ---")
res = rs.find_channels_created_by_a_user(user_id="u-1").all()
print_all(res)
assert_pk(res, ["c-1-1"])
--- Alice owned channels ---
{'type': 'Channel-Ownership', 'pk': 'c-1-1', 'sk': 'u-1'}
[15]:
print("--- Bob owned channels ---")
res = rs.find_channels_created_by_a_user(user_id="u-2").all()
print_all(res)
assert_pk(res, ["c-2-1", "c-2-2"])
--- Bob owned channels ---
{'type': 'Channel-Ownership', 'pk': 'c-2-1', 'sk': 'u-2'}
{'type': 'Channel-Ownership', 'pk': 'c-2-2', 'sk': 'u-2'}
[16]:
print("--- Cathy owned playlists ---")
res = rs.find_playlists_created_by_a_user(user_id="u-3").all()
print_all(res)
assert_pk(res, ["p-3-1", "p-3-2"])
--- Cathy owned playlists ---
{'type': 'Playlist-Ownership', 'pk': 'p-3-1', 'sk': 'u-3'}
{'type': 'Playlist-Ownership', 'pk': 'p-3-2', 'sk': 'u-3'}

Test Channel and Video Relationship (Many to Many)#

[17]:
print("--- Videos in Bob's Channel 1 ---")
res = rs.find_videos_in_channel(channel_id="c-2-1").all()
print_all(res)
assert_pk(res, ["v-2-1", "v-2-2", "v-2-3"])
--- Videos in Bob's Channel 1 ---
{'type': 'Video-Channel-Association', 'pk': 'v-2-1', 'sk': 'c-2-1'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-2', 'sk': 'c-2-1'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-3', 'sk': 'c-2-1'}
[18]:
print("--- Videos in Bob's Channel 2 ---")
res = rs.find_videos_in_channel(channel_id="c-2-2").all()
print_all(res)
assert_pk(res, ["v-2-2", "v-2-3", "v-2-4"])
--- Videos in Bob's Channel 2 ---
{'type': 'Video-Channel-Association', 'pk': 'v-2-2', 'sk': 'c-2-2'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-3', 'sk': 'c-2-2'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-4', 'sk': 'c-2-2'}
[19]:
print("--- Channels that has Bob's Video 1 ---")
res = rs.find_channels_that_has_video(video_id="v-2-1").all()
print_all(res)
assert_sk(res, ["c-2-1"])
--- Channels that has Bob's Video 1 ---
{'type': 'Video-Channel-Association', 'pk': 'v-2-1', 'sk': 'c-2-1'}
[20]:
print("--- Channels that has Bob's Video 2 ---")
res = rs.find_channels_that_has_video(video_id="v-2-2").all()
print_all(res)
assert_sk(res, ["c-2-1", "c-2-2"])
--- Channels that has Bob's Video 2 ---
{'type': 'Video-Channel-Association', 'pk': 'v-2-2', 'sk': 'c-2-1'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-2', 'sk': 'c-2-2'}
[21]:
print("--- Channels that has Bob's Video 3 ---")
res = rs.find_channels_that_has_video(video_id="v-2-3").all()
print_all(res)
assert_sk(res, ["c-2-1", "c-2-2"])
--- Channels that has Bob's Video 3 ---
{'type': 'Video-Channel-Association', 'pk': 'v-2-3', 'sk': 'c-2-1'}
{'type': 'Video-Channel-Association', 'pk': 'v-2-3', 'sk': 'c-2-2'}
[22]:
print("--- Channels that has Bob's Video 4 ---")
res = rs.find_channels_that_has_video(video_id="v-2-4").all()
print_all(res)
assert_sk(res, ["c-2-2"])
--- Channels that has Bob's Video 4 ---
{'type': 'Video-Channel-Association', 'pk': 'v-2-4', 'sk': 'c-2-2'}

Test Playlist and Video Relationship (Many to Many)#

[23]:
print("--- Videos in Cathy's Playlist 1 ---")
res = rs.find_videos_in_playlist(playlist_id="p-3-1").all()
print_all(res)
assert_pk(res, ["v-2-1", "v-2-2", "v-2-3"])
--- Videos in Cathy's Playlist 1 ---
{'type': 'Video-Playlist-Association', 'pk': 'v-2-1', 'sk': 'p-3-1'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-2', 'sk': 'p-3-1'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-3', 'sk': 'p-3-1'}
[24]:
print("--- Videos in Cathy's Playlist 2 ---")
res = rs.find_videos_in_playlist(playlist_id="p-3-2").all()
print_all(res)
assert_pk(res, ["v-2-2", "v-2-3", "v-2-4"])
--- Videos in Cathy's Playlist 2 ---
{'type': 'Video-Playlist-Association', 'pk': 'v-2-2', 'sk': 'p-3-2'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-3', 'sk': 'p-3-2'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-4', 'sk': 'p-3-2'}
[25]:
print("--- Playlist that has Bob's Video 1 ---")
res = rs.find_playlists_that_has_video(video_id="v-2-1").all()
print_all(res)
assert_sk(res, ["p-3-1"])
--- Playlist that has Bob's Video 1 ---
{'type': 'Video-Playlist-Association', 'pk': 'v-2-1', 'sk': 'p-3-1'}
[26]:
print("--- Playlist that has Bob's Video 2 ---")
res = rs.find_playlists_that_has_video(video_id="v-2-2").all()
print_all(res)
assert_sk(res, ["p-3-1", "p-3-2"])
--- Playlist that has Bob's Video 2 ---
{'type': 'Video-Playlist-Association', 'pk': 'v-2-2', 'sk': 'p-3-1'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-2', 'sk': 'p-3-2'}
[27]:
print("--- Playlist that has Bob's Video 3 ---")
res = rs.find_playlists_that_has_video(video_id="v-2-3").all()
print_all(res)
assert_sk(res, ["p-3-1", "p-3-2"])
--- Playlist that has Bob's Video 3 ---
{'type': 'Video-Playlist-Association', 'pk': 'v-2-3', 'sk': 'p-3-1'}
{'type': 'Video-Playlist-Association', 'pk': 'v-2-3', 'sk': 'p-3-2'}
[28]:
print("--- Playlist that has Bob's Video 4 ---")
res = rs.find_playlists_that_has_video(video_id="v-2-4").all()
print_all(res)
assert_sk(res, ["p-3-2"])
--- Playlist that has Bob's Video 4 ---
{'type': 'Video-Playlist-Association', 'pk': 'v-2-4', 'sk': 'p-3-2'}

Test Youtuber Subscription Relationship (Many to Many)#

[29]:
print("--- Users who subscribes Alice ---")
res = rs.find_subscribers_for_youtuber(youtuber_id="u-1").all()
print_all(res)
assert_pk(res, ["u-2", "u-3", "u-4"])
--- Users who subscribes Alice ---
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-2', 'sk': 'u-1'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-3', 'sk': 'u-1'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-4', 'sk': 'u-1'}
[30]:
print("--- Users who subscribes Bob ---")
res = rs.find_subscribers_for_youtuber(youtuber_id="u-2").all()
print_all(res)
assert_pk(res, ["u-1", "u-3"])
--- Users who subscribes Bob ---
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-3', 'sk': 'u-2'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-1', 'sk': 'u-2'}
[31]:
print("--- Users who subscribes Cathy ---")
res = rs.find_subscribers_for_youtuber(youtuber_id="u-3").all()
print_all(res)
assert_pk(res, ["u-4"])
--- Users who subscribes Cathy ---
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-4', 'sk': 'u-3'}
[32]:
print("--- Users who subscribes David ---")
res = rs.find_subscribers_for_youtuber(youtuber_id="u-4").all()
print_all(res)
assert_pk(res, [])
--- Users who subscribes David ---
[33]:
print("--- Alice subscribed who ---")
res = rs.find_subscribed_youtubers(user_id="u-1").all()
print_all(res)
assert_sk(res, ["u-2"])
--- Alice subscribed who ---
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-1', 'sk': 'u-2'}
[34]:
print("--- Bob subscribed who ---")
res = rs.find_subscribed_youtubers(user_id="u-2").all()
print_all(res)
assert_sk(res, ["u-1"])
--- Bob subscribed who ---
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-2', 'sk': 'u-1'}
[35]:
print("--- Cathy subscribed who ---")
res = rs.find_subscribed_youtubers(user_id="u-3").all()
print_all(res)
assert_sk(res, ["u-1", "u-2"])
--- Cathy subscribed who ---
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-3', 'sk': 'u-1'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-3', 'sk': 'u-2'}
[36]:
print("--- David subscribed who ---")
res = rs.find_subscribed_youtubers(user_id="u-4").all()
print_all(res)
assert_sk(res, ["u-1", "u-3"])
--- David subscribed who ---
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-4', 'sk': 'u-1'}
{'type': 'Viewer-Subscribe-Youtuber', 'pk': 'u-4', 'sk': 'u-3'}

Test Channel Subscription Relationship (Many to Many)#

[37]:
print("--- Users who subscribes Alice' Channel 1 ---")
res = rs.find_subscribers_for_channel(channel_id="c-1-1").all()
print_all(res)
assert_pk(res, ["u-2", "u-3"])
--- Users who subscribes Alice' Channel 1 ---
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-2', 'sk': 'c-1-1'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-3', 'sk': 'c-1-1'}
[38]:
print("--- Users who subscribes Bob' Channel 1 ---")
res = rs.find_subscribers_for_channel(channel_id="c-2-1").all()
print_all(res)
assert_pk(res, ["u-1", "u-3"])
--- Users who subscribes Bob' Channel 1 ---
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-1', 'sk': 'c-2-1'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-3', 'sk': 'c-2-1'}
[39]:
print("--- Users who subscribes Bob' Channel 2 ---")
res = rs.find_subscribers_for_channel(channel_id="c-2-2").all()
print_all(res)
assert_pk(res, ["u-1", "u-4"])
--- Users who subscribes Bob' Channel 2 ---
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-1', 'sk': 'c-2-2'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-4', 'sk': 'c-2-2'}
[40]:
print("--- Alice subscribed channels ---")
res = rs.find_subscribed_channels(user_id="u-1").all()
print_all(res)
assert_sk(res, ["c-2-1", "c-2-2"])
--- Alice subscribed channels ---
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-1', 'sk': 'c-2-1'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-1', 'sk': 'c-2-2'}
[41]:
print("--- Bob subscribed channels ---")
res = rs.find_subscribed_channels(user_id="u-2").all()
print_all(res)
assert_sk(res, ["c-1-1"])
--- Bob subscribed channels ---
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-2', 'sk': 'c-1-1'}
[42]:
print("--- Cathy subscribed channels ---")
res = rs.find_subscribed_channels(user_id="u-3").all()
print_all(res)
assert_sk(res, ["c-1-1", "c-2-1"])
--- Cathy subscribed channels ---
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-3', 'sk': 'c-1-1'}
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-3', 'sk': 'c-2-1'}
[43]:
print("--- David subscribed channels ---")
res = rs.find_subscribed_channels(user_id="u-4").all()
print_all(res)
assert_sk(res, ["c-2-2"])
--- David subscribed channels ---
{'type': 'Viewer-Subscribe-Channel', 'pk': 'u-4', 'sk': 'c-2-2'}
[45]: