Hi all. While trying to get more fluent with ORM I looked into a solution for
this Stack Overflow question. For a many-to-one relationship only one child object can have `is_default=True`. If we add a new child object or update an existing one with `is_default=True` we need to set the previous "default" child object to `is_default=False`.
I put together the following and it seems to work fine.
import sqlalchemy as db
from sqlalchemy import event
from sqlalchemy.orm import declarative_base, relationship
connection_uri = "sqlite://"
engine = db.create_engine(
connection_uri,
future=True,
echo=True,
)
Base = declarative_base()
class Address(Base):
__tablename__ = "address"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) address = db.Column(db.String, nullable=False)
is_default = db.Column(db.Boolean, nullable=False, default=False)
user = relationship("User", back_populates="addresses")
def __repr__(self):
return (
f"<Address(user={self.user}, address='{self.address}'"
f", is_default={self.is_default})>"
)
class User(Base):
__tablename__ = "user"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
addresses = relationship("Address", back_populates="user")
def __repr__(self):
Base.metadata.create_all(engine)
def _remove_other_default_addrs(mapper, connection, target):
if target.is_default:
connection.execute(
db.update(Address)
.where(Address.user_id == target.user_id)
.where(Address.is_default)
.values(is_default=False)
)
@event.listens_for(Address, "after_insert")
def receive_after_update(mapper, connection, target):
_remove_other_default_addrs(mapper, connection, target)
@event.listens_for(Address, "after_update")
def receive_after_update(mapper, connection, target):
_remove_other_default_addrs(mapper, connection, target)
with db.orm.Session(engine, future=True) as session:
gord = User(name="Gord")
gord_old_addr = Address(user=gord, address="123 Old Ave", is_default=True)
session.add_all([gord, gord_old_addr])
session.commit()
print(gord_old_addr)
gord_new_addr = Address(user=gord, address="567 New Blvd", is_default=True)
session.add(gord_new_addr)
session.commit()
print("==> session committed")
print(f"==> gord_old_addr.is_default is {gord_old_addr.is_default}")
# ==> gord_old_addr.is_default is False
However, I noticed that if I flushed the session instead of committing it, the old child object remains unchanged
print("==> session flushed")
print(f"==> gord_old_addr.is_default is {gord_old_addr.is_default}")
# ==> gord_old_addr.is_default is True
Is there something I can add to my event listeners (or maybe the session configuration) to ensure that the potentially affected objects get refreshed without actually committing?
(I suppose the same question would apply if there was a server-side trigger on the "address" table that did the same thing. That would be my first choice, but it seems that lots of people are required to implement business logic where they cannot change the database themselves and the DBA won't do it for them.)