[英]How do I set a foreign key in the constructor of a SQLAlchemy (Python, Flask) db model?
[英]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.