sqlalchemy:防止关系对象自动添加到会话中

Joh*_*etz 0 python sql sqlalchemy

我正在编写一个创建一些 SQLAlchemy 对象的 python 脚本,检查其中哪些对象已经添加到数据库中,然后添加任何新对象。我的脚本如下所示:

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}")
Run Code Online (Sandbox Code Playgroud)

当我运行它时,我得到以下结果:

Added: <Person: Johnny>
Added: <Person: Steph>
Already exists: <Pet: Packets>
Already exists: <Pet: Sally>
Already exists: <Pet: Shiloh>
Run Code Online (Sandbox Code Playgroud)

我希望结果如下所示:

Added: <Person: Johnny>
Added: <Person: Steph>
Added: <Pet: Packets>
Added: <Pet: Sally>
Added: <Pet: Shiloh>
Run Code Online (Sandbox Code Playgroud)

在将Pet对象实际添加到会话之前添加对象发生了什么?我怎样才能防止这种情况发生,使我的输出符合预期?

shm*_*mee 5

在将Pet对象实际添加到会话之前添加对象发生了什么?

插入<Person: Johnny>隐式插入<Pet: Packets><Pet: Shiloh>; 插入<Person: Steph>隐式插入<Pet: Sally>

那是因为backref创建了双向关系。
如上所述这里的文档:

[...] 当backref关键字用于单个关系时,这与 [...] 使用back_populates[...]分别创建两个关系完全相同

您创建PetPerson数据库中尚不存在的实例相关的实例。使用默认的级联设置,这会导致相关对象的隐式插入来表示关系的两个方向。

这可以通过echo设置为创建引擎来观察True

engine = create_engine(connection_string, echo=True)
Run Code Online (Sandbox Code Playgroud)

这将启用基本引擎输出:

# 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.
Run Code Online (Sandbox Code Playgroud)

反过来也是类似的;如果您首先指定宠物列表,您的输出如下所示:

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>
Run Code Online (Sandbox Code Playgroud)

正如 Ilja Everilä 在评论中指出的那样,禁用 Pets 隐式插入的最简单方法是save-update从关系的 中删除设置cascades

pets = relationship("Pet", backref="person", cascade="merge")
Run Code Online (Sandbox Code Playgroud)

请注意,这会发出警告:

SAWarning: 类型的对象<Pet>不在会话中,添加操作 Person.pets不会继续

防止通过关系隐式创建宠物的更详细的方法是将它们的实例化推迟到人被插入之后,例如:

# 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}")
Run Code Online (Sandbox Code Playgroud)

输出:

Added: <Person: Johnny>
Added: <Pet: Packets>
Added: <Pet: Shiloh>
Added: <Person: Steph>
Added: <Pet: Sally>
Run Code Online (Sandbox Code Playgroud)

但是,使用默认行为,您可以有效地省略 Pets 的显式插入。只是迭代persons,也会插入所有 Pet 实例;三个不必要的查询被跳过。