Association Fields
Association fields in Protean are designed to represent and manage relationships between different domain models. They facilitate the modeling of complex relationships, while encapsulating the technical aspects of persisting data.
Note that while Aggregates and Entities can manage associations, they can never link to another Aggregate directly. Aggregates are transaction boundaries and no transaction should span across aggregates. Read more in Aggregate concepts.
HasOne
Represents an one-to-one association between an aggregate and its entities. This field is used to define a relationship where an aggregate is associated with at most one instance of a child entity.
from protean import Domain
from protean.fields import HasOne, String
domain = Domain(__file__, load_toml=False)
@domain.aggregate
class Book:
title = String(required=True, max_length=100)
author = HasOne("Author")
@domain.entity(part_of="Book")
class Author:
name = String(required=True, max_length=50)
Note
If you carefully observe the HasOne
field declaration, the child entity's
name is a string value! This is usually the way to avoid circular references.
It applies to all aspects of Protean that link two entities - the string
value will be resolved to the class at runtime.
The Author
entity can now be persisted along with the Book
aggregate:
In [1]: book = Book(
...: title="The Great Gatsby",
...: author=Author(name="F. Scott Fitzgerald")
...: )
In [2]: domain.repository_for(Book).add(book)
Out[2]: <Book: Book object (id: a4a642d9-87ed-44de-9889-c687466f171b)>
In [3]: domain.repository_for(Book)._dao.query.all().items[0].to_dict()
Out[3]:
{'title': 'The Great Gatsby',
'author': {'name': 'F. Scott Fitzgerald',
'id': '1f275e92-9872-4d96-b999-4ef0fbe61013'},
'id': 'a4a642d9-87ed-44de-9889-c687466f171b'}
Note
Protean adds a Reference
field to child entities to preserve the inverse
relationship - from child entity to aggregate - when persisted. This is
visible if you introspect the fields of the Child Entity.
In [1]: from protean.reflection import declared_fields, attributes
In [2]: declared_fields(Author)
Out[2]:
{'name': String(required=True, max_length=50),
'id': Auto(identifier=True),
'book': Reference()}
In [3]: attributes(Author)
Out[3]:
{'name': String(required=True, max_length=50),
'id': Auto(identifier=True),
'book_id': _ReferenceField()}
We will further review persistence related aspects around associations in the Repository section.
HasMany
Represents a one-to-many association between two entities. This field is used to define a relationship where an aggregate has multiple instances of a child entity.
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)
Protean provides helper methods that begin with add_
and remove_
to add
and remove child entities from the HasMany
relationship.
In [1]: post = Post(
...: title="Foo",
...: comments=[
...: Comment(content="Bar"),
...: Comment(content="Baz")
...: ]
...: )
In [2]: post.to_dict()
Out[2]:
{'title': 'Foo',
'comments': [{'content': 'Bar', 'id': '085ed011-15b3-48e3-9363-99a53bc9362a'},
{'content': 'Baz', 'id': '4790cf87-c234-42b6-bb03-1e0599bd6c0f'}],
'id': '29943ac9-a9eb-497b-b6d2-466b30ecd5f5'}
In [3]: post.add_comments(Comment(content="Qux"))
In [4]: post.to_dict()
Out[4]:
{'title': 'Foo',
'comments': [{'content': 'Bar', 'id': '085ed011-15b3-48e3-9363-99a53bc9362a'},
{'content': 'Baz', 'id': '4790cf87-c234-42b6-bb03-1e0599bd6c0f'},
{'content': 'Qux', 'id': 'b1a7aeda-81ca-4d0b-9d7e-6fe0c000b8af'}],
'id': '29943ac9-a9eb-497b-b6d2-466b30ecd5f5'}
You can also use helper methods that begin with get_one_from_
and filter_
to filter
for specific entities within the instances.
get_one_from_
returns a single entity. It raises ObjectNotFoundError
if no matching
entity for the criteria is found and TooManyObjectsError
if more than
one entity is found.
filter
returns a list
of zero or more matching entities.
In [1]: post = Post(
...: title="Foo",
...: comments=[
...: Comment(content="Bar", rating=2.5),
...: Comment(content="Baz", rating=5)
...: ]
...: )
In [2]: post.filter_comments(content="Bar", rating=2.5)
Out[2]: [<Comment: Comment object (id: 3b7fd92e-be11-4b3b-96e9-1caf02779f14)>]
In [3]: comments = post.filter_comments(content="Bar", rating=2.5)
In [4]: comments[0].to_dict()
Out[4]: {'content': 'Bar', 'rating': 2.5, 'id': '3b7fd92e-be11-4b3b-96e9-1caf02779f14'}