The association proxy currently covers the "creator" use case, that is, a new
Item is given, you pass it a "creator" that says, "create a new OrderItem()
object and assign to OrderItem.item", but there is no support at the moment for
a "deleter" which says, "take the OrderItem and set OrderItem.item to None".
the delete use case is not well covered with assocation proxy, I can hardcode a
proof of concept for this case like this:
iff --git a/lib/sqlalchemy/ext/associationproxy.py
b/lib/sqlalchemy/ext/associationproxy.py
index 2f8d8627c..9e1f2a68d 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -1102,7 +1102,9 @@ class _AssociationList(_AssociationCollection):
def remove(self, value):
for i, val in enumerate(self):
if val == value:
+ holder = self.col[i]
del self.col[i]
+ self.setter(holder, None)
return
raise ValueError("value not in list")
but that would need to have a lot of tests written and changelogs and stuff and
also it might want to be a configurable option since I'm not sure under what
cases such a thing might not be what's desired.
so if you wanted to open an issue and all of that there could be a place to
track thinking of getting "assocation proxy" to be comprehensive for
delete/remove operations but for the moment just doing the below will fix for
now:
def remove_item(order, item):
for i, val in enumerate(order.order_items):
if val.item is item:
val.item = None
del order.order_items[i]
return
remove_item(order, item)
print_state(*ids, session)
On Tue, Dec 22, 2020, at 11:59 AM, S wrote:
> I'm trying to set up a bidirectional association proxy, but am encountering
> some unexpected behaviour with adding to the association list vs. deleting.
> Adds/updates are propagated to both association lists immediately, but
> deleting an object from one side's association list ends up with the other
> side's list containing 'None' in its place until the object is expired.
>
> Is this the intended behaviour, or am I setting something up incorrectly? I
> can't find anything in the docs to explain this, but I would love to know why
> this is happening. And ideally, I'd like to avoid having to remember to
> manually expire the other object every time.
>
> Example (adapted from existing docs example):
>
> class *Order*(Base):
> __tablename__ = 'order'
> order_id = Column(Integer, primary_key=True)
>
> order_items = relationship('OrderItem', cascade='all, delete-orphan',
> back_populates='order')
> items = association_proxy('order_items', 'item', creator=lambda x:
> OrderItem(item=x))
>
> def __repr__(self):
> return f'Order({self.order_id})'
>
>
> class *Item*(Base):
> __tablename__ = 'item'
> item_id = Column(Integer, primary_key=True)
>
> order_items = relationship('OrderItem', cascade='all, delete-orphan',
> back_populates='item')
> orders = association_proxy('order_items', 'order', creator=lambda x:
> OrderItem(order=x))
>
> def __repr__(self):
> return f'Item({self.item_id})'
>
>
> class *OrderItem*(Base):
> __tablename__ = 'orderitem'
> id = Column(Integer, primary_key=True)
>
> order_id = Column(Integer, ForeignKey('order.order_id',
> ondelete='CASCADE'), nullable=False)
> item_id = Column(Integer, ForeignKey('item.item_id', ondelete='CASCADE'),
> nullable=False)
>
> item = relationship(Item, back_populates='order_items')
> order = relationship(Order, back_populates='order_items')
>
> def __repr__(self):
> return f'OrderItem({self.id}, item={self.item}, order={self.order})'
>
>
> def *print_state*(order_id, item_id, session):
> order = session.query(Order).filter(Order.order_id == order_id).one()
> item = session.query(Item).filter(Item.item_id == item_id).one()
> order_items = session.query(OrderItem).all()
> print(f' Order.items: {order.items}\n Item.orders: {item.orders}\n
> OrderItems: {order_items}\n-----')
>
>
> if __name__ == '__main__':
> engine = create_engine('sqlite://')
> Base.metadata.create_all(engine)
> session = Session(engine)
>
> item = Item()
> order = Order()
> session.add_all([item, order])
> session.commit()
>
> ids = order.order_id, item.item_id
>
> print('order.items.append(item)')
> order.items.append(item)
> print_state(*ids, session)
>
> print('order.items.remove(item)')
> order.items.remove(item)
> print_state(*ids, session)
>
> print('session.expire(item)')
> session.expire(item)
> print_state(*ids, session)
>
>
> *Output:*
> order.items.append(item)
> Order.items: [Item(1)]
> Item.orders: [Order(1)]
> OrderItems: [OrderItem(1, item=Item(1), order=Order(1))]
> ----
> order.items.remove(item)
> Order.items: []
> *Item.orders: [None]*
> OrderItems: []
> ----
> session.expire(item)
> Order.items: []
> Item.orders: []
> OrderItems: []
> ----
>
> --
> SQLAlchemy -
> The Python SQL Toolkit and Object Relational Mapper
>
> http://www.sqlalchemy.org/
>
> To post example code, please provide an MCVE: Minimal, Complete, and
> Verifiable Example. See http://stackoverflow.com/help/mcve for a full
> description.
> ---
> You received this message because you are subscribed to the Google Groups
> "sqlalchemy" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to [email protected].
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/sqlalchemy/cbb6763f-7442-40d8-91cc-6029d7ae1a54n%40googlegroups.com
>
> <https://groups.google.com/d/msgid/sqlalchemy/cbb6763f-7442-40d8-91cc-6029d7ae1a54n%40googlegroups.com?utm_medium=email&utm_source=footer>.
>
>
> *Attachments:*
> * association_proxy_example.py
--
SQLAlchemy -
The Python SQL Toolkit and Object Relational Mapper
http://www.sqlalchemy.org/
To post example code, please provide an MCVE: Minimal, Complete, and Verifiable
Example. See http://stackoverflow.com/help/mcve for a full description.
---
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/sqlalchemy/95be88e4-8ecf-46ad-b0ac-badee6286766%40www.fastmail.com.