При использовании SQLAlchemy хотелось бы, чтобы поля внешнего ключа заполнялись в объекте Python, когда я передаю связанный объект.Например, предположим, что у вас есть сетевые устройства с портами, и предположим, что устройство имеет составной первичный ключ в базе данных.
Если у меня уже есть ссылка на экземпляр «Device» и я хочу создать новый »Порт "экземпляр, связанный с этим устройством, не зная, существует ли он уже в базе данных, я бы использовал операцию merge
в SA.Однако только установка атрибута device
для экземпляра port
недостаточна.Поля составного внешнего ключа не будут распространяться на экземпляр 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
ссылается на Device
через составной FK в этих трех столбцах.Device
также имеет отношение к Port
, который будет использовать этот FK.
Использование модели
Сначала добавим новое устройство и порт.Поскольку мы используем БД 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» создается с помощью «hostname», «scope» и «poll_ip», и этот экземпляр передается в «Port» экземпляр «p2».Я ожидаю, что «p2» будет «получать» эти 3 значения через внешний ключ.Но это не так.Я вынужден вручную присвоить значения «p2» перед вызовом «слияния».Если значения не назначены, SA не находит идентификатор и пытается выполнить запрос «INSERT» для «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()
Сломанный пример (но ожидающий его)на работу)
Этот блок показывает, как я ожидаю, что он будет работать.Я ожидаю, что назначения значения «устройство» при создании экземпляра порта должно быть достаточно.
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()
Как я могу заставить этот последний блок работать?