Skip to content

BaseRepository

Base class for repositories -- the persistence abstraction for aggregates. Repositories provide a collection-oriented interface (add, get, all) to load and persist aggregates, hiding database details behind a clean domain API.

See Repositories guide for practical usage and Repositories concept for design rationale.

Bases: Element, OptionsMixin

This is the baseclass for concrete Repository implementations.

The three methods in this baseclass to add, get or all entities are sufficient in most cases to handle application requirements. They have built-in support for handling child relationships and honor Unit of Work constructs. While they can be overridden, it is generally suggested to call the parent method first before writing custom code.

Repositories are strictly meant to be used in conjunction with Aggregate elements. It is always prudent to deal with persistence at the transaction boundary, which is at an Aggregate's level.

Source code in src/protean/core/repository.py
50
51
52
def __init__(self, domain: "Domain", provider: BaseProvider) -> None:
    self._domain = domain
    self._provider = provider

add

add(item: Any) -> Any

This method helps persist or update aggregates or projections into the persistence store.

Returns the persisted item.

Protean adopts a collection-oriented design pattern to handle persistence. What this means is that the Repository interface does not hint in any way that there is an underlying persistence mechanism, avoiding any notion of saving or persisting data in the design layer. The task of syncing the data back into the persistence store is handled automatically.

To be specific, a Repository mimics a set collection. Whatever the implementation, the repository will not allow instances of the same object to be added twice. Also, when retrieving objects from a Repository and modifying them, you don't need to "re-save" them to the Repository.

If there is a Unit of Work in progress, then the changes are performed on the UoW's active session. They are committed whenever the entire UoW is committed. If there is no transaction in progress, changes are committed immediately to the persistence store. This mechanism is part of the DAO's design, and is automatically used wherever one tries to persist data.

Source code in src/protean/core/repository.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def add(self, item: Any) -> Any:  # noqa: C901
    """This method helps persist or update aggregates or projections into the persistence store.

    Returns the persisted item.

    Protean adopts a collection-oriented design pattern to handle persistence. What this means is that
    the Repository interface does not hint in any way that there is an underlying persistence mechanism,
    avoiding any notion of saving or persisting data in the design layer. The task of syncing the data
    back into the persistence store is handled automatically.

    To be specific, a Repository mimics a `set` collection. Whatever the implementation, the repository
    will not allow instances of the same object to be added twice. Also, when retrieving objects from
    a Repository and modifying them, you don't need to "re-save" them to the Repository.

    If there is a Unit of Work in progress, then the changes are performed on the
    UoW's active session. They are committed whenever the entire UoW is committed. If there is no
    transaction in progress, changes are committed immediately to the persistence store. This mechanism
    is part of the DAO's design, and is automatically used wherever one tries to persist data.
    """
    # `add` is typically invoked in handler methods in Command Handlers and Event Handlers, which are
    #   enclosed in a UoW automatically. Therefore, if there is a UoW in progress, we can assume
    #   that it is the active session. If not, we will start a new UoW and commit it after the operation
    #   is complete.
    own_current_uow = None
    if not (current_uow and current_uow.in_progress):
        own_current_uow = UnitOfWork()
        own_current_uow.start()

    # If there are HasMany/HasOne fields in the aggregate, sync child objects added/removed,
    if has_association_fields(item):
        self._sync_children(item)

    # Persist only if the item object is new, or it has changed since last persistence
    if (not item.state_.is_persisted) or (
        item.state_.is_persisted and item.state_.is_changed
    ):
        self._dao.save(item)

        # If Aggregate has signed up Fact Events, raise them now
        if item.element_type == DomainObjects.AGGREGATE and item.meta_.fact_events:
            payload = item.to_dict()

            # Remove internal attributes from the payload, as they are not needed for the Fact Event
            payload.pop("state_", None)
            payload.pop("_version", None)

            # Construct and raise the Fact Event
            fact_event = item._fact_event_cls(**payload)
            item.raise_(fact_event)
    elif (
        item.element_type == DomainObjects.AGGREGATE
        and item._events
        and current_uow
        and current_uow.in_progress
    ):
        # Aggregate has pending events but no own-field changes (e.g., only child
        # entities were added/removed via HasMany).  We still need to track it in
        # the identity map so that _gather_events picks up these events on commit.
        current_uow._add_to_identity_map(item)

    # If we started a UnitOfWork, commit it now
    if own_current_uow:
        own_current_uow.commit()

    return item

get

get(identifier) -> Any

This is a utility method to fetch data from the persistence store by its key identifier. All child objects, including enclosed entities, are returned as part of this call.

Returns the fetched object.

All other data filtering capabilities can be implemented by using the underlying DAO's BaseDAO.filter method.

Filter methods are typically implemented as domain-contextual queries, like find_adults(), find_residents_of_area(zipcode), etc. It is also possible to make use of more complicated, domain-friendly design patterns like the Specification pattern.

Source code in src/protean/core/repository.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
def get(self, identifier) -> Any:
    """This is a utility method to fetch data from the persistence store by its key identifier. All child objects,
    including enclosed entities, are returned as part of this call.

    Returns the fetched object.

    All other data filtering capabilities can be implemented by using the underlying DAO's
    ``BaseDAO.filter`` method.

    Filter methods are typically implemented as domain-contextual queries, like `find_adults()`,
    `find_residents_of_area(zipcode)`, etc. It is also possible to make use of more complicated,
    domain-friendly design patterns like the `Specification` pattern.
    """
    item = self._dao.get(identifier)
    return item