在SQLAlchemy中从表反向映射到模型

时间:2010-05-17 16:48:06

标签: python sqlalchemy

要在我的基于SQLAlchemy的应用程序中提供活动日志,我有一个这样的模型:

class ActivityLog(Base):
    __tablename__ = 'activitylog'
    id = Column(Integer, primary_key=True)
    activity_by_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    activity_by = relation(User, primaryjoin=activity_by_id == User.id)
    activity_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    activity_type = Column(SmallInteger, nullable=False)

    target_table = Column(Unicode(20), nullable=False)
    target_id = Column(Integer, nullable=False)
    target_title = Column(Unicode(255), nullable=False)

日志包含多个表的条目,因此我不能使用ForeignKey关系。日志条目如下所示:

doc = Document(name=u'mydoc', title=u'My Test Document',
               created_by=user, edited_by=user)
session.add(doc)
session.flush() # See note below
log = ActivityLog(activity_by=user, activity_type=ACTIVITY_ADD,
                  target_table=Document.__table__.name, target_id=doc.id,
                  target_title=doc.title)
session.add(log)

这给我留下了三个问题:

  1. 我必须在doc对象获得id之前刷新会话。如果我使用了ForeignKey列和relation映射器,我可以简单地调用ActivityLog(target=doc)并让SQLAlchemy完成工作。有没有办法解决需要手工冲洗的问题?

  2. target_table参数过于冗长。我想我可以使用ActivityLog中的target属性设置器来解决这个问题,该属性设置器会自动从给定实例中检索表名和id。

  3. 最重要的是,我不确定如何从数据库中检索模型实例。给定一个ActivityLog实例log,调用self.session.query(log.target_table).get(log.target_id)不起作用,因为query()期望模型作为参数。

  4. 一种解决方法似乎是使用多态并从ActivityLog识别的基础模型派生我的所有模型。像这样:

    class Entity(Base):
        __tablename__ = 'entities'
        id = Column(Integer, primary_key=True)
        title = Column(Unicode(255), nullable=False)
        edited_at = Column(DateTime, onupdate=datetime.utcnow, nullable=False)
        entity_type = Column(Unicode(20), nullable=False)
        __mapper_args__ = {'polymorphic_on': entity_type}
    
    class Document(Entity):
        __tablename__ = 'documents'
        __mapper_args__ = {'polymorphic_identity': 'document'}
        body = Column(UnicodeText, nullable=False)
    
    class ActivityLog(Base):
        __tablename__ = 'activitylog'
        id = Column(Integer, primary_key=True)
        ...
        target_id = Column(Integer, ForeignKey('entities.id'), nullable=False)
        target = relation(Entity)
    

    如果我这样做,ActivityLog(...).target在引用Document时会给我一个Document实例,但我不确定是否需要为所有内容提供两个表的开销。我应该继续这样做吗?

2 个答案:

答案 0 :(得分:1)

解决这个问题的一种方法是多态关联。它应解决您的所有3个问题,并使数据库外键约束起作用。请参阅SQLAlchemy源代码中的polymorphic association example。 Mike Bayer有一个旧的blogpost,可以更详细地讨论这个问题。

答案 1 :(得分:1)

绝对浏览博客帖子和蚂蚁链接的示例。我没有找到解释上的混淆,而是假设有更多关于这个主题的经验。

我可以提出的一些建议是:

  • ForeignKeys:总的来说,我同意他们是一件好事,但我不确定它在你的情况下在概念上是否重要:你似乎正在使用这个ActivityLog作为正交交叉切割问题(AOP);但是具有外键的版本会有效地使您的业务对象意识到<{1>}的。使用架构设置将ActivityLog用于审计目的的另一个问题是,如果允许对象FKdeletion要求将删除此对象的所有ActivityLog条目。
  • FK:每当您创建/修改(/删除)对象时,您都会手动执行所有这些日志记录。使用SA,您可以使用before_commit实现Automatic logging,这将自动为您完成工作。

这样你完全可以避免写下面的部分:

SessionExtension

EDIT-1:已添加完整的示例代码

  • 该代码基于http://techspot.zzzeek.org/?p=13的第一个非FK版本。
  • 不使用FK的选择是基于以下事实:出于审计目的时 主对象被删除,它不应级联删除所有审计日志条目。 这也使得可审计对象无意识了解他们正在接受审计的事实。
  • 实施使用SA一对多关系。有可能是一些 对象被多次修改,这将导致许多审计日志条目。 默认情况下,SA会在向其添加新条目时加载关系对象 名单。假设在“正常”使用期间,我们只想添加新审核 日志条目,我们使用log = ActivityLog(activity_by=user, activity_type=ACTIVITY_ADD, target_table=Document.__table__.name, target_id=doc.id, target_title=doc.title) session.add(log) 标志,以便从主对象的关系 将从不加载。从对方导航时加载, 并且还可以使用自定义查询从主对象加载,如图所示 在示例中也使用lazy='noload' readonly属性。

代码(可通过某些测试运行):

activitylog_readonly