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.
Design note: no delete/remove method.
Repositories intentionally do not support hard deletion. Domain state changes (cancellation,
deactivation, archival) should be modeled as explicit state transitions via commands and events,
not as record erasure. Hard deletion is available at the infrastructure level (_dao.delete())
for projection rebuilds, test teardown, and compliance requirements (e.g. GDPR right to erasure).
Source code in src/protean/core/repository.py
68 69 70 | |
query
property
query: QuerySet
Return a QuerySet for fluent filtering on the aggregate's data store.
Use this inside custom repository methods instead of self._dao.query::
@domain.repository(part_of=Person)
class PersonRepository:
def adults(self):
return self.query.filter(age__gte=18).all().items
find_by
find_by(**kwargs: Any) -> Any
Find a single aggregate matching the given criteria.
Raises ObjectNotFoundError if no match is found.
Raises TooManyObjectsError if multiple matches are found.
Example::
@domain.repository(part_of=Person)
class PersonRepository:
def find_by_email(self, email: str) -> Person:
return self.find_by(email=email)
Source code in src/protean/core/repository.py
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
find
find(criteria: Q) -> ResultSet
Find all aggregates matching a Q criteria expression.
Returns a ResultSet containing
the matching aggregates. Accepts composable Q objects, making
it easy to build reusable, domain-named query functions::
from protean.utils.query import Q
def overdue_orders() -> Q:
return Q(status="pending", due_date__lt=datetime.now())
results = repo.find(overdue_orders())
results = repo.find(overdue_orders() & Q(total__gte=5000))
Source code in src/protean/core/repository.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
exists
exists(criteria: Q) -> bool
Check if any aggregate matches the given Q criteria.
Returns True when at least one aggregate satisfies the
criteria, False otherwise. Unlike find, this method
does not load aggregate objects -- it only checks for existence::
if repo.exists(Q(email="john@example.com")):
raise ValueError("Email already taken")
Source code in src/protean/core/repository.py
201 202 203 204 205 206 207 208 209 210 211 | |
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
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | |
get
get(identifier: Any) -> 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
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 | |
get_or_none
get_or_none(identifier: Any) -> Any | None
Fetch data from the persistence store by its key identifier, same as get(),
but return None instead of raising ObjectNotFoundError when nothing matches.
On a hit, the returned object has all child objects, including enclosed entities,
pre-warmed exactly as get() does. On a miss, this method returns None and never
raises ObjectNotFoundError. Any other error (for example TooManyObjectsError, or
a provider/connection error) still propagates.
Use get() instead when a missing identifier should be treated as a hard failure.
Source code in src/protean/core/repository.py
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 | |