Skip to content

Commit

Permalink
Persisting Aggregates - Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
subhashb committed Jun 12, 2024
1 parent 570ab90 commit fcda5a6
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 18 deletions.
5 changes: 5 additions & 0 deletions docs/core-concepts/building-blocks/repositories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Repositories

- Collection-oriented design
- Mimics a `set` collection
- Has in-built Unit of Work context
34 changes: 18 additions & 16 deletions docs/guides/compose-a-domain/identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@ In [5]: user.to_dict()
Out[5]: {'user_id': '9cf4ddc4-2919-4021-bd1a-c8083b5fdda7', 'name': 'John Doe'}
```

### Automatic Identity field

When an identity field is not supplied, an `Auto` field called `id` is
automatically added to the entity.

```py
{! docs_src/guides/domain-definition/fields/simple-fields/001.py !}
```

```shell hl_lines="6"
In [1]: from protean.reflection import declared_fields

In [2]: declared_fields(Person)
Out[2]:
{'name': String(required=True, max_length=50, min_length=2),
'id': Auto(identifier=True)}
```

### Composite keys

Protean does not support composite keys. A `NotSupportedError` is thrown when
Expand All @@ -121,22 +139,6 @@ In [10]: @domain.aggregate
NotSupportedError: {'_entity': ['Multiple identifier fields found in entity Order. Only one identifier field is allowed.']}
```

When an identity field is not supplied, an `Auto` field called `id` is
automatically added to the entity.

```py
{! docs_src/guides/domain-definition/fields/simple-fields/001.py !}
```

```shell hl_lines="6"
In [1]: from protean.reflection import declared_fields

In [2]: declared_fields(Person)
Out[2]:
{'name': String(required=True, max_length=50, min_length=2),
'id': Auto(identifier=True)}
```

## Element-level Identity Customization

The identity of a single entity element can be customized by explicit
Expand Down
Empty file.
29 changes: 29 additions & 0 deletions docs/guides/persist-state/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Persisting State

- About Repositories and Repository Pattern

- Different available repositories
- Repository Configuration
- Automatic generation of repositories

## Basic Structure

A repository provides three primary methods to interact with the persistence
store:

### **`add`** - Adds a new entity to the persistence store.

### **`get`** - Retrieves an entity from the persistence store.

- Persisting aggregates
- Retreiving aggregates
- Queries
- Data Access Objects
- Removing aggregates

- Custom Repositories
- Registering a custom Repository
- Database-specific Repositories

- Working with the Application Layer
- Unit of Work
124 changes: 124 additions & 0 deletions docs/guides/persist-state/persist-aggregates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Persist Aggregates

Aggregates are saved into the configured database using `add` method of the
repository.

```python hl_lines="23"
{! docs_src/guides/persist-state/001.py !}
```

1. Identity, by default, is a string.

```shell
In [1]: domain.repository_for(Person).get("1")
Out[1]: <Person: Person object (id: 1)>

In [2]: domain.repository_for(Person).get("1").to_dict()
Out[2]: {'name': 'John Doe', 'email': 'john.doe@localhost', 'id': '1'}
```

## Transaction

The `add` method is enclosed in a [Unit of Work](unit-of-work.md) context by
default. Changes are committed to the persistence store when the Unit Of Work
context exits.

The following calls are equivalent in behavior:

```python
...
# Version 1
domain.repository_for(Person).add(person)
...

...
# Version 2
from protean import UnitOfWork

with UnitOfWork():
domain.repository_for(Person).add(person)
...
```

This means changes across the aggregate cluster are committed as a single
transaction (assuming the underlying database supports transactions, of course).

```python hl_lines="22-30 33"
{! docs_src/guides/persist-state/002.py !}
```

!!!note
This is especially handy in ***Relational databases*** because each entity is a
separate table.

## Events

The `add` method also publishes events to configured brokers upon successfully
persisting to the database.

```python hl_lines="15"
{! docs_src/guides/persist-state/003.py !}
```

```shell hl_lines="12-16 21-22"
In [1]: post = Post(title="Events in Aggregates", body="Lorem ipsum dolor sit amet, consectetur adipiscing...")

In [2]: post.to_dict()
Out[2]:
{'title': 'Events in Aggregates',
'body': 'Lorem ipsum dolor sit amet, consectetur adipiscing...',
'published': False,
'id': 'a9ea7763-c5b2-4c8c-9c97-43ba890517d0'}

In [3]: post.publish()

In [4]: post._events
Out[4]: [<PostPublished: PostPublished object ({
'post_id': 'a9ea7763-c5b2-4c8c-9c97-43ba890517d0',
'body': 'Lorem ipsum dolor sit amet, consectetur adipiscing...'
})>]

In [5]: domain.repository_for(Post).add(post)
Out[5]: <Post: Post object (id: a9ea7763-c5b2-4c8c-9c97-43ba890517d0)>

In [6]: post._events
Out[6]: []
```

## Updates

Recall that Protean repositories behave like a `set` collection. Updating is
as simple as mutating an aggregate and persisting it with `add` again.

