为什么不在关系中重复违反UniqueConstraint?

时间:2018-05-14 17:01:06

标签: python sqlalchemy

使用以下模型,为什么以下交互成功在同一事务期间向关系添加重复关联?我期望(并且需要)它失败,并在关联表上放置了UniqueConstraint。

型号:

from app import db # this is the access to SQLAlchemy
class User(db.Model):

    id = db.Column(db.Integer, primary_key=True)

    sz_shirt_dress_sleeve = db.relationship(
        'SizeKeyShirtDressSleeve',
        secondary=LinkUserSizeShirtDressSleeve,
        backref=db.backref('users', lazy='dynamic'),
        order_by="asc(SizeKeyShirtDressSleeve.id)")

class SizeKeyShirtDressSleeve(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    size = db.Column(db.Integer)

    def __repr__(self):
        return 'Dress shirt sleeve size: %r' % self.size

LinkUserSizeShirtDressSleeve = db.Table(
    'link_user_size_shirt_dress_sleeve',
    db.Column(
        'size_id',
        db.Integer,
        db.ForeignKey('size_key_shirt_dress_sleeve.id'), primary_key=True),
    db.Column(
        'user_id',
        db.Integer,
        db.ForeignKey('user.id'), primary_key=True),
    db.UniqueConstraint('size_id', 'user_id', name='uq_association')
)

由于关联表上的UniqueConstraint,我预计此交互式会话会导致IntegrityError。它没有,并允许我将相同的大小关联两次:

>>> from app.models import User, SizeKeyShirtDressSleeve
>>> db.session.add(User(id=8))
>>> db.session.commit()
>>> u = User.query.filter(User.id==8).one()
>>> u
<User id: 8, email: None, password_hash: None>
>>> u.sz_shirt_dress_sleeve
[]
>>> should_cause_error = SizeKeyShirtDressSleeve.query.first()
>>> should_cause_error
Dress shirt sleeve size: 3000
>>> u.sz_shirt_dress_sleeve.append(should_cause_error)
>>> u.sz_shirt_dress_sleeve.append(should_cause_error)
>>> u.sz_shirt_dress_sleeve
[Dress shirt sleeve size: 3000, Dress shirt sleeve size: 3000]
>>> db.session.commit()
>>> 
等等,什么?这种关系不代表我的关联表中的内容吗?我想我应该验证:

(紧接在同一会话之后)

>>> from app.models import LinkUserSizeShirtDressSleeve as Sleeve
>>> db.session.query(Sleeve).filter(Sleeve.c.user_id==8).all()
[(1, 8)]
>>>

因此u.sz_shirt_dress_sleeve 并非准确地表示关联表的状态。 ...好的。但我需要它。事实上,如果我尝试在关系中添加另一个should_cause_error对象,我知道它会失败:

>>> u.sz_shirt_dress_sleeve.append(should_cause_error)
>>> db.session.commit()
# huge stack trace
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: link_user_size_shirt_dress_sleeve.size_id, link_user_size_shirt_dress_sleeve.user_id [SQL: 'INSERT INTO link_user_size_shirt_dress_sleeve (size_id, user_id) VALUES (?, ?)'] [parameters: (1, 8)] (Background on this error at: http://sqlalche.me/e/gkpj)
>>> 

大!好的,所以我推断的事情: 1)关系列表中可能有重复的项目。 2)关系列表可能无法准确反映其负责的关联表的状态。 3)UniqueConstraint有效...只要我继续在单独的交易中与关系互动(由session.commit()打断)。

问题:1),2)或3)是否不正确?如何防止在同一交易中的关系列表中出现重复项?

1 个答案:

答案 0 :(得分:1)

Those three things are all correct. 3) should be qualified: the UniqueConstraint always works in the sense that your database will never be inconsistent; it just doesn't give you an error unless the relationship you're adding is already flushed.

The fundamental reason this happens is an impedance mismatch between an association table in SQL and its representation in SQLAlchemy. A table in SQL is a multiset of tuples, so with that UNIQUE constraint, your LinkUserSizeShirtDressSleeve table is a set of (size_id, user_id) tuples. On the other hand, the default representation of a relationship in SQLAlchemy an ordered list of objects, but it imposes some limitations on the way it maintains this list and the way it expects you to interact with this list, so it behaves more like a set in some ways. In particular, it silently ignores duplicate entries in your association table (if you happen to not have a UNIQUE constraint), and it assumes that you never add duplicate objects to this list in the first place.

If this is a problem for you, just make the behavior more in line with SQL by using collection_class=set on your relationship. If you want an error to be raised when you add duplicate entries into the relationship, create a custom collection class based on set that fails on duplicate adds. In some of my projects, I've resorted to monkey-patching the relationship constructor to set collection_class=set on all of my relationships to make this less verbose.

Here's how I would such a custom collection class:

class UniqueSet(set):
    def add(self, el):
        if el in self:
            raise ValueError("Value already exists")
        super().add(el)