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]:
{'balance': {'currency': 'USD', 'amount': 100.0},
'name': 'Checking',
'id': '513b8a78-e00f-45ce-bb6f-11ef0cccbec6'}
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 List, ValueObjectFromEntity
@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. Defaults toString. Accepted field types areBoolean,Date,DateTime,Float,Identifier,Integer,String, andText.
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()
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'}
In [3]: user2 = User(email="jane.doe@gmail.com", roles=[1, 2])
ERROR: Error during initialization: {'roles': ['Invalid value [1, 2]']}
...
ValidationError: {'roles': ['Invalid value [1, 2]']}
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]:
{'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'},
'id': '4a9538bf-1eb1-4621-8ced-86bcc4362a51'}
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
Note that unlike HasMany fields, you have to supply a new entire list of
Value Objects if you want to update the field. Appendind to the list will not
work.
In [6]: retrieved_order.customer.addresses.append(
...: Address(street="456 Side St", city="Anytown", state="CA", country="USA")
...: )
In [7]: domain.repository_for(Order).add(retrieved_order)
Out[7]: <Order: Order object (id: 4a9538bf-1eb1-4621-8ced-86bcc4362a51)>
In [8]: updated_order = domain.repository_for(Order).get(order.id)
In [9]: len(updated_order.customer.addresses)
Out[9]: 2
# This did not work!
In [10]: updated_order.customer.addresses = [
...: Address(street="123 Main St", city="Anytown", state="CA", country="USA"),
...: Address(street="321 Side St", city="Anytown", state="CA", country="USA"),
...: Address(street="456 Side St", city="Anytown", state="CA", country="USA"),
...: ]
In [11]: domain.repository_for(Order).add(updated_order)
Out[11]: <Order: Order object (id: 4a9538bf-1eb1-4621-8ced-86bcc4362a51)>
In [12]: refreshed_order = domain.repository_for(Order).get(order.id)
In [13]: len(refreshed_order.customer.addresses)
Out[13]: 3
# This worked!
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'}
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 simply store the field as a python object.