```shell hl_lines="15 20 22 25 27"
In [1]: post = Post(
...: id="1",
...: title="Events in Aggregates",
...: body="Lorem ipsum dolor sit amet, consectetur adipiscing..."
...: )

In [2]: domain.repository_for(Post).add(post)
Out[2]: <Post: Post object (id: 1)>

In [3]: domain.repository_for(Post).get("1")
Out[3]: <Post: Post object (id: 1)>

In [4]: domain.repository_for(Post).get("1").to_dict()
Out[4]:
{'title': 'Events in Aggregates',
'body': 'Lorem ipsum dolor sit amet, consectetur adipiscing...',
'published': False,
'id': '1'}

In [5]: post.title = "(Updated Title) Events in Entities"

In [6]: domain.repository_for(Post).add(post)
Out[6]: <Post: Post object (id: 1)>

In [7]: domain.repository_for(Post).get("1").to_dict()
Out[7]:
{'title': '(Updated Title) Events in Entities',
'body': 'Lorem ipsum dolor sit amet, consectetur adipiscing...',
'published': False,
'id': '1'}
```
Empty file.
4 changes: 4 additions & 0 deletions docs/guides/persist-state/unit-of-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Unit of Work


Never enclose updates to multiple aggregates in a single unit of work.
20 changes: 20 additions & 0 deletions docs_src/guides/persist-state/001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from protean import Domain
from protean.fields import String

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Person:
name = String(required=True, max_length=50)
email = String(required=True, max_length=254)


domain.init(traverse=False)
with domain.domain_context():
person = Person(
id="1", # (1)
name="John Doe",
email="john.doe@localhost",
)
domain.repository_for(Person).add(person)
33 changes: 33 additions & 0 deletions docs_src/guides/persist-state/002.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from protean import Domain
from protean.fields import Float, HasMany, String, Text

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Post:
title = String(required=True, max_length=100)
body = Text()
comments = HasMany("Comment")


@domain.entity(part_of=Post)
class Comment:
content = String(required=True, max_length=50)
rating = Float(max_value=5)


domain.init(traverse=False)
with domain.domain_context():
post = Post(
id="1",
title="A Great Post",
body="This is the body of a great post",
comments=[
Comment(id="1", content="Amazing!", rating=5.0),
Comment(id="2", content="Great!", rating=4.5),
],
)

# This persists one `Post` record and two `Comment` records
domain.repository_for(Post).add(post)
21 changes: 21 additions & 0 deletions docs_src/guides/persist-state/003.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from protean import Domain
from protean.fields import Boolean, Identifier, String, Text

domain = Domain(__file__, load_toml=False)


@domain.aggregate
class Post:
title = String(required=True, max_length=100)
body = Text()
published = Boolean(default=False)

def publish(self):
self.published = True
self.raise_(PostPublished(post_id=self.id, body=self.body))


@domain.event(part_of=Post)
class PostPublished:
post_id = Identifier(required=True)
body = Text()
8 changes: 8 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ theme:
- content.code.select
markdown_extensions:
- admonition
- attr_list
- md_in_html
- mdx_include
- pymdownx.details
- pymdownx.superfences:
Expand Down Expand Up @@ -124,6 +126,12 @@ nav:
- guides/domain-behavior/aggregate-mutation.md
- guides/domain-behavior/raising-events.md
- guides/domain-behavior/domain-services.md
- Persisting State:
- guides/persist-state/index.md
- guides/persist-state/persist-aggregates.md
- guides/persist-state/retreive-aggregates.md
- guides/persist-state/custom-repository.md
- guides/persist-state/unit-of-work.md
- Accessing the domain:
- guides/access-domain/index.md
- guides/access-domain/commands.md
Expand Down
2 changes: 1 addition & 1 deletion src/protean/adapters/repository/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def get_connection(self, provider_name="default"):
except KeyError:
raise AssertionError(f"No Provider registered with name {provider_name}")

def repository_for(self, part_of):
def repository_for(self, part_of) -> BaseRepository:
"""Retrieve a Repository registered for the Aggregate"""
if self._providers is None:
self._initialize()
Expand Down
3 changes: 2 additions & 1 deletion src/protean/domain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from protean.core.event import BaseEvent
from protean.core.event_handler import BaseEventHandler
from protean.core.model import BaseModel
from protean.core.repository import BaseRepository
from protean.domain.registry import _DomainRegistry
from protean.exceptions import ConfigurationError, IncorrectUsageError
from protean.fields import HasMany, HasOne, Reference, ValueObject
Expand Down Expand Up @@ -949,7 +950,7 @@ def handlers_for(self, event: BaseEvent) -> List[BaseEventHandler]:
############################

# FIXME Optimize calls to this method with cache, but also with support for Multitenancy
def repository_for(self, part_of):
def repository_for(self, part_of) -> BaseRepository:
if isinstance(part_of, str):
raise IncorrectUsageError(
{
Expand Down

0 comments on commit fcda5a6

Please sign in to comment.