'Imperatively map both entity and value object inside that entity to one table
Let's say I have an entity which is mapped to a postgresql table in sqlalchemy.
@dataclass
class User:
id: int
email: str
password: str
country: str
city: str
is_admin: bool = False
def __eq__(self, other) -> bool:
return self.id == other.id
user_table = sa.Table(
'user',
mapper_registry.metadata,
sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()'), nullable=False),
sa.Column('email', sa.String(100), nullable=False, unique=True),
sa.Column('password', sa.String(128), nullable=False),
sa.Column('country', sa.String(50)),
sa.Column('city', sa.String(50)),
sa.Column('is_admin', sa.Boolean(), server_default=sa.text('false'), nullable=False),
)
user_mapper = mapper_registry.map_imperatively(user_model.User, user_table)
That's not hard. Now I want to move address information into a separate value object inside User entity, but still map it to one table on postgresql side.
@dataclass(frozen=True)
class UserAddress:
country: str | None = None
city: str | None = None
@dataclass
class User:
id: int
email: str
password: str
address: UserAddress = UserAddress()
is_admin: bool = False
def __eq__(self, other) -> bool:
return self.id == other.id
user_table = sa.Table(
'user',
mapper_registry.metadata,
sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()'), nullable=False),
sa.Column('email', sa.String(100), nullable=False, unique=True),
sa.Column('password', sa.String(128), nullable=False),
sa.Column('country', sa.String(50)),
sa.Column('city', sa.String(50)),
sa.Column('is_admin', sa.Boolean(), server_default=sa.text('false'), nullable=False),
)
user_mapper = ???
What is the best way to do this?
UPD: I have found composites in SQLAlchemy documentation, which kinda works:
user_mapper = mapper_registry.map_imperatively(user_model.User, user_table, properties={
'info': composite(user_model.UserInfo, user_table.c.name, user_table.c.last_name, user_table.c.birthdate, user_table.c.phone)
})
BUT it requires UserInfo to have __composite_values__ method, which I want to avoid. I don't want UserInfo class to know anything about db-specific stuff and implement any methods for it.
Is there any other way?
Solution 1:[1]
Since there are no other answers, I am going to show what I ended up doing. Can't say that I am happy with this solution, but it works.
Basically I am defining models at domain layer and then injecting method(s) needed to use composites at infrastructure layer.
Something like this:
domain/user/user_model.py
@dataclass(frozen=True)
class UserAddress:
country: str | None = None
city: str | None = None
@dataclass
class User:
id: int
email: str
password: str
address: UserAddress = UserAddress()
is_admin: bool = False
def __eq__(self, other) -> bool:
return self.id == other.id
infra/postgresql/user/user_mappings.py
from app.domain.user import user_model
user_table = sa.Table(
'user',
mapper_registry.metadata,
sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()'), nullable=False),
sa.Column('email', sa.String(100), nullable=False, unique=True),
sa.Column('password', sa.String(128), nullable=False),
sa.Column('country', sa.String(50)),
sa.Column('city', sa.String(50)),
sa.Column('is_admin', sa.Boolean(), server_default=sa.text('false'), nullable=False),
)
def userinfo_comp_values(self):
return self.name, self.last_name, self.birthdate, self.phone
user_model.UserInfo.__composite_values__ = userinfo_comp_values
user_mapper = mapper_registry.map_imperatively(user_model.User, user_table, properties={
'info': composite(user_model.UserInfo, user_table.c.name, user_table.c.last_name, user_table.c.birthdate, user_table.c.phone)
})
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | estw272 |
