使用 SQLAlchemy 关联代理强制唯一性

Posted

技术标签:

【中文标题】使用 SQLAlchemy 关联代理强制唯一性【英文标题】:Enforcing uniqueness using SQLAlchemy association proxies 【发布时间】:2015-06-10 00:21:07 【问题描述】:

我正在尝试使用关联代理来更简单地处理标签式记录,但我遇到了一个问题,即强制唯一性和让对象重用现有标签而不是总是创建新标签。

这是一个类似于我的设置。文档中的示例有一些 enforcing uniqueness 的配方,但它们都依赖于对会话的访问,并且通常需要一个全局会话,而我无法做到这一点。

from sqlalchemy import Column, Integer, String, create_engine, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy

Base = declarative_base()
engine = create_engine('sqlite://', echo=True)
Session = sessionmaker(bind=engine)


def _tag_find_or_create(name):
    # can't use global objects here, may be multiple sessions and engines
    # ?? No access to session here, how to do a query
    tag = session.query(Tag).filter_by(name=name).first()
    tag = Tag.query.filter_by(name=name).first()
    if not tag:
        tag = Tag(name=name)
    return tag


class Item(Base)
    __tablename__ = 'item'

    id = Column(Integer, primary_key=True)
    tags = relationship('Tag', secondary='itemtag')
    tagnames = association_proxy('tags', 'name', creator=_tag_find_or_create)


class ItemTag(Base)
    __tablename__ = 'itemtag'

    id = Column(Integer, primary_key=True)
    item_id = Column(Integer, ForeignKey('item.id'))
    tag_id = Column(Integer, ForeignKey('tag.id'))


class Tag(Base)
    __tablename__ = 'tag'

    id = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False) 


# Scenario 1
session = Session()
item = Item()
session.add(item)
item.tagnames.append('red')

# Scenario 2
item2 = Item()
item2.tagnames.append('blue')
item2.tagnames.append('red')
session.add(item2)

如果没有 creator 功能,我只会得到大量重复的 Tag 项。 creator 函数似乎是进行此类检查的最明显位置,但我不确定如何从 creator 函数内部进行查询。

考虑示例底部提供的两种方案。在第一个示例中,似乎应该有一种方法可以在 creator 函数中访问会话,因为要添加标签的对象已经与会话相关联。

在第二个示例中,Item 对象尚未与会话关联,因此验证检查无法在 creator 函数中进行。当对象实际添加到会话中时,它必须稍后发生。

对于第一种情况,我将如何访问 creator 函数中的会话对象?

对于第二种情况,有没有办法“监听” 何时将父对象添加到会话并在那时验证关联代理?

【问题讨论】:

【参考方案1】:

对于第一种情况,您可以使用object_session

至于整体问题:是的,您需要访问当前会话;如果在您的应用程序中使用 scoped_session 是合适的,那么您链接到的食谱的第二部分应该可以正常使用。请参阅Contextual/Thread-local Sessions 了解更多信息。

在事件从 transient 变为 persistent 状态时处理事件和更改对象不会使您的代码漂亮或非常健壮。所以我会立即将新的Tag 对象添加到会话中,如果事务回滚,它们就不会在数据库中。

请注意,在多用户环境中,您可能会遇到竞争条件:同一个标签是新标签,并且由两个用户同时创建。最后提交的用户将失败(如果您对数据库有唯一约束)。 在这种情况下,您可能会考虑没有唯一约束,并有一个(每日)程序来清理这些重复项(并重新分配关系)。随着时间的推移,新物品会越来越少,发生这种冲突的可能性也会越来越小。

【讨论】:

如何访问 creator 函数中的 Item 对象以使用object_session?有没有办法告诉 Association_proxy 将父对象传递给创建者函数? 例如,您可以在Item: def add_tag(self, tag_name): object_session(self).query(...) ... 上创建一个辅助方法,在这种情况下您可以跳过关联代理。使用关联代理,您可以创建自己的子类和 intercept 修改操作。但实际上,我认为UniqueObject recipe 应该适合上下文绑定会话。

以上是关于使用 SQLAlchemy 关联代理强制唯一性的主要内容,如果未能解决你的问题,请参考以下文章

SQLAlchemy:创建多对多并填充关联

按关联代理排序:无效的 sql

5 . 4 . 6 强制数据完整性

SQLAlchemy 关联表(关联对象模式)引发 IntegrityError

SQLAlchemy - 不要对关系强制外键约束

如何强制 Flask/SQLAlchemy 创建表?