4 ways of mapping Value Object in SQLAlchemy

In the previous post Persistency of DDD aggregate, I described persisting as aggregate with proper application boundaries. Now let’s describe how to persist ValueObject in a relational database using SQLAlchemy and still with boundaries in mind.

The persistency of ValueObject in relational databases can be tricky. Unlike entities, there is no id for value objects. We can say that value is an ID. The best approach is to store value in the same table as an entity. But for value objects, we have different classes. Sometimes we could want to have a separate table for value. For example, an address can have many fields. We may want to store it in a separate table.

Also, we need to keep in mind that proper ValueObject is immutable. So we need to handle in properly cause all DB entries are mutable.

Using SQLAlchemy we have 4 solutions for ValueObject.

  1. Immutable Value Object mapped in the same table as a parent entity
  2. Mutable Value Object Mapped in the same table as a parent entity
  3. Immutable Value Object stored in a different table
  4. Mutable Value Object stored in a different table

Example Entity and Value Object

As an example, I will use simple DTOs representing data. But I want to have a boundary between infrastructure and domain level. DTO will be an interface implemented by the DB model. Domain level will have no idea about implementation or any DB ID that creates only for persistency purposes.

Our Value Object is Money. It’s a simple dataclass with amount and currency. The only difference between examples will be

@datacalss(frozen=True)

for immutable Value Object or

@dataclass(frozen=False)

for mutable Value Object.

Currency = NewType('Currency', Text)


@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: Currency


@dataclass
class Subscription:
    id: UUID
    name: Text
    fee: Money


class Repository(Protocol):
    def create(self, name: Text, fee: Money) -> Subscription:
        ...

    def save(self, dto: Subscription) -> None:
        ...

Implementation of the Repository will also be identical in every case.

class Repository(entity.Repository):
    def __init__(self, session: Session) -> None:
        self._session = session
        self.query = session.query(Subscription)

    def create(self, name: Text, fee: entity.Money) -> Subscription:
        return Subscription(id=uuid1(), name=name, fee=fee)

    def save(self, model: Subscription) -> None:
        self._session.add(model)
        try:
            self._session.commit()
        except:
            self._session.rollback()
            raise

1. Immutable Value Object mapped in the same table as a parent entity

This one is pretty easy. We can just use a regular property without a setter.

class Subscription(entity.Subscription, Base):
    __table__: Table
    __tablename__ = 'immutable_property_vo_subscription_plans'

    id = Column(UUIDType(binary=True), primary_key=True)
    name = Column(String(100), nullable=False, index=True, unique=True)
    when_created = Column(DateTime, nullable=False, default=datetime.utcnow)
    when_updated = Column(DateTime, nullable=True, onupdate=datetime.utcnow)
    _fee_amount = Column('fee_amount', Float(asdecimal=True), nullable=False)
    _fee_currency = Column('fee_currency', CurrencyType, nullable=False)

    @hybrid_property
    def fee(self) -> entity.Money:
        return entity.Money(Decimal(self._fee_amount), self._fee_currency)

    @fee.setter
    def fee(self, value: entity.Money) -> None:
        self._fee_amount = value.amount
        self._fee_currency = value.currency

    def __hash__(self):
        return hash(self.id)

Full code with tests is here.

2. Mutable Value Object Mapped in the same table as a parent entity

This one is a little more tricky. Property with setter won’t help us in this case. Just because the fee is mutable and we can change only one attribute inside this dataclass. SQLAlchemy has MutableComposite in mutable extension for this case.

To make a composite we need at least composite values method.

def __composite_values__(self) -> Tuple[Decimal, entity.Currency]:

This is very simple, returning values tuple to create ValueObject.

Classmethod coerce allows us to convert DTO Money dataclass into DB aware model. Money composite. It’s needed for sqlalchemy session to track objects.

And the very important part is marking data as changed. For this, we need to call MutableComposite.changed() when setting attribute values directly. For this, we need to call it on settattr.

Below you have coplete code for mutable value object mapped to same table as Entity. Still domain model is not using model.Money directly. Instead all conversion is managed by db layer.

class Money(entity.Money, MutableComposite):
    def __composite_values__(self) -> Tuple[Decimal, entity.Currency]:
        return self.amount, self.currency

    def __setattr__(self, key: Text, value: Any) -> None:
        super().__setattr__(key, value)
        self.changed()

    @classmethod
    def coerce(cls, key: Text, value: entity.Money) -> 'Money':
        return Money(value.amount, value.currency)

    def __eq__(self, other: entity.Money) -> bool:
        same_amount = self.amount == other.amount
        same_currency = self.currency == other.currency
        return same_amount and same_currency


