我正在尝试构建一个简单的小数据库模型,该模型可以方便地将对模型实例所做的更改存储为所谓的历史项目。整个想法是为所有历史记录项创建一个表,因此我使用了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列类型进行更多工作)。但是仍然怀疑是否有可能以“更清洁”的方式做到这一点。
答案 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
。