Skip to main content
 首页 » 编程设计

python之如何通过在构造函数中传递相关实体来使 SQLAlchemy 为外键设置值

2024年09月03日798°冷暖

当使用 SQLAlchemy 时,我希望在传入相关对象时在 Python 对象上填写外键字段。例如,假设您有带端口的网络设备,并假设该设备在数据库中有一个复合主键。

如果我已经有一个“设备”实例的引用,并且想创建一个链接到该设备的新“端口”实例,而不知道它是否已经存在于数据库中,我将使用 merge在南非运营。但是,仅在 port 实例上设置 device 属性是不够的。复合外键的字段将不会传播到 port 实例,SA 将无法确定数据库中是否存在该行并无条件地发出 INSERT 语句而不是 UPDATE

以下代码示例演示了该问题。它们应该作为一个 .py 文件运行,这样我们就有了相同的内存中 SQLite 实例!它们只是为了便于阅读而拆分。

模型定义

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) 

该模型定义了一个 Device 类,该类具有包含三个字段的复合 PK。 Port 类通过这三列上的复合 FK 引用 DeviceDevice 也与将使用该 FK 的 Port 有关系。

使用模型

首先,我们添加一个新的设备和端口。由于我们正在使用内存中的 SQLite 数据库,因此这将是数据库中仅有的两个条目。通过将一个设备插入数据库,我们在设备中有了一些东西 我们希望在 session “sess2”中的后续合并中加载的表

sess1 = Session() 
d1 = Device(hostname='d1', scope='s1', poll_ip='pi1') 
p1 = Port(device=d1, name='port1') 
sess1.add(d1) 
sess1.commit() 
sess1.close() 

工作示例

这个 block 可以工作,但它不是以我期望的方式编写的。更准确地说,实例“d1”是用“hostname”、“scope”和“poll_ip”实例化的,并且该实例被传递给“Port”实例“p2”。我希望“p2”会通过外键“接收”这 3 个值。但事实并非如此。在调用“合并”之前,我被迫手动将值分配给“p2”。如果未分配值,SA 将找不到身份并尝试运行“p2”的“插入”查询,这将与现有实例发生冲突。

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() 

错误的例子(但希望它能工作)

此 block 显示了我希望它如何工作。我希望在创建 Port 实例时为“device”赋值就足够了。

sess3 = Session() 
d1 = Device(hostname='d1', scope='s1', poll_ip='pi1') 
p2 = Port(device=d1, name='port1') 
p2 = sess3.merge(p2) 
sess3.commit() 
sess3.close() 

我怎样才能使最后一个 block 起作用?

请您参考如下方法:

在您显式或通过 commit() 发出 flush() 之前,子对象的 FK 不会更新。我认为这样做的原因是,如果关系的父对象也是一个具有自动增量 PK 的新实例,SQLAlchemy 需要从数据库中获取 PK,然后才能更新子对象上的 FK(但我支持予以更正!)。

根据docs , 一个 merge():

examines the primary key of the instance. If it’s present, it attempts to locate that instance in the local identity map. If the load=True flag is left at its default, it also checks the database for this primary key if not located locally.

If the given instance has no primary key, or if no instance can be found with the primary key given, a new instance is created.

由于您在 flushing 之前合并,您的 p2 实例上的 PK 数据不完整,所以这一行 p2 = sess3 .merge(p2) 返回一个新的 Port 实例,其属性值与您之前创建的 p2 相同,由 session< 跟踪。然后,sess3.commit() 最终发出将 FK 数据填充到 p2 的刷新,然后在尝试写入 时引发完整性错误端口 表。虽然,插入 sess3.flush() 只会更早地引发完整性错误,而不是避免它。

像这样的东西会起作用:

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() 

This question有更详尽的 SQLAlchemy existing_or_new 样式函数示例。