使用SQLAlchemy时,我希望在传递相关对象时在Python对象上填写外键字段。例如,假定您的网络设备带有端口,并且假定该设备在数据库中具有复合主键。
如果我已经引用了“设备”实例,并且想要创建一个链接到该设备的新“端口”实例,而又不知道该实例是否已存在于数据库中,那么我将在SA中使用merge
操作。但是,仅在device
实例上设置port
属性是不够的。复合外键的字段将不会传播到port
实例,并且SA将无法确定数据库中行的存在,并且将无条件地发出INSERT
语句而不是{{1 }}。
以下代码示例演示了此问题。它们应作为一个UPDATE
文件运行,因此我们具有相同的内存中SQLite实例!仅出于可读性考虑将它们分开。
.py
该模型定义一个from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Unicode, ForeignKeyConstraint, create_engine
from sqlalchemy.orm import sessionmaker, relation
from textwrap import dedent
Base = declarative_base()
class Device(Base):
__tablename__ = 'device'
hostname = Column(Unicode, primary_key=True)
scope = Column(Unicode, primary_key=True)
poll_ip = Column(Unicode, primary_key=True)
notes = Column(Unicode)
ports = relation('Port', backref='device')
class Port(Base):
__tablename__ = 'port'
__table_args__ = (
ForeignKeyConstraint(
['hostname', 'scope', 'poll_ip'],
['device.hostname', 'device.scope', 'device.poll_ip'],
onupdate='CASCADE', ondelete='CASCADE'
),
)
hostname = Column(Unicode, primary_key=True)
scope = Column(Unicode, primary_key=True)
poll_ip = Column(Unicode, primary_key=True)
name = Column(Unicode, primary_key=True)
engine = create_engine('sqlite://', echo=True)
Base.metadata.bind = engine
Base.metadata.create_all()
Session = sessionmaker(bind=engine)
类,该类具有包含三个字段的复合PK。 Device
类通过这三列上的复合FK引用Port
。 Device
也与将使用该FK的Device
有关系。
首先,我们添加一个新设备和端口。当我们使用内存中的SQLite数据库时,这些将是数据库中仅有的两个条目。通过将一台设备插入数据库,我们在设备中有了一些东西 我们期望在会话“ sess2”中的后续合并中加载的表
Port
此块可以工作,但是它的编写方式并不符合我的预期。更准确地说,实例“ d1”用“主机名”,“作用域”和“ poll_ip”实例化,并且该实例被传递给“端口”实例“ p2”。我希望“ p2”将通过外键“接收”这三个值。但事实并非如此。我被迫在调用“合并”之前将值手动分配给“ p2”。如果未分配值,则SA将找不到标识,并尝试对“ p2”运行“ INSERT”查询,这将与现有实例冲突。
sess1 = Session()
d1 = Device(hostname='d1', scope='s1', poll_ip='pi1')
p1 = Port(device=d1, name='port1')
sess1.add(d1)
sess1.commit()
sess1.close()
此块显示了我希望它如何工作。我希望在创建Port实例时为“设备”分配一个值就足够了。
sess2 = Session()
d1 = Device(hostname='d1', scope='s1', poll_ip='pi1')
p2 = Port(device=d1, name='port1')
p2.hostname=d1.hostname
p2.poll_ip=d1.poll_ip
p2.scope = d1.scope
p2 = sess2.merge(p2)
sess2.commit()
sess2.close()
如何使最后一个块正常工作?
答案 0 :(得分:2)
在您显式或通过flush()
发出commit()
之前,子对象的FK不会更新。我认为这样做的原因是,如果关系的父对象也是具有自动增量PK的新实例,则SQLAlchemy需要从数据库获取PK,然后才能更新子对象上的FK(但我坚持予以纠正!)。
根据docs,一个merge()
:
检查实例的主键。如果存在,它将尝试 在本地身份映射中定位该实例。如果负载=真 标志保留为默认值,它还会为此检查数据库 主键(如果不在本地)。
如果给定实例没有主键,或者没有实例可以 找到并给出主键后,就会创建一个新实例。
由于您在merging
之前flushing
,因此p2
实例上的PK数据不完整,因此此行p2 = sess3.merge(p2)
返回一个新的Port
实例,其中包含与您先前创建的p2
相同的属性值。然后,session
最后发出刷新,将FK数据填充到sess3.commit()
上,然后在尝试写入p2
表时引发完整性错误。尽管插入port
只会提早出现完整性错误,而不能避免。
类似的事情会起作用:
sess3.flush()
This question提供了用于SQLAlchemy的def existing_or_new(sess, kls, **kwargs):
inst = sess.query(kls).filter_by(**kwargs).one_or_none()
if not inst:
inst = kls(**kwargs)
return inst
id_data = dict(hostname='d1', scope='s1', poll_ip='pi1')
sess3 = Session()
d1 = Device(**id_data)
p2 = existing_or_new(sess3, Port, name='port1', **id_data)
d1.ports.append(p2)
sess3.commit()
sess3.close()
样式函数的更详尽的示例。