SQLAlchemy - 不对关系强制执行外键约束

时间:2011-08-18 15:37:31

标签: python sql-server join foreign-keys sqlalchemy

我有一个Test模型/表和一个TestAuditLog模型/表,使用SQLAlchemy和SQL Server 2008.两者之间的关系是Test.id == TestAuditLog.entityId,其中一个测试有很多审计日志。 TestAuditLog旨在保留Test表中行的更改历史记录。我想跟踪删除Test的时间,但是我遇到了麻烦。在SQL Server Management Studio中,我将FK_TEST_AUDIT_LOG_TEST关系的“Enforce Foreign Key Constraint”属性设置为“否”,认为允许TestAuditLog行与entityId存在且不存在由于Test.id已被删除,因此会更长时间与Test相关联。但是,当我尝试使用SQLAlchemy创建TestAuditLog然后删除Test时,我收到错误:

  

(IntegrityError)('23000',“[23000] [Microsoft] [ODBC SQL Server驱动程序] [SQL Server]无法将值NULL插入列'AL_TEST_ID',表'TEST_AUDIT_LOG';列不允许空值。 UPDATE失败。(515)(SQLExecDirectW); [01000] [Microsoft] [ODBC SQL Server驱动程序] [SQL Server]语句已终止。(3621)“)u'UPDATE [TEST_AUDIT_LOG] SET [AL_TEST_ID] =? WHERE [TEST_AUDIT_LOG]。[AL_ID] =?' (无,8)

我认为由于TestTestAuditLog之间的外键关系,在删除Test行之后,SQLAlchemy尝试更新所有测试的审核日志以获得{ {1}} NULL。我不希望它这样做;我希望SQLAlchemy单独保留审计日志。如何告诉SQLAlchemy允许entityId未与任何entityId连接的审核日志存在?

我尝试从表中删除Test.id,但我仍然可以说ForeignKey并获取所有测试的审核日志,并且SQLAlchemy抱怨不知道如何加入myTest.auditsTest。当我在TestAuditLog上指定了primaryjoin时,对于没有列relationshipForeignKey的问题抱怨道。

以下是我的模特:

ForeignKeyConstraint

以下是我在保留审核日志的同时尝试删除测试的方法,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") ... 完好无损:

entityId

1 个答案:

答案 0 :(得分:12)

您可以通过以下方式解决此问题:

  • POINT-1: ForeignKey级别和SA级别都没有RDBMS
  • POINT-2:明确指定关系的连接条件
  • POINT-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列可以为空。通过这种方式,您仍然可以使用此列检查已删除对象的审计跟踪。

相关问题