I have an application which listens for before_flush events and does some
side effects when column values are deleted. But in some cases the history
for an attribute is empty even though it has been deleted. For example:
person = Person(address="VALUE1")
self.session.add(person)
self.session.flush()
#self.session.commit()
# Without the next line, deletion of VALUE1 is not registered if committed
above
#assert person.address
person.address = None
self.session.commit()
Here, "VALUE1" is registered as deleted in the history only if:
- flush is called but not commit, or
- person.address is accessed before it is set to None
Is this a bug or is it expected?
A complete example is attached with some prints that show the attribute state.
SQLAlchemy version 1.1.14
python version 3.6.2 [GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)]
Best regards
Levon Saldamli
--
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 [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.
import logging
import sys
import sqlalchemy
from sqlalchemy import Column, Integer, inspect, event
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.sqltypes import String
logging.disable(logging.CRITICAL)
engine = create_engine('sqlite:///:memory:', echo=True)
Base = declarative_base()
Session = sessionmaker(bind=engine)
def myprint(*args, **kwargs):
print("-----", *args, **kwargs)
class SessionEventListener:
@classmethod
def _before_flush(cls, session: Session, flush_context, instances):
myprint("_before_flush")
cls._deleted_values(session)
@classmethod
def _after_flush(cls, session: Session, flush_context):
myprint("_after_flush")
cls._deleted_values(session)
@classmethod
def _after_flush_postexec(cls, session: Session, flush_context):
myprint("_after_flush_postexec")
cls._deleted_values(session)
@classmethod
def _before_commit(cls, session: Session):
myprint("_before_commit")
cls._deleted_values(session)
@classmethod
def _after_commit(cls, session: Session):
myprint("_after_commit")
cls._deleted_values(session)
@classmethod
def _deleted_values(cls, session):
added_or_dirty_models = list(session.new) + list(session.dirty)
for model in added_or_dirty_models:
for deleted_values in cls._deleted_values_in(model):
myprint(f"DELETED VALUE: {deleted_values}")
@classmethod
def _deleted_values_in(cls, model_instance):
states = cls._value_attribute_states(model_instance)
for attribute_state in states:
myprint(f"history: {attribute_state.key}: {attribute_state.history}")
deleted = attribute_state.history.deleted or []
for value in deleted:
if value is not None:
yield value
@classmethod
def _value_attribute_states(cls, model_instance):
instance_state = inspect(model_instance)
for attribute_state in instance_state.attrs:
yield attribute_state
@classmethod
def _column_is_of_type(cls, inspected_instance, column_key, wanted_type):
column = inspected_instance.mapper.columns.get(column_key)
return column is not None and isinstance(column.type, wanted_type)
@classmethod
def setup(cls, session):
event.listen(session, 'before_flush', cls._before_flush)
event.listen(session, 'after_flush', cls._after_flush)
event.listen(session, 'after_flush_postexec', cls._after_flush_postexec)
event.listen(session, 'before_commit', cls._before_commit)
event.listen(session, 'after_commit', cls._after_commit)
class Person(Base):
__tablename__ = 'container'
id = Column(Integer, primary_key=True, autoincrement=True)
address = Column(String, nullable=True, unique=True)
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
Base.metadata.create_all(engine)
class TestClass:
def __init__(self, session):
self.session = session
SessionEventListener.setup(self.session)
def test_1(self):
""" DELETED VALUE1 and DELETED VALUE2 is expected"""
person = Person(address="VALUE1")
self.session.add(person)
self.session.commit()
value_1 = person.address
myprint(f"VALUE_1: {value_1}")
person.address = "VALUE2"
self.session.flush()
self.session.commit()
# Without the next line, deletion of VALUE2 is not registered if committed above
# assert person.address
person.address = "VALUE3"
self.session.commit()
def test_2(self):
""" DELETED VALUE1 is expected"""
person = Person(address="VALUE1")
self.session.add(person)
#self.session.flush()
self.session.commit()
# Without the next line, deletion of VALUE1 is not registered if committed above
# assert person.address
person.address = None
self.session.commit()
myprint("SQLALchemy version", sqlalchemy.__version__ )
myprint("python version", sys.version )
session = Session()
test1 = TestClass(session)
myprint("")
myprint("TEST_1")
myprint("")
test1.test_1()
myprint("")
myprint("TEST_2")
myprint("")
session.close()
session = Session()
test2 = TestClass(session)
test2.test_2()
session.close()