我有一个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
行,该行具有一个不再连接到任何TestAuditLog
的entityId
行,因为该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)
我认为,由于Test
和Test
之间的外键关系,在删除TestAuditLog
行后,sqlAlchemy正在尝试更新所有测试的审核日志,使其具有Test
NULL
。我不希望这样做;我希望SQLAlchemy将审计日志放在一边。如何告诉sqlAlchemy允许存在其entityId
不与任何entityId
连接的审核日志?
我试图从我的表中删除Test.id
,但我仍然想说ForeignKey
并获取测试的所有审计日志,而sqlachemy抱怨说不知道如何加入myTest.audits
和Test
。然后,当我在TestAuditLog
上指定了一个primaryjoin
时,它抱怨没有对列使用relationship
或ForeignKey
。
以下是我的模型:
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列可以为空。这样,您仍然可以使用此列检查已删除对象的审核跟踪。