Container Fields
Applies to: DDD · CQRS · Event Sourcing
ValueObject
Represents a field that holds a value object. This field is used to embed a Value Object within an entity.
Arguments
value_object_cls: The class of the value object to be embedded.
from protean import Domain
from protean.fields import Float, String, ValueObject
domain = Domain()
@domain.value_object
class Balance:
"""A composite amount object, containing two parts:
* currency code - a three letter unique currency code
* amount - a float value
"""
currency: String(max_length=3, required=True)
amount: Float(required=True, min_value=0.0)
@domain.aggregate
class Account:
balance = ValueObject(Balance)
name: String(max_length=30)
You can provide an instance of the Value Object as input to the value object field:
In [1]: account = Account(
...: balance=Balance(currency="USD", amount=100.0),
...: name="Checking"
...: )
In [2]: account.to_dict()
Out[2]:
{'name': 'Checking',
'id': '513b8a78-e00f-45ce-bb6f-11ef0cccbec6',
'balance': {'currency': 'USD', 'amount': 100.0},
'_version': -1}
ValueObjectFromEntity
A convenience variant of ValueObject that auto-generates its value object
class from an entity. Instead of manually defining a VO that mirrors an
entity's fields, this descriptor derives it at class-body evaluation time.
Arguments
entity_cls: The entity (or aggregate) class to project into a value object. Identity/unique fields become optional,Referencefields are excluded, andHasOne/HasManyassociations are recursively converted.
from protean.fields import (
HasMany,
Identifier,
Integer,
List,
String,
ValueObjectFromEntity,
)
@domain.aggregate
class Order:
customer_id: Identifier(required=True)
items = HasMany("OrderItem")
@domain.entity(part_of=Order)
class OrderItem:
product_id: String(max_length=50, required=True)
quantity: Integer(min_value=1)
@domain.command(part_of=Order)
class PlaceOrder:
customer_id: Identifier(required=True)
items: List(content_type=ValueObjectFromEntity(OrderItem))
This is equivalent to calling value_object_from_entity(OrderItem) and
passing the result to a ValueObject field. Use whichever form is clearer
for your use case.
See Projecting Entities into Value Objects for the full guide including round-trip conversion back to entities.
List
A field that represents a list of values.
Optional Arguments
content_type: The type of items in the list. It can be a simple field such asString,Integer,Float,Decimal,Boolean,Date,DateTime,Identifier,TextorDict, or aValueObjectfield (see List of Value Objects).Autoraises aValidationError. Association fields such asHasManyandReferenceare not supported. When you leave it out, the list accepts items of any type.
from protean import Domain
from protean.fields import List, String
domain = Domain()
@domain.aggregate
class User:
email: String(max_length=255, required=True, unique=True)
roles: List(content_type=String)
The value is provided as a list, and the values in the list are validated
to be of the right type.
In [1]: user = User(email="john.doe@gmail.com", roles=['ADMIN', 'EDITOR'])
In [2]: user.to_dict()
Out[2]:
{'email': 'john.doe@gmail.com',
'roles': ['ADMIN', 'EDITOR'],
'id': '582d946b-409b-4b15-b3be-6a90284264b3',
'_version': -1}
In [3]: user2 = User(email="jane.doe@gmail.com", roles=[1, 2])
...
ValidationError: {'roles': ['Input should be a valid string', 'Input should be a valid string']}
List of Value Objects
A List field can even hold a list of ValueObject instances. The content of
the List will be persisted as a list of dicts, so the field will behave
essentially like List(Dict()) when it comes to persistence. However, it will
have the added benefit of a validation structure of content within the List.
from protean import Domain
from protean.fields import HasOne, List, String, ValueObject
domain = Domain()
@domain.value_object
class Address:
street: String(max_length=100)
city: String(max_length=25)
state: String(max_length=25)
country: String(max_length=25)
@domain.entity(part_of="Order")
class Customer:
name: String(max_length=50, required=True)
email: String(max_length=254, required=True)
addresses: List(content_type=ValueObject(Address))
@domain.aggregate
class Order:
customer = HasOne(Customer)
In [1]: order = Order(
...: customer=Customer(
...: name="John Doe",
...: email="john@doe.com",
...: addresses=[
...: Address(street="123 Main St", city="Anytown", state="CA", country="USA"),
...: Address(street="321 Side St", city="Anytown", state="CA", country="USA"),
...: ],
...: )
...: )
In [2]: order.to_dict()
Out[2]:
{'id': '4a9538bf-1eb1-4621-8ced-86bcc4362a51',
'customer': {'name': 'John Doe',
'email': 'john@doe.com',
'addresses': [{'street': '123 Main St',
'city': 'Anytown',
'state': 'CA',
'country': 'USA'},
{'street': '321 Side St',
'city': 'Anytown',
'state': 'CA',
'country': 'USA'}],
'id': 'f5c5a750-e9fe-47db-877e-44b7c0ca1dfc'},
'_version': -1}
In [3]: domain.repository_for(Order).add(order)
Out[3]: <Order: Order object (id: 4a9538bf-1eb1-4621-8ced-86bcc4362a51)>
In [4]: retrieved_order = domain.repository_for(Order).get(order.id)
In [5]: len(retrieved_order.customer.addresses)
Out[5]: 2
To change the list, append to it or assign a new list, then save the aggregate through its repository. The change is stored only when you save.
In [6]: retrieved_order.customer.addresses.append(
...: Address(street="456 Side St", city="Anytown", state="CA", country="USA")
...: )
In [7]: len(domain.repository_for(Order).get(order.id).customer.addresses)
Out[7]: 2
In [8]: domain.repository_for(Order).add(retrieved_order)
Out[8]: <Order: Order object (id: 4a9538bf-1eb1-4621-8ced-86bcc4362a51)>
In [9]: len(domain.repository_for(Order).get(order.id).customer.addresses)
Out[9]: 3
Dict
A field that represents a dictionary.
Optional Arguments
pickled: Whether the dict should be pickled when stored. Defaults toFalse.
from protean import Domain
from protean.fields import Dict, String
domain = Domain()
@domain.aggregate
class UserEvent:
name: String(max_length=255)
payload: Dict()
A regular dictionary can be supplied as value to payload:
In [1]: event=UserEvent(
...: name="UserRegistered",
...: payload={'name': 'John Doe', 'email': 'john.doe@example.com'}
...: )
In [2]: event.to_dict()
Out[2]:
{'name': 'UserRegistered',
'payload': {'name': 'John Doe', 'email': 'john.doe@example.com'},
'id': '44e9143f-f4a6-40da-9128-4b6c013420d4',
'_version': -1}
Note
Some database implementations (like Postgresql) can store dicts as JSON by default. You can force it to store the pickled value as a Python object by specifying pickled=True. Databases that don’t support lists store the field as a python object.
Dict of Value Objects
A Dict field can hold a string-keyed code → value object map with
value_type. Each value is a ValueObject instance, reconstructed and validated
on load and serialized to a plain dict for storage (a single JSON column), just
like a list of value objects:
from protean.fields import Dict, String, ValueObject
@domain.value_object
class Address:
street: String(max_length=100)
city: String(max_length=25)
@domain.aggregate
class Customer:
name: String(max_length=50)
addresses: Dict(value_type=ValueObject(Address)) # {"home": Address(...), ...}
The values must be value objects; use the untyped Dict() for loose JSON of
primitives. Reach for this when modelling a small, embedded code → concept map
(see the reference-data pattern).