On Tue, Mar 1, 2016 at 9:43 AM, Bastien Sevajol <[email protected]> wrote:
> Hi, > > I'm confronted to an misunderstanding or unexpected behavior of session > trigger flush. I want to manage revision of a document, like in this > <http://docs.sqlalchemy.org/en/latest/_modules/examples/versioned_rows/versioned_rows.html> > doc example. > But the behavior is unexpected: I want to dot something like this: > > 1. create a document with it's first revision > 2. flush -> creation of document + revision id database > 3. modify revision of dicument > 4. flush -> session *before_flush* > <http://docs.sqlalchemy.org/en/latest/orm/events.html#sqlalchemy.orm.events.SessionEvents.before_flush> > hook create a new revision instead update it > > But the following example code make a unexpected flush in point "3." > Example test.py: > > from sqlalchemy import create_engine, Column, Integer, Text, ForeignKey, > inspect, String > from sqlalchemy.ext.declarative import declarative_base > from sqlalchemy.orm import sessionmaker, SessionExtension, relationship > > DeclarativeBase = declarative_base() > > > class ContentRevision(DeclarativeBase): > __tablename__ = 'content_revision' > revision_id = Column(Integer, primary_key=True) > content_id = Column(Integer, ForeignKey('content.id')) > description = Column(Text()) > title = Column(String(32)) > node = relationship("Content", foreign_keys=[content_id], > back_populates="revisions") > > @classmethod > def new_from(cls, revision): > columns = (column.key for column in inspect(cls).attrs if column.key > != 'revision_id') > new_revision = cls() > > for column_name in columns: > column_value = getattr(revision, column_name) > setattr(new_revision, column_name, column_value) > > return new_revision > > > class Content(DeclarativeBase): > __tablename__ = 'content' > id = Column(Integer, primary_key=True) > revisions = relationship("ContentRevision", > foreign_keys=[ContentRevision.content_id], > back_populates="node") > > > class VersionExtension(SessionExtension): > def before_flush(self, session, flush_context, instances): > print('EVENT: before_flush') > for instance in session.dirty: > if isinstance(instance, ContentRevision): > print('EVENT: before_flush: ContentRevision found in > dirty') > if session.is_modified(instance, passive=True): > print('EVENT: before_flush: ContentRevision has been > modified') > if inspect(instance).has_identity: > print('EVENT: before_flush: revision updated, > create new instead') > previous_revision = instance > new_revision = ContentRevision.new_from(instance) > session.expunge(previous_revision) > session.add(new_revision) > else: > print('EVENT: before_flush: revision is a new > revision, no change') > > # Prepare database and session > > engine = create_engine('sqlite://', echo=False) > DeclarativeBase.metadata.create_all(engine) > session_maker = sessionmaker(engine, extension=[VersionExtension()]) > session = session_maker() > > # Start example scenario > > print('SCENARIO: Create new content') > content1 = Content(revisions=[ContentRevision(description='rev1', title= > 'title1')]) > print('SCENARIO: Add content to session') > session.add(content1) > print('SCENARIO: flush session') > session.flush() > > assert session.query(ContentRevision.revision_id, > ContentRevision.title, > ContentRevision.description).order_by(ContentRevision > .revision_id).all() == \ > [(1, 'title1', 'rev1')] > > print('SCENARIO: update revision title') > content1.revisions[0].title = 'title2' > > > assert session.query(ContentRevision.revision_id, > ContentRevision.title, > ContentRevision.description).order_by(ContentRevision > .revision_id).all() == \ > [(1, 'title1', 'rev1'), (2, 'title2', 'rev1')] # Why session > flushed here ? > > > print('SCENARIO: update revision description') > content1.revisions[0].description = 'rev2' > > assert session.query(ContentRevision.revision_id, > ContentRevision.title, > ContentRevision.description).order_by(ContentRevision > .revision_id).all() == \ > [(1, 'title1', 'rev1'), (2, 'title2', 'rev1')] # Why session > don't flushed here ? > > print('SCENARIO: flush session') > session.flush() > > assert session.query(ContentRevision.revision_id, > ContentRevision.title, > ContentRevision.description).order_by(ContentRevision > .revision_id).all() == \ > [(1, 'title1', 'rev1'), (2, 'title2', 'rev1')] # Why session > don't flushed here ? > > print('SCENARIO: end') > > > Output is (for python3.4 test.py): > > SCENARIO: Create new content > SCENARIO: Add content to session > SCENARIO: flush session > EVENT: before_flush > SCENARIO: update revision title > EVENT: before_flush > EVENT: before_flush: ContentRevision found in dirty > EVENT: before_flush: ContentRevision has been modified > EVENT: before_flush: revision updated, create new instead > SCENARIO: update revision description > SCENARIO: flush session > SCENARIO: end > > Why session flush at *content1.revisions[0].title = 'title2'* line ? Why > session *don't *flush at last session.flush() line ? > Is it possible to use *before_flush* event to make my "new revision" when > finish to edit it's values ? > > The answer to your first question is that SQLAlchemy has a feature called "autoflush": http://docs.sqlalchemy.org/en/rel_1_0/orm/session_basics.html#flushing When you call session.query(), SQLAlchemy will first flush the session to ensure that the results of the query match any changes that you've made in the session. As for why you don't get a new revision when editing the description, try inserting the following after the "description = 'rev2'" line: print content1 in session print content1.revisions[0] in session print content1.revisions[1] in session For me at least, it turns out that revisions[0] is not in the session. I assume this is because of the "session.expunge(previous_revision)" call in the VersionExtension. Since the object is no longer part of the session, changes you make to the object will not be flushed to the database. Hope that helps, Simon -- 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.
