Как сделать, чтобы SQLAlchemy устанавливал значения для внешнего ключа, передавая связанную сущность в конструктор? - PullRequest
0 голосов
/ 20 октября 2018

При использовании 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()

Как я могу заставить этот последний блок работать?

1 Ответ

0 голосов
/ 21 октября 2018

FK дочернего объекта не обновляется, пока вы не введете flush() ни явно, ни через commit().Я думаю, что причина этого заключается в том, что если родительский объект отношения также является новым экземпляром с автоматическим приращением PK, SQLAlchemy необходимо получить PK из базы данных, прежде чем он сможет обновить FK на дочернем объекте (но я стоюбыть исправленным!).

Согласно документам , merge():

проверяет первичный ключ экземпляра.Если он присутствует, он пытается найти этот экземпляр в локальной карте идентификации.Если флаг load = True оставлен по умолчанию, он также проверяет базу данных на наличие этого первичного ключа, если он не находится локально.

Если у данного экземпляра нет первичного ключа или если экземпляр не может быть найден с помощьюПо первичному ключу создается новый экземпляр.

Поскольку вы merging до flushing, в вашем экземпляре 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()

Этот вопрос имеет большеподробные примеры функций стиля existing_or_new для SQLAlchemy.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...