Skip to content

Entities

Aggregates cluster multiple domain elements together to represent a concept. They are usually composed of two kinds of elements - those with unique identities (Entities) and those without (Value Objects).

Entities represent unique objects in the domain model just like Aggregates, but they don't manage other objects. Just like Aggregates, Entities are identified by unique identities that remain the same throughout its life - they are not defined by their attributes or values. For example, a passenger in the airline domain is an Entity. The passenger's identity remains the same across multiple seat bookings, even if her profile information (name, address, etc.) changes over time.

Note

In Protean, Aggregates are actually entities that have taken on the additional responsibility of managing the lifecycle of one or more related entities.

Definition

An Entity is defined with the Domain.entity decorator:

from protean.domain import Domain
from protean.fields import Date, String

publishing = Domain(__name__)


@publishing.aggregate
class Post:
    name = String(max_length=50)
    created_on = Date()


@publishing.entity(part_of=Post)
class Comment:
    content = String(max_length=500)

An Entity has to be associated with an Aggregate. If part_of is not specified while defining the identity, you will see an IncorrectUsageError:

>>> @publishing.entity
... class Comment:
...     content = String(max_length=500)
... 
IncorrectUsageError: 'Entity `Comment` needs to be associated with an Aggregate'

An Entity cannot enclose another Entity (or Aggregate). Trying to do so will throw IncorrectUsageError.

>>> @publishing.entity
... class SubComment:
...     parent = Comment()
... 
IncorrectUsageError: 'Entity `Comment` needs to be associated with an Aggregate'

Configuration

Similar to an aggregate, an entity's behavior can be customized with by passing additional options to its decorator, or with a Meta class as we saw earlier.

Available options are:

abstract

Marks an Entity as abstract if True. If abstract, the entity cannot be instantiated and needs to be subclassed.

auto_add_id_field

If True, Protean will not add an identifier field (acting as primary key) by default to the entity. This option is usually combined with abstract to create entities that are meant to be subclassed by other aggregates.

schema_name

The name to store and retrieve the entity from the persistence store. By default, schema_name is the snake case version of the Entity's name.

database_model

Similar to an aggregate, Protean automatically constructs a representation of the entity that is compatible with the configured database. While the generated model suits most use cases, you can also explicitly construct a model and associate it with the entity, just like in an aggregate.

Note

An Entity is always persisted in the same persistence store as the its Aggregate.

Associations

Entities can enclose other entities within them using HasOne and HasMany relationships, similar to aggregates. Additionally, entities automatically receive Reference fields that establish inverse relationships to their parent aggregate.

Automatic Reference Fields

When an entity is associated with an aggregate, Protean automatically creates a Reference field that points back to the parent:

@domain.aggregate
class Order:
    number = String(max_length=20)
    items = HasMany("OrderItem")

@domain.entity(part_of=Order)
class OrderItem:
    product_name = String(max_length=100)
    quantity = Integer()
    # Automatically gets: order = Reference(Order)
    # Automatically gets: order_id = String()  # Shadow field

Explicit Reference Fields

You can also explicitly define reference fields for more control:

@domain.entity(part_of=Order)
class OrderItem:
    product_name = String(max_length=100)
    quantity = Integer()
    order = Reference(Order, referenced_as="order_number")
    # Creates shadow field 'order_number' instead of 'order_id'

Reference fields enable navigation from child entities back to their parent aggregate:

# Access parent aggregate from entity
order_item = OrderItem(product_name="Widget", quantity=2)
parent_order = order_item.order  # Order object
order_id = order_item.order_id   # Order's ID value

For comprehensive relationship documentation, see Expressing Relationships and Association Fields.