Reputation: 689
Same issue report in SQLAlchemy Attempting to Twice Delete Many to Many Secondary Relationship.
But accepted answer is not working.
Model structure is
class Product(Base):
""" The SQLAlchemy declarative model class for a Product object. """
__tablename__ = 'products'
id = Column(Integer, primary_key=True)
part_number = Column(String(10), nullable=False, unique=True)
name = Column(String(80), nullable=False, unique=True)
description = Column(String(2000), nullable=False)
categories = relationship('Category', secondary=product_categories,
backref=backref('categories', lazy='dynamic'))
class Category(Base):
""" The SQLAlchemy declarative model class for a Category object. """
__tablename__ = 'categories'
id = Column(Integer, primary_key=True)
lft = Column(Integer, nullable=False)
rgt = Column(Integer, nullable=False)
name = Column(String(80), nullable=False)
description = Column(String(2000), nullable=False)
order = Column(Integer)
products = relationship('Product', secondary=product_categories,
backref=backref('products', lazy='dynamic', order_by=name))
product_categories = Table('product_categories', Base.metadata,
Column('products_id', Integer, ForeignKey('products.id')),
Column('categories_id', Integer, ForeignKey('categories.id'))
)
Issue is when try to delete Product instance, SQLAlchemy returns an error
Traceback (most recent call last):
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/pyramid_debugtoolbar-2.3-py2.7.egg/pyramid_debugtoolbar/toolbar.py", line 178, in toolbar_tween
response = _handler(request)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/pyramid_debugtoolbar-2.3-py2.7.egg/pyramid_debugtoolbar/panels/performance.py", line 57, in resource_timer_handler
result = handler(request)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/pyramid/tweens.py", line 21, in excview_tween
response = handler(request)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/pyramid_tm-0.10-py2.7.egg/pyramid_tm/__init__.py", line 95, in tm_tween
reraise(*exc_info)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/pyramid_tm-0.10-py2.7.egg/pyramid_tm/__init__.py", line 83, in tm_tween
manager.commit()
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/transaction-1.4.3-py2.7.egg/transaction/_manager.py", line 111, in commit
return self.get().commit()
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/transaction-1.4.3-py2.7.egg/transaction/_transaction.py", line 280, in commit
reraise(t, v, tb)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/transaction-1.4.3-py2.7.egg/transaction/_transaction.py", line 271, in commit
self._commitResources()
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/transaction-1.4.3-py2.7.egg/transaction/_transaction.py", line 417, in _commitResources
reraise(t, v, tb)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/transaction-1.4.3-py2.7.egg/transaction/_transaction.py", line 389, in _commitResources
rm.tpc_begin(self)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/zope.sqlalchemy-0.7.5-py2.7.egg/zope/sqlalchemy/datamanager.py", line 90, in tpc_begin
self.session.flush()
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/SQLAlchemy-0.9.8-py2.7-macosx-10.6-intel.egg/sqlalchemy/orm/session.py", line 1919, in flush
self._flush(objects)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/SQLAlchemy-0.9.8-py2.7-macosx-10.6-intel.egg/sqlalchemy/orm/session.py", line 2037, in _flush
transaction.rollback(_capture_exception=True)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/SQLAlchemy-0.9.8-py2.7-macosx-10.6-intel.egg/sqlalchemy/util/langhelpers.py", line 60, in __exit__
compat.reraise(exc_type, exc_value, exc_tb)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/SQLAlchemy-0.9.8-py2.7-macosx-10.6-intel.egg/sqlalchemy/orm/session.py", line 2001, in _flush
flush_context.execute()
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/SQLAlchemy-0.9.8-py2.7-macosx-10.6-intel.egg/sqlalchemy/orm/unitofwork.py", line 372, in execute
rec.execute(self)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/SQLAlchemy-0.9.8-py2.7-macosx-10.6-intel.egg/sqlalchemy/orm/unitofwork.py", line 479, in execute
self.dependency_processor.process_deletes(uow, states)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/SQLAlchemy-0.9.8-py2.7-macosx-10.6-intel.egg/sqlalchemy/orm/dependency.py", line 1023, in process_deletes
secondary_update, secondary_delete)
File "/Users/derek/pyramid/myproject/lib/python2.7/site-packages/SQLAlchemy-0.9.8-py2.7-macosx-10.6-intel.egg/sqlalchemy/orm/dependency.py", line 1111, in _run_crud
result.rowcount)
StaleDataError: DELETE statement on table 'product_categories' expected to delete 1 row(s); Only 0 were matched.
Database is Postgres and When I remove the backref from one model, no issue but I need to use backref on both models.
Setting supports_sane_rowcount not working for postgres
engine.dialect.supports_sane_rowcount = engine.dialect.supports_sane_multi_rowcount = False
Is there any workable solution for this?
Upvotes: 1
Views: 193
Reputation: 123399
When I use your classes I get warnings like
SAWarning: relationship 'Product.products' will copy column products.id to column product_categories.products_id, which conflicts with relationship(s): 'Category.categories' (copies products.id to product_categories.products_id), 'Product.categories' (copies products.id to product_categories.products_id). If this is not the intention, consider if these relationships should be linked with back_populates, or if viewonly=True should be applied to one or more if they are read-only. For the less common case that foreign key constraints are partially overlapping, the orm.foreign() annotation can be used to isolate the columns that should be written towards. To silence this warning, add the parameter 'overlaps="categories,categories"' to the 'Product.products' relationship. (Background on this error at: https://sqlalche.me/e/14/qzyx)
The issue goes away if I use back_populates
instead of backref
:
class Product(Base):
# …
categories = relationship(
"Category",
secondary=product_categories,
back_populates="products",
lazy="dynamic",
order_by = "Category.name",
)
class Category(Base):
# …
products = relationship(
"Product",
secondary=product_categories,
back_populates="categories",
lazy="dynamic",
order_by="Product.name",
)
Complete working example:
from sqlalchemy import (
create_engine,
Column,
Integer,
String,
Table,
ForeignKey,
select,
)
from sqlalchemy.orm import declarative_base, relationship, Session
engine = create_engine(
"postgresql://scott:[email protected]/test",
)
Base = declarative_base()
product_categories = Table(
"product_categories",
Base.metadata,
Column("products_id", Integer, ForeignKey("products.id")),
Column("categories_id", Integer, ForeignKey("categories.id")),
)
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True)
part_number = Column(String(10), nullable=False, unique=True)
name = Column(String(80), nullable=False, unique=True)
description = Column(String(2000), nullable=False)
categories = relationship(
"Category",
secondary=product_categories,
back_populates="products",
lazy="dynamic",
order_by = "Category.name",
)
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True)
lft = Column(Integer, nullable=False)
rgt = Column(Integer, nullable=False)
name = Column(String(80), nullable=False)
description = Column(String(2000), nullable=False)
order = Column(Integer)
products = relationship(
"Product",
secondary=product_categories,
back_populates="categories",
lazy="dynamic",
order_by="Product.name",
)
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
# setup
with Session(engine) as sess:
category_a = Category(
lft=0, rgt=0, name="category_a", description="category_a"
)
product_a = Product(
part_number="product_a",
name="product_a",
description="product_a",
categories=[category_a],
)
sess.add_all([category_a, product_a])
sess.commit()
# test
engine.echo = True
with Session(engine) as sess:
p = sess.execute(
select(Product).where(Product.part_number == "product_a")
).scalar()
sess.delete(p)
sess.commit()
"""SQL emitted:
DELETE FROM product_categories WHERE product_categories.products_id = %(products_id)s AND product_categories.categories_id = %(categories_id)s
[generated in 0.00074s] {'products_id': 1, 'categories_id': 1}
DELETE FROM products WHERE products.id = %(id)s
[generated in 0.00069s] {'id': 1}
COMMIT
"""
Upvotes: 1