Reputation: 347
I have a mixin class that I define near the beginning of the SQLAlchemy app and then inherit on pretty much every declarative model I use.
class Owned(object):
@declared_attr
def created_by_id(cls):
return Column(Integer, ForeignKey("accounts.id"),
nullable = True)
@declared_attr
def created_by(cls):
return relationship("Account", foreign_keys = cls.created_by_id)
@declared_attr
def updated_by_id(cls):
return Column(Integer, ForeignKey("accounts.id"),
nullable = True)
@declared_attr
def updated_by(cls):
return relationship("Account", foreign_keys = cls.updated_by_id)
This works well for most intended use cases.
class Thing(Owned, Base): # Base is from SQLAlchemy's declarative_base()
pass
account = session.query(Account).first()
thing = Thing(created_by = account, updated_by = account)
session.add(thing)
session.commit()
session.refresh(thing)
assert thing.created_by == account # pass
assert thing.updated_by == account # pass
However, I get unexpected behaviour when I define Account
itself as inheriting from Owned
.
class Account(Owned, Base):
pass
account_old = session.query(Account).first()
account_new = Account(created_by = account_old, updated_by = account_old)
session.add(account_new)
session.commit()
session.refresh(account_new)
assert account_new.created_by_id == account_old.id # pass
assert account_new.updated_by_id == account_old.id # pass
# BUT!
assert account_new.created_by == account_old # fail
assert account_new.updated_by == account_old # fail
account_new.created_by # []
account_new.updated_by # []
I see that, in this case, I've turned created_by_id
and updated_by_id
into self-referential foreign keys. What I don't understand, however, is why SQLAlchemy isn't populating their associated relationship
columns with the expected Account
instances.
What am I doing wrong?
Upvotes: 2
Views: 881
Reputation: 2006
Convenient solution would be using @declared_attr.directive
decorator.
So, in your mixin class you can write something like following:
@declared_attr.directive
def order(cls) -> Mapped[OrderTable | None]:
return relationship(primaryjoin=f'{cls.__name__}.order_no==OrderTable.id')
Upvotes: 0
Reputation: 52939
In an adjacency list relationship the "direction" is assumed by default to be one-to-many. Using the remote_side
directive establishes that the relationship is many-to-one, which is what you're after:
def _create_relationship(cls, foreign_keys):
kwgs = {}
# Bit of a chicken or egg situation:
if cls.__name__ == "Account":
kwgs["remote_side"] = [cls.id]
return relationship("Account", foreign_keys=foreign_keys, **kwgs)
class Owned:
@declared_attr
def created_by_id(cls):
return Column(Integer, ForeignKey("accounts.id"),
nullable = True)
@declared_attr
def created_by(cls):
return _create_relationship(cls, cls.created_by_id)
@declared_attr
def updated_by_id(cls):
return Column(Integer, ForeignKey("accounts.id"),
nullable = True)
@declared_attr
def updated_by(cls):
return _create_relationship(cls, cls.updated_by_id)
Upvotes: 0