title: How do you retrieve the changes made on a model instance before commited?
body: <p>I'm trying to build a simple little DB-model that allows me to easily store the changes made to a model instance as so called history-items. The whole idea is to have a single table for all the history-items so I used the <a href="https://docs.sqlalchemy.org/en/13/_modules/examples/generic_associations/generic_fk.html" rel="nofollow noreferrer">example</a> for that from the sqlalchemy docs. To make this fully functional I of course need to some how retrieve the changes made to an instance itself. Is there an elegant way to aquire this from the instance itself or even the session?</p>
  
  <p>I've already tried storing the changes while they are made via the <code>__setattr__</code> datamodel hook. And it did kind of work, but I'm still wondering if there is a "cleaner" way to this.</p>
  
  <p>This is what the above mentioned approach looks like:</p>
  
  <pre class="lang-py prettyprint-override"><code>from collections import defaultdict
  from datetime import datetime
  from enum import IntEnum, unique
  import json
  
  from sqlalchemy import and_, event, inspect, Column,\
                         Integer, Text, Enum, DateTime
  from sqlalchemy.types import TypeDecorator, VARCHAR
  from sqlalchemy.orm import foreign, backref, remote, relationship
  from sqlalchemy.ext.declarative import declarative_base
  
  
  __all__ = (
      'HistoryItem',
  )
  
  
  Base = declarative_base()
  
  
  class JSONEncodedDict(TypeDecorator):
      impl = VARCHAR
  
      def process_bind_param(self, value, dialect):
          if value is not None:
              value = json.dumps(value, default=str)
  
          return value
  
      def process_result_value(self, value, dialect):
          if value is not None:
              value = json.loads(value)
          return value
  
  
  class HistoryItem(Base):
      @unique
      class Types(IntEnum):
          CREATE = auto()
          EDIT   = auto()
          DELETE = auto()
  
          @classmethod
          def get_type(cls, obj):
              return {
                  HasHistory.States.FRESH:   HistoryItem.Types.CREATE,
                  HasHistory.States.EDITED:  HistoryItem.Types.EDIT,
                  HasHistory.States.DELETED: HistoryItem.Types.DELETE,
              }[obj.current_state]
  
      id = Column(Integer, primary_key=True)
      type = Column(Enum(Types))
      timestamp = Column(DateTime, default=lambda: datetime.now())
      diff = Column(JSONEncodedDict())
  
      target_discriminator = Column(String())
      target_id = Column(Integer())
  
      @property
      def target(self):
          return getattr(self, f"target_{self.target_discriminator}")
  
      @classmethod
      def build_for(cls, obj, user=None):
          assert isinstance(obj, HasHistory), "Can only build historyitems for models that have a history."
  
          type = HistoryItem.Type.get_type(obj)
          diff = obj.changes
  
          hi = HistoryItem(type=type, diff=diff)
          obj.history.append(hi)
          return hi
  
  
  class HasHistory:
      @unique
      class States(IntEnum):
          FRESH   = auto()
          EDITED  = auto()
          DELETED = auto()
  
      def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
          self._changes = defaultdict(list)
  
      def __setattr__(self, name, value):
          if name in self.__class__.__table__.c:
              self._changes.extend([getattr(self, name), value])
          return super().__setattr__(name, value)
  
      @property
      def changes(self):
          return {
              name: (changes[0], changes[1])
              for name, changes in self._changes.items()
          }
  
      @property
      def current_state(self):
          inspection = inspect(self)
          if inspection.transient:
              return HasHistory.States.FRESH
          elif inspection.deleted:
              return HasHistory.States.DELETED
          elif inspection.persistant:
              return HasHistory.States.EDITED
  
  
  @event.listens_for(HasHistory, "mapper_configured", propagate=True)
  def setup_listener(mapper, class_):
      discriminator = class_.__name__.lower()
      class_.history_discriminator = discriminator
      class_.history = relationship(
          HistoryItem,
          primaryjoin=and_(
              class_.id == foreign(remote(HistoryItem.target_id)),
              HistoryItem.target_discriminator == discriminator,
          ),
          backref=backref(
              f"target_{discriminator}",
              primaryjoin=remote(class_.id) == foreign(HistoryItem.target_id),
          ),
      )
  
      @event.listens_for(class_.history, "append")
      def append_history(self, history_item, event):
          history_item.target_discriminator = discriminator
  </code></pre>
  
  <p>Again, the approach does work (though admittedly it does require some more work on the JSONEncodedDict column type to make it compatible). But it still wonder if it is possible to do this in a "cleaner" fashion.</p>