I have a users
table and a roles
table, such that a User
optionally has a single Role
:
class User(Base):
id: Mapped[int] = mapped_column(primary_key=True)
role: Mapped[Optional["Role"]] = relationship(back_populates="user")
# ...
class Role(Base):
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
user: Mapped[Optional["User"]] = relationship(back_populates="role")
# ...
Now in code I would like that User.role
always return a Role
. If a specific User
doesn't have a Role
, it should just return a new default one: Role()
.
How can I replace User.role
with some kind of getter function that does this?
A somewhat obvious solution would be:
class User(Base):
# ...
@property
def role_or_new():
return self.role or Role()
However, I kind of want to override the default attribute name, role
.
There are some similarly named existing questions, but I think they're all subtly different:
role
behavesFollowing @metatoaser's hint, I renamed the relationship from the user and created a property with the original name for compatibility:
class User(Base):
id: Mapped[int] = mapped_column(primary_key=True)
role_row: Mapped[Optional["Role"]] = relationship(back_populates="user")
# ...
@property
def role() -> Role:
if self.role_row is None:
self.role_row = Role()
# Consider using `make_transient(self.role_row)` here
return self.role_row
class Role(Base):
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
user: Mapped[Optional["User"]] = relationship(back_populates="role_row")
# ...
Mind that the back_populates=
of the Role
model is now different too.
I opted for a public property, role_row
, such an advanced user could still choose to use the original relationship, which would be discouraged with a name like _role
.
Final note: the newly created Role()
will be queued in the session by default! E.g.:
user = session.get(User, 1)
if user.role.access:
# ...
user.email = new_email
session.commit()
...will trigger the creation of a new Role
in the database! This is because every created object by default corresponds to a database row.
If you want to prevent this default object from always appearing in the database on the next commit, you could add a call to make_transient in the custom getter.