Hello all, Couple of weeks back (see [*]), while discussing that history_meta.py performs an update and an insert for each session.dirty object whenever session.flush() happens, Simon suggested the following solution to coalesce all changes within the same transaction into a single insert and hence a single version increase:
> 1. In a before_flush hook, check to see if the object has really changed (using the code from create_version). If it has, save it away in a set somewhere. > 2. In a before_commit hook, iterate over that set and create the actual history entries. I went ahead and produced a proof of concept of what he suggested. Although I have not fully tested it, it seems to work well. Nonetheless, I wanted to 2x check with you couple of points: a) For step 1, I used `session.info` to stash `history_objects` - a dictionary containing the newly created XYZHistory instances keyed by their "parent" XYZ instance's id and version. Is that a good place to store them? I guess I would need to "clear" it up on a `session.rollback` and `session.commit` ? b) For step 2, increase each XYZ instance's version `before_commit` right when any newly added XYZHistory instance is being added to the session. Any thoughts? suggestions? comments? Thanks in advance Here is a sneak peak of the changes: ```python # history_meta.py @event.listens_for(session, 'before_flush') def versioned_session_before_flush_handler(session): if "history-objects" not in session.info: history_objects = dict() session.info["history-objects"] = history_objects ... @event.listens_for(session, 'before_commit') def versioned_session_before_commit_handler(session): for hist in history_objects.values(): session.add(hist) vobj = session.query(inspect(hist).class_.__versioning_cls__).get(hist.id) # __versioning_cls__ is the class with the Versioned mixin vobj.version = hist.version + 1 # near the bottom of `create_version` ... hist_pk = (inspect(obj).identity, obj.version) hist = history_objects.get(hist_pk, None) if hist is None: hist = hist_cls() ... ``` [*] https://groups.google.com/forum/#!topic/sqlalchemy/jlEqIidnW_s https://groups.google.com/forum/#!topic/sqlalchemy/b0tEjftCWkM Here is the diff (against rel_0_9_8) $ git diff rel_0_9_8..test_version_relationship_0_9_8 -- examples/versioned_history/history_meta.py diff --git a/examples/versioned_history/history_meta.py b/examples/versioned_history/history_meta.py index f9e979a..84ba87b 100644 --- a/examples/versioned_history/history_meta.py +++ b/examples/versioned_history/history_meta.py @@ -7,6 +7,10 @@ from sqlalchemy import Table, Column, ForeignKeyConstraint, Integer, DateTime from sqlalchemy import event import datetime from sqlalchemy.orm.properties import RelationshipProperty +from sqlalchemy import inspect + +import logging +log = logging.getLogger("History-Meta") def col_references_table(col, table): for fk in col.foreign_keys: @@ -68,9 +72,11 @@ def _history_mapper(local_mapper): # "changed" column stores the UTC timestamp of when the # history row was created. # This column is optional and can be omitted. + """ cols.append(Column('changed', DateTime, default=datetime.datetime.utcnow, info=version_meta)) + """ if super_fks: cols.append(ForeignKeyConstraint(*zip(*super_fks))) @@ -95,6 +101,8 @@ def _history_mapper(local_mapper): bases = local_mapper.base_mapper.class_.__bases__ versioned_cls = type.__new__(type, "%sHistory" % cls.__name__, bases, {}) + versioned_cls.__versioning_cls__ = cls + m = mapper( versioned_cls, table, @@ -191,20 +210,46 @@ def create_version(obj, session, deleted=False): if obj_changed is True: break + log.info("%r relationship changed ? %r" % (obj, obj_changed)) + if not obj_changed and not deleted: return - attr['version'] = obj.version - hist = history_cls() - for key, value in attr.items(): - setattr(hist, key, value) - session.add(hist) - obj.version += 1 + hist_pk = (inspect(obj).identity, obj.version) + hist = session.info["history-objects"].get(hist_pk, None) + if hist is None: + hist = history_cls() + attr['version'] = obj.version + session.info["history-objects"].update({ hist_pk : hist }) + + for key, value in attr.items(): + setattr(hist, key, value) + + #session.add(hist) + #obj.version += 1 def versioned_session(session): @event.listens_for(session, 'before_flush') def before_flush(session, flush_context, instances): - for obj in versioned_objects(session.dirty): - create_version(obj, session) - for obj in versioned_objects(session.deleted): - create_version(obj, session, deleted=True) + versioned_session_before_flush_handler(session) + + @event.listens_for(session, 'before_commit') + def before_commit(session): + versioned_session_before_commit_handler(session) + +def versioned_session_before_flush_handler(session): + if "history-objects" not in session.info: + log.info("history-objects added to session.info") + session.info["history-objects"] = dict() + for obj in versioned_objects(session.dirty): + create_version(obj, session) + for obj in versioned_objects(session.deleted): + create_version(obj, session, deleted=True) + +def versioned_session_before_commit_handler(session): + for hist in session.info["history-objects"].values(): + session.add(hist) + vobj = session.query(inspect(hist).class_.__versioning_cls__).get(hist.id) + assert vobj and hist.version == vobj.version, "vobj: %r hist: %r" % (vobj, hist) + vobj.version = hist.version + 1 -- 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 post to this group, send email to sqlalchemy@googlegroups.com. Visit this group at https://groups.google.com/group/sqlalchemy. For more options, visit https://groups.google.com/d/optout.