出于性能原因,我有一个非规范化数据库,其中一些表包含从其他表中的许多行聚合的数据。我想使用SQLAlchemy events维护这个非规范化数据缓存。例如,假设我正在编写论坛软件,并希望每个Thread
都有一个列跟踪线程中所有注释的组合字数,以便有效地显示该信息:
class Thread(Base):
id = Column(UUID, primary_key=True, default=uuid.uuid4)
title = Column(UnicodeText(), nullable=False)
word_count = Column(Integer, nullable=False, default=0)
class Comment(Base):
id = Column(UUID, primary_key=True, default=uuid.uuid4)
thread_id = Column(UUID, ForeignKey('thread.id', ondelete='CASCADE'), nullable=False)
thread = relationship('Thread', backref='comments')
message = Column(UnicodeText(), nullable=False)
@property
def word_count(self):
return len(self.message.split())
因此,每次插入注释时(为简单起见,我们都说永远不会编辑或删除注释),我们希望更新关联的word_count
对象上的Thread
属性。所以我想做一些像
def after_insert(mapper, connection, target):
thread = target.thread
thread.word_count = sum(c.word_count for c in thread.comments)
print "updated cached word count to", thread.word_count
event.listen(Comment, "after_insert", after_insert)
因此,当我插入Comment
时,我可以看到事件触发并看到它已正确计算字数,但该更改未保存到数据库中的Thread
行。我没有看到有关after_insert documentation中更新的其他表格的任何警告,但我确实在其他一些表格中看到了一些注意事项,例如after_delete。
有没有一种支持的方法来使用SQLAlchemy事件执行此操作?我已经将SQLAlchemy事件用于许多其他事情了,所以我想做所有这些事情,而不是必须编写数据库触发器。
答案 0 :(得分:36)
after_insert()事件是执行此操作的一种方法,您可能会注意到它传递了一个SQLAlchemy Connection
对象,而不是Session
,就像其他刷新相关事件一样。映射器级刷新事件通常用于直接在给定Connection
上调用SQL:
@event.listens_for(Comment, "after_insert")
def after_insert(mapper, connection, target):
thread_table = Thread.__table__
thread = target.thread
connection.execute(
thread_table.update().
where(thread_table.c.id==thread.id).
values(word_count=sum(c.word_count for c in thread.comments))
)
print "updated cached word count to", thread.word_count
这里值得注意的是,直接调用UPDATE语句也比在整个工作单元流程中再次运行该属性更改更高效。
但是,这里并不真正需要像after_insert()这样的事件,因为我们知道在刷新之前“word_count”的值。我们实际上知道它是注释和线程对象相互关联,我们也可以使用属性事件始终在内存中保持Thread.word_count完全新鲜:
def _word_count(msg):
return len(msg.split())
@event.listens_for(Comment.message, "set")
def set(target, value, oldvalue, initiator):
if target.thread is not None:
target.thread.word_count += (_word_count(value) - _word_count(oldvalue))
@event.listens_for(Comment.thread, "set")
def set(target, value, oldvalue, initiator):
# the new Thread, if any
if value is not None:
value.word_count += _word_count(target.message)
# the old Thread, if any
if oldvalue is not None:
oldvalue.word_count -= _word_count(target.message)
这种方法的最大优点是也没有必要遍历thread.comments,对于卸载的集合意味着发出另一个SELECT。
另一种方法是在before_flush()中执行此操作。下面是一个快速而又脏的版本,可以对其进行细化,以便更仔细地分析已更改的内容,以确定word_count是否需要更新:
@event.listens_for(Session, "before_flush")
def before_flush(session, flush_context, instances):
for obj in session.new | session.dirty:
if isinstance(obj, Thread):
obj.word_count = sum(c.word_count for c in obj.comments)
elif isinstance(obj, Comment):
obj.thread.word_count = sum(c.word_count for c in obj.comments)
我会使用属性事件方法,因为它是最高性能和最新的。
答案 1 :(得分:4)
您可以使用SQLAlchemy-Utils aggregated
列执行此操作:http://sqlalchemy-utils.readthedocs.org/en/latest/aggregates.html