class Subscription(entity.Subscription, Base):
    __table__: Table
    __tablename__ = 'mutable_composite_vo_subscription_plans'

    id = Column(UUIDType(binary=True), primary_key=True)
    name = Column(String(100), nullable=False, index=True, unique=True)
    when_created = Column(DateTime, nullable=False, default=datetime.utcnow)
    when_updated = Column(DateTime, nullable=True, onupdate=datetime.utcnow)

    fee = composite(
        Money,
        Column('fee_amount', Float(asdecimal=True), nullable=False),
        Column('fee_currency', CurrencyType, nullable=False),
    )

    def __hash__(self):
        return hash(self.id)

Full code with tests is here.

3. Immutable Value Object stored in a different table

A separate table for value objects it’s a one-to-one relation. If we will put subscription_id in the value object table then we have one-to-many relation (or we can use unique). But in my opinion id of money entry should be inside the subscription table. But we need to be aware of deleting value object when it’s not needed anymore so there won’t be objects without entity.

The easy approach to this is building standard relations and a new model will be implementing the Money interface. We just need to remember about cleaning, so we need proper cascade settings. This would be enough if our application would use Fee model directly. It’s a DB implementation detail. So we need conversion from entity Money to Fee model. For this, we will use a listener for a set event that will create a new entry for every change. 

With this implementation on every change of value object, there will be an old entry delete command and new insert command. So maybe event with immutable vo we can use DB mapping that is modifying the old entry. This is described in the next point.

class Fee(entity.Money, Base):
    __table__: Table
    __tablename__ = 'mutable_separate_vo_fees'

    id = Column(Integer, primary_key=True, autoincrement=True)
    amount = Column(Float(asdecimal=True), nullable=False)
    currency = Column(CurrencyType, nullable=False)

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, entity.Money):
            return NotImplemented
        same_amount = self.amount == other.amount
        same_currency = self.currency == other.currency
        return same_amount and same_currency


class Subscription(entity.Subscription, Base):
    __table__: Table
    __tablename__ = 'mutable_separate_vo_subscription_plans'

    id = Column(UUIDType(binary=True), primary_key=True)
    name = Column(String(100), nullable=False, index=True, unique=True)
    when_created = Column(DateTime, nullable=False, default=datetime.utcnow)
    when_updated = Column(DateTime, nullable=True, onupdate=datetime.utcnow)
    fee_id = Column(Integer, ForeignKey(Fee.id), nullable=False)
    fee = relationship(
        Fee, cascade='save-update,merge,delete,delete-orphan', uselist=False,
        single_parent=True,
    )


@listens_for(Subscription.fee, 'set', retval=True)
def convert_money_to_fee_on_set(
        t: Subscription, value: entity.Money, old: Optional[Fee], e: Event,
) -> Fee:
    return Fee(value.amount, value.currency)

Full code with tests is here.

4. Mutable Value Object stored in a different table

This example is almost the same as the previous one. The only difference is in the listener. We create a new entry only if there is none (for example on creating an entity). In other cases, we will update the entry with a new value.

Worth noting is that if you will set

@dataclass(frozen=True)

on Value Object than this implementation will be valid for immutable version. And won’t create insert and delete on every change. So I would recommend this one in both cases.

class Fee(entity.Money, Base):
    __table__: Table
    __tablename__ = 'mutable_separate_vo_fees'

    id = Column(Integer, primary_key=True, autoincrement=True)
    amount = Column(Float(asdecimal=True), nullable=False)
    currency = Column(CurrencyType, nullable=False)

    def __eq__(self, other: Any) -> bool:
        if not isinstance(other, entity.Money):
            return NotImplemented
        same_amount = self.amount == other.amount
        same_currency = self.currency == other.currency
        return same_amount and same_currency


class Subscription(entity.Subscription, Base):
    __table__: Table
    __tablename__ = 'mutable_separate_vo_subscription_plans'

    id = Column(UUIDType(binary=True), primary_key=True)
    name = Column(String(100), nullable=False, index=True, unique=True)
    when_created = Column(DateTime, nullable=False, default=datetime.utcnow)
    when_updated = Column(DateTime, nullable=True, onupdate=datetime.utcnow)
    fee_id = Column(Integer, ForeignKey(Fee.id), nullable=False)
    fee = relationship(
        Fee, cascade='save-update,merge,delete,delete-orphan', uselist=False,
        single_parent=True, backref='subscription',
    )


@listens_for(Subscription.fee, 'set', retval=True)
def convert_money_to_fee_on_set(
        t: Subscription, value: entity.Money, old: Optional[Fee], e: Event,
) -> Fee:
    if old is None:
        return Fee(value.amount, value.currency)
    else:
        old.amount = value.amount
        old.currency = value.currency
        return old

Full code with tests is here.

close

Hi there 👋
It’s nice to meet you.

Sign up to join my mailing list.

I don’t spam!