Skip to main content
 首页 » 编程设计

python之在 SQLAlchemy 中通过 association_proxy 分配一组新的多对多相关标签的最优雅的方法是什么

2025年05月04日9zlslch

这是我的 Flask-SQLAlchemy 声明代码:

from sqlalchemy.ext.associationproxy import association_proxy 
from my_flask_project import db 
 
 
tagging = db.Table('tagging', 
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id', ondelete='cascade'), 
              primary_key=True), 
    db.Column('role_id', db.Integer, db.ForeignKey('role.id', ondelete='cascade'), 
              primary_key=True) 
) 
 
 
class Tag(db.Model): 
    id = db.Column(db.Integer, primary_key=True) 
    name = db.Column(db.String(100), unique=True, nullable=False) 
 
    def __init__(self, name=None): 
        self.name = name 
 
 
class Role(db.Model): 
 
    id = db.Column(db.Integer, primary_key=True) 
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='cascade')) 
    user = db.relationship('User', backref=db.backref('roles', cascade='all', 
                           lazy='dynamic')) 
    ... 
    tags = db.relationship('Tag', secondary=tagging, cascade='all', 
                           backref=db.backref('roles', cascade='all')) 
    tag_names = association_proxy('tags', 'name') 
 
    __table_args__ = ( 
        db.UniqueConstraint('user_id', 'check_id'), 
    ) 

我认为这是非常标准的多对多标记解决方案。现在,我想获取一个角色的所有标签并为一个角色设置一组新标签。

第一个很简单:

print role.tags 
print role.tag_names 

然而,第二个让我整天都在 Python 代码上磕磕绊绊 :-( 我以为我可以这样做:

role.tag_names[:] = ['red', 'blue', 'white'] 

...或者至少使用 role.tags[:] = ... 类似的东西,但我发明的一切都引发了许多完整性错误,因为 SQLAlchemy 没有检查是否存在任何现有的标签并尝试将它们全部作为全新实体插入。

我最终的解决方案是:

# cleanup input 
tag_names = set(filter(None, tag_names)) 
 
# existings tags to be updated 
to_update = [t for t in role.tags if t.name in tag_names] 
 
# existing tags to be added 
to_add = list( 
    Tag.query.filter(Tag.name.in_(tag_names - set(role.tag_names))) 
) 
 
# tags to be created 
existing_tags = to_update + to_add 
to_create = [Tag(name) for name in tag_names - set([t.name for t in existing_tags])] 
 
# assign new tags 
role.tags[:] = existing_tags + to_create 
 
# omitted bonus: find a way how to get rid of orphan tags 

问题是:这真的是正确的解决方案吗?有没有更优雅的方法来解决这个微不足道的问题?我认为整个事情与this question有关.也许我只是太傻了,也许我把事情弄得太复杂了……无论如何,谢谢你的任何建议!

请您参考如下方法:

实际上,SQLAlchemy 通过调用 Session.merge() 检查对象是否存在。但它是通过身份来实现的——它的主键。最简单的解决方案是将 name 设为主键,一切都会正常进行。当然,在这种情况下,三个表链将变得多余,除非您要向 Tag 添加一些额外的字段(例如计数器)。