sqlalchemy: запретить автоматическое добавление объектов отношений в сеанс - PullRequest
0 голосов
/ 01 июля 2019

Я пишу скрипт на python, который создает некоторые объекты SQLAlchemy, проверяет, какие из этих объектов уже были добавлены в базу данных, а затем добавляет любые новые объекты. Мой скрипт выглядит так:

from sqlalchemy import Column, String, Integer, ForeignKey, create_engine
from sqlalchemy.orm import relationship, Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

# Define models
class Person(Base):
    __tablename__ = "Person"
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    pets = relationship("Pet", backref="person")

    def __repr__(self):
        return f"<Person: {self.name}>"


class Pet(Base):
    __tablename__ = "Pet"
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    person_id = Column(Integer, ForeignKey("Person.id"))

    def __repr__(self):
        return f"<Pet: {self.name}>"

connection_string = "sqlite:///db.sqlite3"
engine = create_engine(connection_string)
session = Session(
    bind=engine, expire_on_commit=False, autoflush=False, autocommit=False
)

# Build tables
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)

# Create data
persons = [
    Person(name="Johnny"),
    Person(name="Steph"),
]

pets = [
    Pet(name="Packets", person=persons[0]),
    Pet(name="Sally", person=persons[1]),
    Pet(name="Shiloh", person=persons[0]),
]

# Populate tables with data
for items in [persons, pets]:
    for item in items:
        q = session.query(item.__class__).filter_by(name=item.name).one_or_none()
        if q:
            print(f"Already exists: {item}")
            continue
        session.add(item)
        session.commit()
        print(f"Added: {item}")

Когда я запускаю его, я получаю следующий результат:

Added: <Person: Johnny>
Added: <Person: Steph>
Already exists: <Pet: Packets>
Already exists: <Pet: Sally>
Already exists: <Pet: Shiloh>

Я ожидаю, что результат будет выглядеть так:

Added: <Person: Johnny>
Added: <Person: Steph>
Added: <Pet: Packets>
Added: <Pet: Sally>
Added: <Pet: Shiloh>

Что происходит при добавлении объектов Pet до их фактического добавления в сеанс? Как я могу предотвратить это, чтобы мой вывод соответствовал ожиданиям?

1 Ответ

2 голосов
/ 01 июля 2019

Что происходит, это добавляет Pet объекты, прежде чем они фактически добавлен в сеанс?

Вставка <Person: Johnny> неявно вставляет <Pet: Packets> и <Pet: Shiloh>; вставка <Person: Steph> неявно вставляет <Pet: Sally>.

Это потому, что backref создает двунаправленные отношения.
Как описано здесь в документах:

[...] когда ключевое слово backref используется для одного отношения, оно точно так же, как если бы [...] были созданы две связи индивидуально используя back_populates [...]

Вы создаете Pet экземпляров, которые относятся к Person экземплярам, ​​которые еще не существуют в базе данных. При использовании настроек каскадирования по умолчанию это приводит к неявным вставкам связанных объектов для представления обоих направлений взаимосвязи.

Это можно наблюдать, создав двигатель с echo, установленным на True:

engine = create_engine(connection_string, echo=True)

Включает базовую мощность двигателя:

# Time stamps and log level omitted for brevity
# First iteration of the loop (Johnny):
sqlalchemy.engine.base.Engine INSERT INTO "Person" (name) VALUES (?)
sqlalchemy.engine.base.Engine ('Johnny',)
sqlalchemy.engine.base.Engine INSERT INTO "Pet" (name, person_id) VALUES (?, ?)
sqlalchemy.engine.base.Engine ('Packets', 1)
sqlalchemy.engine.base.Engine INSERT INTO "Pet" (name, person_id) VALUES (?, ?)
sqlalchemy.engine.base.Engine ('Shiloh', 1)
# Second iteration of the loop (Steph):
sqlalchemy.engine.base.Engine INSERT INTO "Person" (name) VALUES (?)
sqlalchemy.engine.base.Engine ('Steph',)
sqlalchemy.engine.base.Engine INSERT INTO "Pet" (name, person_id) VALUES (?, ?)
sqlalchemy.engine.base.Engine ('Sally', 2)
# Third to fifth iteration: the Pets already exist.

Обратный путь аналогичен; если вы сначала укажете список питомцев, ваш вывод будет выглядеть так:

Added: <Pet: Packets>            # implicitly creates Person Johnny and, through Johnny, Pet Shiloh
Added: <Pet: Sally>              # implicitly creates Person Steph
Already exists: <Pet: Shiloh>    
Already exists: <Person: Johnny>
Already exists: <Person: Steph>

Как отметил Илья Эверила в комментариях, самый простой способ отключить неявную вставку Pets - удалить параметр save-update из отношения cascades:

pets = relationship("Pet", backref="person", cascade="merge")

Обратите внимание, что выдает предупреждение:

SAWarning: объект типа <Pet> не находится в сеансе, добавьте операцию вместе Person.pets не будет продолжено

Более подробный способ предотвратить неявное создание домашних животных посредством отношений - отложить их инстанцирование до тех пор, пока люди не будут вставлены, например ::101061

# Don't instantiate just yet
# pets = [
#     Pet(name="Packets", person=persons[0]),
#     Pet(name="Sally", person=persons[1]),
#     Pet(name="Shiloh", person=persons[0]),
# ]

pets = {persons[0]: ['Packets', 'Shiloh'],
        persons[1]: ['Sally']}

for item in persons:
    if session.query(item.__class__).filter_by(name=item.name).one_or_none():
        print(f"Already exists: {item}")
        continue
    session.add(item)
    session.commit()
    print(f"Added: {item}")
    for pet in pets[item]:
        p = Pet(name=pet, person=item)
        session.add(p)
        session.commit()
        print(f"Added: {p}")

Выход:

Added: <Person: Johnny>
Added: <Pet: Packets>
Added: <Pet: Shiloh>
Added: <Person: Steph>
Added: <Pet: Sally>

Однако, с поведением по умолчанию, вы можете фактически опустить явную вставку Pets. Итерация persons также вставит все экземпляры Pet; три ненужных запроса пропущены.

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