Hi all. While trying to get more fluent with ORM I looked into a solution 
for this Stack Overflow question 
<https://stackoverflow.com/q/66027263/2144390>. 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.

# https://stackoverflow.com/q/66027263/2144390
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):
        return f"<User(name='{self.name}')>"


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.id != target.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.)

-- 
SQLAlchemy - 
The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and Verifiable 
Example.  See  http://stackoverflow.com/help/mcve for a full description.
--- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/sqlalchemy/4d2990e4-78fd-48de-8e92-bd2eb39af05fn%40googlegroups.com.

Reply via email to