SQLAlchemy — many-to-many

Admin SQLAlchemy

Описание связи many-to-many (многие ко многим) в SQLAlchemy. Пример создания таблиц и сохранения данных.

Описание моделей (классов)

Мы создаем два класса: Task и Category, в одной таблице будут сохранятся задачи, в другой категории.

Таблица задач

Таблица категорий

У одной задачи может быть несколько категорий. Для этих целей наилучшим образом подойдет сохранение связи между задачей и категорией в отдельной таблице. Это будет третья таблица, которая будет создана с помощью связи db.relationship.

Все это делается описанием классов моделей таким образом:

# ploshadka.net
from datetime import datetime
from flask_login import UserMixin
from flask_security import RoleMixin

tasks_categories = db.Table(
    'tasks_categories',
    db.Column('task_id', db.Integer(), db.ForeignKey('task.id')),
    db.Column('cat_id', db.Integer(), db.ForeignKey('categories.id'))
)

class Task(db.Model, RoleMixin):
    __tablename__ = 'tasks'
    id = db.Column(db.Integer(), primary_key=True, unique=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    name = db.Column(db.String(255))
    time = db.Column(db.DateTime())

    # Для получения доступа к связанным объектам
    cats = db.relationship('Category', secondary=tasks_categories, backref=db.backref('tasks', lazy='dynamic'))

class Category(db.Model, RoleMixin):
    __tablename__ = 'categories'
    id = db.Column(db.Integer(), primary_key=True, unique=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    name = db.Column(db.String(255))

Если требуется в дальнейшем удалять одной командой все связанные данные, то в db.relationship надо добавить пару дополнительных свойств такого плана:

    cats = db.relationship(
        'Category',
        secondary=tasks_categories,
        backref=db.backref('task', lazy='dynamic'),
        single_parent=True,
        cascade="all, delete, delete-orphan"
    )

Однако в данном примере это не подходит, иначе удалив задачу с привязанной к ней категорией, мы удалим эти категории из всех других задач в том числе.

Как сохранить данные

Пример ниже представляет из себя методы с роутом (через блюпринт). Она получает данные через метод POST. Затем эти данные сохраняются в БД и возвращаются обратно.

Внутри методов осуществляется сохранение задачи, категорий и связи с ними. Все основные моменты пояснены в первом методе.

Метод сохранения к задаче одной категории

# ploshadka.net
@task.route('/add-new-item/', methods=['POST'])
@login_required
def add_new_item():

    # Данные с фронта
    data = request.get_json()
    name = data['name']
    category = data['category']

    # Текущий пользователь
    user_id = flask_login.current_user.id

    # Определим существование категории
    c1 = db.session.query(Category).filter_by(user_id=user_id, name=category).first()

    # Если категории нет, создадим её
    if c1 is None:
        c1 = Category(user_id=user_id, name=category)
        db.session.add(c1)

        # Используем flush, чтобы получить id категории, которая будет добавлена
        db.session.flush()

    # Добавим задачу
    new_task = Task(user_id=user_id, name=name, time=datetime.now())

    # Добавим связь
    new_task.cats.append(c1)
    db.session.add(new_task)

    # Теперь сохраним все что выше в нашу базу данных
    db.session.commit()

    # Вернем обновленные данные обратно на фронт
    return get_items()

Метод сохранения к задаче несколько категорий

В этом случае наш метод немного изменится. С фронта мы получаем список категорий через запятую. Формируем из этой строки список категорий. Проверяем каждую категорию на существование в базе данных. Затем или добавляем к задаче её или создаём и опять же добавляем к задаче.

# ploshadka.net
@task_bp.route('/add-new-item/', methods=['POST'])
@login_required
def add_new_item():
    user_id = flask_login.current_user.id

    # Получим данные с фронта
    data = request.get_json()
    name = data['name']
    categories = data['categories']

    # Добавим новую задачу
    new_task = Task(user_id=user_id, name=name, time=datetime.now())

    # Сделаем лист из нескольких категорий (разделим их по запятым)
    categories = [x.strip() for x in categories.split(',')]

    # Для каждой категории
    for category_name in categories:

        # Определим существует ли категория
        category_in_db = db.session.query(Category).filter_by(user_id=user_id, name=category_name).first()

        if category_in_db:
            new_task.cats.append(category_in_db)
        else:
            c = Category(user_id=user_id, name=category_name)
            db.session.add(c)
            db.session.flush()
            new_task.cats.append(c)

    # Сохраним
    db.session.add(new_task)
    db.session.commit()

    # Вернем обновленные данные
    return get_items()

Если вам пригодилась информация, вы можете поблагодарить автора сайта символическим пожертвованием:

Добавить комментарий

Напишите свой комментарий, если вам есть что добавить/поправить/спросить по теме текущей статьи:
"SQLAlchemy — many-to-many"