Python SQLAlchemy-don';t在关系上强制外键约束

Python SQLAlchemy-don';t在关系上强制外键约束,python,sql-server,join,foreign-keys,sqlalchemy,Python,Sql Server,Join,Foreign Keys,Sqlalchemy,我有一个Testmodel/table和一个TestAuditLogmodel/table,使用SQLAlchemy和sqlserver2008。两者之间的关系是Test.id==TestAuditLog.entityId,其中一个测试有多个审核日志TestAuditLog用于保存Test表中行的更改历史记录。我也想跟踪测试何时被删除,但我在这方面遇到了问题。在SQLServerManagementStudio中,我将FK\u TEST\u AUDIT\u LOG\u TEST关系的“”属性设置

我有一个
Test
model/table和一个
TestAuditLog
model/table,使用SQLAlchemy和sqlserver2008。两者之间的关系是Test.id==TestAuditLog.entityId,其中一个测试有多个审核日志
TestAuditLog
用于保存
Test
表中行的更改历史记录。我也想跟踪
测试
何时被删除,但我在这方面遇到了问题。在SQLServerManagementStudio中,我将
FK\u TEST\u AUDIT\u LOG\u TEST
关系的“”属性设置为“否”,认为这将允许
TestAuditLog
行与
entityId
一起存在,该行不再连接到任何
TEST.id
,因为
TEST
已被删除。但是,当我尝试使用SQLAlchemy创建一个
TestAuditLog
,然后删除
Test
时,我得到一个错误:

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

我认为,由于
Test
TestAuditLog
之间的外键关系,在我删除
Test
行之后,SQLAlchemy正在尝试更新所有测试的审计日志,使其具有
NULL
实体ID
。我不希望这样做;我希望SQLAlchemy不处理审计日志。我怎么能这样做呢ell SQLAlchemy是否允许存在其
entityId
未与任何
Test.id
连接的审核日志

我试图从我的表中删除
ForeignKey
,但我仍然希望能够说
myTest.audits
,并获得测试的所有审核日志,SQLAlchemy抱怨不知道如何加入
test
TestAuditLog
。当时我在
关系上指定了
primaryjoin
,它抱怨没有列的
ForeignKey
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")
    ...
下面是我如何试图删除一个测试,同时保持其审核日志、其
entityId
的完整性:

    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点:
    RDBMS
    级别和SA级别上都没有
    ForeignKey
  • 第2点:明确指定关系的连接条件
  • 第3点:标记关系级联以依赖于标记
下面完整的代码片段应该会给您一个想法(要点在
代码中突出显示):

从sqlalchemy导入创建引擎、列、整数、字符串、外键
从sqlalchemy.orm导入作用域为的会话、会话生成器、关系
从sqlalchemy.ext.declarative导入声明性基础
Base=声明性_Base()
engine=create_engine('sqlite://:memory:',echo=False)
会话=会话生成器(绑定=引擎)
类TestAuditLog(基本):
__tablename\uuuu='TEST\u AUDIT\u LOG'
id=列(整数,主键=True)
注释=列(字符串)
entityId=Column('TEST\u AUDIT\u LOG',Integer,nullable=False,
#第1点
#ForeignKey('TEST.TS\u TEST\u ID',ondelete=“CASCADE”),
)
定义初始化(self,注释):
self.comment=注释
定义报告(自我):
返回“”%(self.id、self.entityId、self.comment)
等级测试(基础):
__tablename_uu='TEST'
id=列('TS\u TEST\u id',整数,主键=True)
名称=列(字符串)
审核=关系(TestAuditLog,backref='test',
#第2点
primaryjoin=“Test.id==TestAuditLog.entityId”,
外键=[TestAuditLog.\uUuU表\uUuU.c.TEST\u审计\u日志],
#第3点
被动_='all',
)
定义初始化(self,name):
self.name=名称
定义报告(自我):
返回“”%(self.id,self.name)
Base.metadata.create_all(引擎)
###################
##测验
会话=会话()
#创建测试数据
测试=[范围(3)内i的测试(“测试-”+str(i))]
_cnt=0
对于测试中的测试:
对于范围(2)内的:
_t、 audits.append(TestAuditLog(“comment-”+str(_cnt)))
_cnt+=1
会话。添加所有(测试)
session.commit()
session.expunge_all()
打印'-'*80
#检查测试数据,删除一个测试
t1=session.query(Test).get(1)
打印“t:”,t1
打印“t.a:”,t1.a
会话.删除(t1)
session.commit()
session.expunge_all()
打印'-'*80
#检查已删除测试的数据库中是否仍有审核
t1=session.query(Test).get(1)
断言t1是None
_q=session.query(TestAuditLog.filter)(TestAuditLog.entityId==1)
_r=_q.all()
断言len(_r)==2
对于_ain _r:
打印

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

关系
上的
被动\u deletes='all'
成功了!我就是这样做的le保留关系和SQLAlchemy没有返回并尝试删除
测试上的
entityId
。谢谢!仅供参考-还需要在关系的父端设置
lazy=“dynamic”
,以便SQLAlchemy在不需要时不会获取所有子项(即,仅更新父表中不相关的字段时)。@Greg0ry:不,您不需要。如:中所述,默认情况下,所有对象间关系都是延迟加载的……因此,除非您执行其他操作,否则父对象不应加载子对象
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