在提交之前,如何检索对模型实例所做的更改?

时间:2019-04-15 17:24:09

标签: python sqlalchemy

我正在尝试构建一个简单的小数据库模型,该模型可以方便地将对模型实例所做的更改存储为所谓的历史项目。整个想法是为所有历史记录项创建一个表,因此我使用了example作为sqlalchemy文档中的表。为了使此功能完全正常,我当然需要一些如何检索对实例本身所做的更改的信息。是否有一种优雅的方法可以从实例本身甚至会话中获取它?

我已经尝试存储通过__setattr__数据模型挂钩进行的更改。它确实做了一些工作,但我仍然想知道是否有一种“更清洁”的方法。

这是上面提到的方法的样子:

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

同样,该方法确实有效(尽管可以接受,但确实需要对JSONEncodedDict列类型进行更多工作)。但是仍然怀疑是否有可能以“更清洁”的方式做到这一点。

1 个答案:

答案 0 :(得分:1)

长话短说,我找到了问题的答案,而且看来我看起来还不够努力。当在模型实例上调用sqlalchemy的inspect方法时,会生成一个所谓的InstanceState对象。所述对象以所谓的ImmutableMapping的形式包含实例上所有与db相关的属性的AttributeState。从上述AttributeState中,您可以相当简单地提取实例的历史记录。整个过程如下所示:

inspection = inspect(obj)
diff = dict()
for attr in inspection.attrs:
    field = attr.key
    if attr.history.has_changes():
        added, unchanged, deleted = attr.history
        diff[field] = {
            "to": [*added, *unchanged] or None,
            "from": [*deleted, *unchanged] or None,
        }

此代码生成一个名为diff的字典,其中包含所有已更改字段的更改。产生的diff可以读为:

  

field上的obj已由from更改为to