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

时间:2018-10-20 07:58:29

标签: python sqlalchemy

使用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引用PortDevice也与将使用该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()

如何使最后一个块正常工作?

1 个答案:

答案 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() 样式函数的更详尽的示例。