assoc proxy / set relationship: `obj.foo = set(obj.foo)` results in duplicate rows

Issue #3583 duplicate
Adrian created an issue

I have an association_proxy on a set-like relationship. Normal set operations work perfectly when it comes to not adding duplicates, but when I replace the whole set e.g. using foo.bar = set(foo.bar) (in my real application the right side comes from a form) I get errors due to duplicate rows being inserted.

from sqlalchemy import *
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import *


Base = declarative_base()


class A(Base):
    __tablename__ = 'test_a'

    id = Column(Integer, primary_key=True)
    b_rel = relationship('B', collection_class=set, cascade='all, delete-orphan')
    b = association_proxy('b_rel', 'value', creator=lambda x: B(value=x))


class B(Base):
    __tablename__ = 'test_b'
    __table_args__ = UniqueConstraint('a_id', 'value'),

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey('test_a.id'), nullable=False)
    value = Column(String)


e = create_engine('sqlite:///:memory:', echo=True)
# e = create_engine('postgresql:///test', echo=True)
Base.metadata.create_all(e)
# e.execute('TRUNCATE test_a, test_b;')
s = Session(e)

a = A()
a.b = {'x', 'y', 'z'}
s.add(a)
s.commit()

print
print 'adding existing element to set'
a.b.add('x')
s.flush()

print
print 'assigning same items to set'
a.b = set(a.b)
s.flush()

Comments (5)

  1. Mike Bayer repo owner

    there's nothing unexpected here at all. the association proxy is only a proxy, it does not itself implement a set(), and on the collection side, B('x') and another B('x') are totally different. some entirely new feature would be needed to anticipate this use case. the use case for now can be achieved using event listeners on the collection that enforce uniqueness (e.g. like the uniqueobject recipe).

  2. Mike Bayer repo owner

    the add() method does a uniqueness check whereas the AssocProxy calling clear() prevents this here. Unfortunately the "proxy_bulk_set" API makes it difficult to change this becasue we need to add a replace() method to proxy classes.

  3. Log in to comment