Каков самый элегантный способ назначить новый набор связанных тегов «многие ко многим» через association_proxy в SQLAlchemy? - PullRequest
2 голосов
/ 11 февраля 2012

Это мой декларативный код 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

Вопрос:это действительно правильное решение? Есть ли более элегантный способ, как решить эту тривиальную проблему? Я думаю, что весь вопрос связан с этим вопросом . Может быть, я просто глупый, может быть, я делаю вещи слишком сложными... в любом случае, спасибо за любые предложения!

1 Ответ

1 голос
/ 15 февраля 2012

На самом деле SQLAlchemy проверяет, существует ли объект, вызывая Session.merge().Но он делает это индивидуально - его первичный ключ.Самое простое решение - сделать name первичный ключ, и все будет работать.Конечно, цепочка из трех таблиц станет избыточной в этом случае, если вы не собираетесь добавлять дополнительные поля в Tag (например, counter).

...