SQLAlchemy的“ post_update”与从会话中删除的对象的行为有所不同

时间:2018-09-21 17:41:23

标签: python mysql sqlalchemy

我正在尝试将行从一个数据库实例复制到另一个数据库,该数据库在不同环境中具有相同的架构。此架构中的两个表以这样的方式链接:它们导致相互依赖的行。插入这些行后,post_update将按预期顺序运行,但是update语句会将ID字段的值设置为None而不是预期的ID。

当使用从会话中删除的对象时,仅发生 。使用新创建的对象时,post_update的行为完全符合预期。

示例

我建立了一个看起来像这样的关系:

class Category(Base):
    __tablename__ = 'categories'
    id = Column(Integer, primary_key=True)
    top_product_id = Column(Integer, ForeignKey('products.id'))
    products = relationship('Product', primaryjoin='Product.category_id == Category.id', back_populates='category', cascade='all', lazy='selectin')
    top_product = relationship('Product', primaryjoin='Category.top_product_id == Product.id', post_update=True, cascade='all', lazy='selectin')


class Product(Base):
    __tablename__ = 'products'
    id = Column(Integer, primary_key=True)
    category_id = Column(Integer, ForeignKey('categories.id'))
    category = relationship('Category', primaryjoin='Product.category_id == Category.id', back_populates='products', cascade='all', lazy='selectin')

如果我从一个数据库查询类别及其相关产品,然后尝试将其写入另一个数据库,则top_product_id的更新将无法正常工作,而是将其值设置为None。以下代码:

category = source_session.query(Category).filter(Category.id == 99).one()
source_session.expunge(category)
make_transient(category)
for products in category.products:
    make_transient(product)
# this step is necessary to prevent a foreign key error on the initial category insert
category.top_product_id = None
dest_session.add(category)

SQLAlchemy的结果将生成以下SQL:

INSERT INTO categories (name, top_product_id) VALUES (%s, %s)
('SomeCategoryName', None)
INSERT INTO products (name, category_id) VALUES (%s, %s)
('SomeProductName', 99)
UPDATE categories SET top_product_id=%s WHERE categories.id = %s
(None, 99)

但是,如果我使用新创建的对象,一切都会按预期进行。

category = Category()
product = Product()
category.name = 'SomeCategoryName'
product.name = 'SomeProductName'
product.category = category
category.top_product = product
dest_session.add(category)

导致:

INSERT INTO categories (name, top_product_id) VALUES (%s, %s)
('SomeCategoryName', None)
INSERT INTO products (name, category_id) VALUES (%s, %s)
('SomeProductName', 99)
UPDATE categories SET top_product_id=%s WHERE categories.id = %s
(1, 99)

除了这种区别外,这两个动作之间的所有行为都相同。正确创建了所有其他关系,并按预期设置了ID和外键。只有在top_product_id创建的update子句中设置的post_update无法正常工作。

作为其他故障排除步骤,我尝试了:

  1. 创建新对象
  2. 将它们添加到会话中
  3. 刷新与数据库的会话
  4. 清除会话中的对象
  5. 取消设置对象上的外键ID字段(以避免初始插入错误)并使对象处于瞬态状态
  6. 将对象重新添加到会话中
  7. 重新刷新到数据库

在第一次刷新到数据库时,top_product_id的设置正确。第二个参数设置为None。因此,这证实了问题不在于会话之间的差异,而是与从会话中删除对象并使它们成为临时对象有关。在删除/制作瞬态过程中,一定有/没有/没有发生某些事情,这些事情使这些对象处于根本不同的状态,并阻止post_update采取应有的方式。

任何关于从这里出发的想法都会受到赞赏。

1 个答案:

答案 0 :(得分:1)

我假设您的Base类在name列中混在一起?

您的目标是使inspect(category).committed_state看起来像新创建的对象一样(也许id属性除外)。每个产品对象都相同。

在您的“新建对象”示例中,category的{​​{1}}在刷新会话之前如下所示:

