簡體   English   中英

如何通過在構造函數中傳遞相關實體來為外鍵設置值?

[英]How do I make SQLAlchemy set values for a foreign key by passing a related entity in the constructor?

使用 SQLAlchemy 時,我希望在傳入相關對象時在 Python 對象上填充外鍵字段。 例如,假設您有帶有端口的網絡設備,並假設該設備在數據庫中有一個復合主鍵。

如果我已經有了對“設備”實例的引用,並且想要創建一個鏈接到該設備的新“端口”實例而不知道它是否已經存在於數據庫中,我將在 SA 中使用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 引用Device Device還與將使用該 FK 的Port有關系。

使用模型

首先,我們添加一個新設備和端口。 由於我們使用的是內存中的 SQLite 數據庫,因此這將是數據庫中僅有的兩個條目。 通過將一個設備插入數據庫,我們在設備表中有一些我們希望在會話“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()

工作示例

這個塊可以工作,但它不是按照我期望的方式編寫的。 更准確地說,實例“d1”用“主機名”、“范圍”和“poll_ip”實例化,並且該實例被傳遞給“端口”實例“p2”。 我希望“p2”會通過外鍵“接收”這 3 個值。 但事實並非如此。 在調用“合並”之前,我被迫手動將值分配給“p2”。 如果未分配值,SA 將找不到身份並嘗試對“p2”運行“INSERT”查詢,這將與現有實例沖突。

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

破碎的例子(但希望它能工作)

這個塊顯示了我希望它如何工作。 我希望在創建 Port 實例時為“設備”分配一個值就足夠了。

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

我怎樣才能使最后一個塊工作?

在您顯式或通過commit()發出flush()之前,子對象的 FK 不會更新。 我認為這樣做的原因是,如果關系的父對象也是一個帶有自增 PK 的新實例,SQLAlchemy 需要從數據庫中獲取 PK,然后才能更新子對象上的 FK(但我支持待糾正!)。

根據文檔,一個merge()

檢查實例的主鍵。 如果它存在,它會嘗試在本地身份映射中定位該實例。 如果 load=True 標志保留為默認值,它還會檢查數據庫以查找此主鍵(如果不在本地)。

如果給定的實例沒有主鍵,或者如果沒有找到具有給定主鍵的實例,則會創建一個新實例。

當您在flushing之前merging ,您的p2實例上存在不完整的 PK 數據,因此這一行p2 = sess3.merge(p2)返回一個新的Port實例,其屬性值與您之前創建的p2屬性值相同,由session 然后, sess3.commit()最后發出刷新,將 FK 數據填充到p2 ,然后在嘗試寫入port表時引發完整性錯誤。 雖然,插入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()

這個問題有更全面的 SQLAlchemy 的existing_or_new風格函數的例子。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM