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.pickled: Whether the list should be pickled when stored. Defaults toFalse.
Note
Some database implementations (like Postgresql) can store lists 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.
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.