committed_state

{'id': symbol('NEVER_SET'), 'name': symbol('NO_VALUE'), 'products': [], 'top_product': symbol('NEVER_SET')} 的{​​{1}}看起来像这样:

product

要获取更新后的行为,您既需要使committed_state过期(以防止其被包含在{'category': symbol('NEVER_SET'), 'id': symbol('NEVER_SET'), 'name': symbol('NO_VALUE')} 中),又要捏造category.top_product_id的{​​{1}} (使SQLAlchemy相信该值已更改,因此需要引起INSERT。)

首先,在category.top_product过渡之前使committed_state过期:

UPDATE

然后捏造category.top_product_id的{​​{1}}(这可以在使category瞬变之前或之后发生):

source_session.expire(category, ["top_product_id"])

完整示例:

category.top_product

哪个在committed_state中生成此DML:

category

似乎from sqlalchemy import inspect from sqlalchemy.orm.base import NEVER_SET inspect(category).committed_state.update(top_product=NEVER_SET) 应该将from sqlalchemy import Column, ForeignKey, Integer, String, create_engine, inspect from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Session, make_transient, relationship from sqlalchemy.orm.base import NEVER_SET class Base(object): name = Column(String(50), nullable=False) Base = declarative_base(cls=Base) class Category(Base): __tablename__ = 'categories' id = Column(Integer, primary_key=True) top_product_id = Column(Integer, ForeignKey('products.id')) products = relationship('Product', primaryjoin='Product.category_id == Category.id', back_populates='category', cascade='all', lazy='selectin') top_product = relationship('Product', primaryjoin='Category.top_product_id == Product.id', post_update=True, cascade='all', lazy='selectin') class Product(Base): __tablename__ = 'products' id = Column(Integer, primary_key=True) category_id = Column(Integer, ForeignKey('categories.id'), nullable=False) category = relationship('Category', primaryjoin='Product.category_id == Category.id', back_populates='products', cascade='all', lazy='selectin') source_engine = create_engine('sqlite:///') dest_engine = create_engine('sqlite:///', echo=True) def fk_pragma_on_connect(dbapi_con, con_record): dbapi_con.execute('pragma foreign_keys=ON') from sqlalchemy import event for engine in [source_engine, dest_engine]: event.listen(engine, 'connect', fk_pragma_on_connect) Base.metadata.create_all(bind=source_engine) Base.metadata.create_all(bind=dest_engine) source_session = Session(bind=source_engine) dest_session = Session(bind=dest_engine) source_category = Category(id=99, name='SomeCategoryName') source_product = Product(category=source_category, id=100, name='SomeProductName') source_category.top_product = source_product source_session.add(source_category) source_session.commit() source_session.close() # If you want to test UPSERTs in dest_session. # dest_category = Category(id=99, name='PrevCategoryName') # dest_product = Product(category=dest_category, id=100, name='PrevProductName') # dest_category.top_product = dest_product # dest_session.add(dest_category) # dest_session.commit() # dest_session.close() category = source_session.query(Category).filter(Category.id == 99).one() # Ensure relationship attributes are initialized before we make objects transient. _ = category.top_product # source_session.expire(category, ['id']) # only if you want new IDs in dest_session source_session.expire(category, ['top_product_id']) for product in category.products: # Ensure relationship attributes are initialized before we make objects transient. _ = product.category # source_session.expire(product, ['id']) # only if you want new IDs in dest_session # Not strictly needed as long as Product.category is not a post-update relationship. source_session.expire(product, ['category_id']) make_transient(category) inspect(category).committed_state.update(top_product=NEVER_SET) for product in category.products: make_transient(product) # Not strictly needed as long as Product.category is not a post-update relationship. inspect(product).committed_state.update(category=NEVER_SET) dest_session.add(category) # Or, if you want UPSERT (must retain original IDs in this case) # dest_session.merge(category) dest_session.flush() 重置为新对象,但我想不是。