我有一个Test模型/表和一个TestAuditLog模型/表,使用sqlAlchemy和SQL Server 2008。两者之间的关系是Test.id == TestAuditLog.entityId,其中一个测试有许多审计日志。TestAuditLog用于保存对Test表中的行所做更改的历史记录。我还想跟踪删除Test的时间,但我遇到了这个问题。在SQL Server Management Studio中,我将关系的“Enforce Foreign Key Constraint属性设置为“no”,认为这将允许存在一个FK_TEST_AUDIT_LOG_TEST行,该行具有一个不再连接到任何TestAuditLogentityId行,因为该Test.id已被删除。但是,当我尝试用sqlAlchemy创建一个Test然后删除TestAuditLog时,我得到一个错误:
(integrityerror)(“23000”,“[23000][microsoft][odbc sql server driver][sql server]无法将值NULL插入列“al_test_id”,表“test_audit_log”;列不允许空值。更新失败。(515)(sqlexecdirectw);[01000][microsoft][odbc sql server driver][sql server]语句已终止。(3621)“)u'update[test_audit_log]set[al_test_id]=?其中[test_audit_log]。[al_id]=?'(无,8)
我认为,由于TestTest之间的外键关系,在删除TestAuditLog行后,sqlAlchemy正在尝试更新所有测试的审核日志,使其具有TestNULL。我不希望这样做;我希望SQLAlchemy将审计日志放在一边。如何告诉sqlAlchemy允许存在其entityId不与任何entityId连接的审核日志?
我试图从我的表中删除Test.id,但我仍然想说ForeignKey并获取测试的所有审计日志,而sqlachemy抱怨说不知道如何加入myTest.auditsTest。然后,当我在TestAuditLog上指定了一个primaryjoin时,它抱怨没有对列使用relationshipForeignKey
以下是我的模型:

class TestAuditLog(Base, Common):
    __tablename__ = u'TEST_AUDIT_LOG'
    entityId = Column(u'AL_TEST_ID', INTEGER(), ForeignKey(u'TEST.TS_TEST_ID'),
        nullable=False)
    ...

class Test(Base, Common):
    __tablename__ = u'TEST'
    id = Column(u'TS_TEST_ID', INTEGER(), primary_key=True, nullable=False)
    audits = relationship(TestAuditLog, backref="test")
    ...

下面是我如何在保留审计日志的同时删除一个测试,它们的ForeignKeyConstraint是完整的:
    test = Session.query(Test).first()
    Session.begin()
    try:
        Session.add(TestAuditLog(entityId=test.id))
        Session.flush()
        Session.delete(test)
        Session.commit()
    except:
        Session.rollback()
        raise

最佳答案

您可以通过以下方式解决此问题:
第1点:在ForeignKey水平和sa水平上都没有RDBMS
点2:显式指定关系的联接条件
点3:标记关系级联依赖于passive_deletes标志
下面完整工作的代码片段应该给您一个想法(要点在code中突出显示):

from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import scoped_session, sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
engine = create_engine('sqlite:///:memory:', echo=False)

Session = sessionmaker(bind=engine)

class TestAuditLog(Base):
    __tablename__ = 'TEST_AUDIT_LOG'
    id = Column(Integer, primary_key=True)
    comment = Column(String)

    entityId = Column('TEST_AUDIT_LOG', Integer, nullable=False,
                     # POINT-1
                     #ForeignKey('TEST.TS_TEST_ID', ondelete="CASCADE"),
                     )

    def __init__(self, comment):
        self.comment = comment

    def __repr__(self):
        return "<TestAuditLog(id=%s entityId=%s, comment=%s)>" % (self.id, self.entityId, self.comment)

class Test(Base):
    __tablename__ = 'TEST'
    id = Column('TS_TEST_ID', Integer, primary_key=True)
    name = Column(String)

    audits = relationship(TestAuditLog, backref='test',
                # POINT-2
                primaryjoin="Test.id==TestAuditLog.entityId",
                foreign_keys=[TestAuditLog.__table__.c.TEST_AUDIT_LOG],
                # POINT-3
                passive_deletes='all',
            )

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return "<Test(id=%s, name=%s)>" % (self.id, self.name)


Base.metadata.create_all(engine)

###################
## tests
session = Session()

# create test data
tests = [Test("test-" + str(i)) for i in range(3)]
_cnt = 0
for _t in tests:
    for __ in range(2):
        _t.audits.append(TestAuditLog("comment-" + str(_cnt)))
        _cnt += 1
session.add_all(tests)
session.commit()
session.expunge_all()
print '-'*80

# check test data, delete one Test
t1 = session.query(Test).get(1)
print "t: ", t1
print "t.a: ", t1.audits
session.delete(t1)
session.commit()
session.expunge_all()
print '-'*80

# check that audits are still in the DB for deleted Test
t1 = session.query(Test).get(1)
assert t1 is None
_q = session.query(TestAuditLog).filter(TestAuditLog.entityId == 1)
_r = _q.all()
assert len(_r) == 2
for _a in _r:
    print _a

另一个选项是复制FK中使用的列,并使用ON CASCADE SET NULL选项使FK列可以为空。这样,您仍然可以使用此列检查已删除对象的审核跟踪。

10-08 09:46