Объединение Flask с SQLAlchemy: практические варианты и их реализация

Раздел: Веб-разработка -> Flask и SQLAlchemy

Интеграция Flask и SQLAlchemy: методы и код

Эта статья описывает несколько путей интеграции Flask с SQLAlchemy. Каждый вариант сопровождается вопросом, который он решает, примерами кода и указанием возможных проблем.

Как создать типичное Flask-приложение с SQLAlchemy через расширение Flask-SQLAlchemy?

Наиболее распространённый способ - использование Flask-SQLAlchemy. Этот пакет упрощает настройку, управление сессиями и связывание с контекстом приложения Flask.

Установка

pip install flask flask-sqlalchemy

Python flask sqlalchemy (интеграция flask и sqlalchemy)

Базовая настройка

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///mydb.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __repr__(self):
        return f''

@app.route('/add_user')
def add_user():
    user = User(username='john', email='john@example.com')
    db.session.add(user)
    db.session.commit()
    return f'User {user.username} created'

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True)

Пояснение

Объект db инкапсулирует SQLAlchemy. Модели наследуются от db.Model, что автоматически связывает их с текущим приложением. db.session - это сессия, управляемая Flask. Вызов db.create_all() внутри контекста приложения создаёт таблицы.

Типичные ошибки и их решение

  • RuntimeError: Working outside of application context - возникает, если db.create_all() вызван вне контекста приложения. Решение: обернуть вызов в with app.app_context(): ... или передать app параметром при инициализации.
  • IntegrityError из-за дублирования уникальных полей. Решение: проверять существование записи перед добавлением.
  • SQLALCHEMY_TRACK_MODIFICATIONS без False может потреблять память. Установка False отключает сигналы.

Как избежать привязки к Flask и использовать SQLAlchemy напрямую?

Если требуется более тонкий контроль над сессиями или переносимость кода за пределы Flask, применяется чистый SQLAlchemy без расширения. Также это удобно для тестирования.

Пример настройки

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite:///standalone.db')
Base = declarative_base()

class Product(Base):
    __tablename__ = 'products'
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    price = Column(Integer, default=0)

Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()
product = Product(name='Widget', price=1500)
session.add(product)
session.commit()
session.close()

Пояснение

Здесь нет привязки к Flask. Сессия создаётся вручную через sessionmaker. Необходимо явно закрывать сессию, чтобы освободить ресурсы. Для использования в Flask потребуется самостоятельно управлять контекстом сессии (например, через текера или декоратор).

Типичные ошибки и их решение

  • DetachedInstanceError при попытке доступа к объекту после закрытия сессии. Решение: сохранять данные до закрытия или использовать session.expire_on_commit = False.
  • Не забыть session.commit() - изменения не сохранятся. Решение: оборачивать операции в блок try/finally и выполнять commit.

Как управлять изменениями схемы базы данных с помощью миграций?

Для эволюции схемы (добавление столбцов, изменение типов) применяется Alembic в связке с Flask-Migrate (надстройка над Alembic для Flask).

Настройка через Flask-Migrate

pip install Flask-Migrate

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///migrate.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80))
    body = db.Column(db.Text)

Команды (выполняются в терминале):

flask db init          # инициализация папки миграций
flask db migrate -m "Initial migration"  # создание скрипта миграции
flask db upgrade       # применение изменений к БД

Пояснение

Flask-Migrate автоматически обнаруживает изменения в моделях и генерирует миграции. flask db migrate создаёт файл, который можно проверить и изменить вручную.

Типичные ошибки и их решение

  • Migration script not generated - если не импортированы все модели. Решение: импортировать модели перед migrate или в файле app.py.
  • Дублирование версий при использовании нескольких веток. Решение: удалить лишние файлы миграций и заново создать одну.

Как обеспечить асинхронную работу с базой данных в Flask?

Для асинхронных веб-фреймворков (или при использовании ASGI-серверов) требуется асинхронный драйвер. SQLAlchemy поддерживает async через asyncpg (для PostgreSQL) или aiosqlite.

Настройка с aiosqlite и Flask (через Quart или хак)

Flask синхронный, поэтому асинхронность обычно используют с Quart (асинхронный аналог Flask). Пример с Quart:

pip install quart quart-sqlalchemy aiosqlite

from quart import Quart
from quart_sqlalchemy import SQLAlchemy

app = Quart(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite+aiosqlite:///async.db'
db = SQLAlchemy(app)

@app.route('/async_add')
async def async_add():
    async with db.session.begin():
        db.add(User(username='async_user', email='async@example.com'))
    return 'User added asynchronously'

Для Flask можно использовать asyncio.run в представлении, но это не даёт реального параллелизма. Лучше перейти на Quart.

Типичные ошибки и их решение

  • RuntimeError: No running event loop - попытка использовать async вне асинхронного контекста. Решение: использовать только в async-функциях.
  • Проблемы с драйверами: не все базы данных имеют стабильные асинхронные драйверы. Проверять версии.

Расширенные примеры и нестандартные приёмы

Здесь приведены более сложные сценарии использования SQLAlchemy совместно с Flask.

1. Работа с отношениями (один-ко-многим)

Пример модели с внешним ключом и связью.

Пример
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

class Author(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100))
    books = db.relationship('Book', backref='author', lazy='dynamic')

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200))
    author_id = db.Column(db.Integer, db.ForeignKey('author.id'))

# Создание объектов
author = Author(name='Лев Толстой')
db.session.add(author)
book1 = Book(title='Война и мир', author=author)
book2 = Book(title='Анна Каренина', author=author)
db.session.add_all([book1, book2])
db.session.commit()

# Запрос книг автора
books = author.books.all()
for b in books:
    print(b.title)
Война и мир
Анна Каренина

2. Многие-ко-многим с помощью вспомогательной таблицы

Пример
tags_articles = db.Table('tags_articles',
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
    db.Column('article_id', db.Integer, db.ForeignKey('article.id'), primary_key=True)
)

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))

class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100))
    tags = db.relationship('Tag', secondary=tags_articles, backref='articles', lazy='subquery')

# Использование
article = Article(title='Flask и SQLAlchemy')
tag1 = Tag(name='Python')
tag2 = Tag(name='ORM')
article.tags = [tag1, tag2]
db.session.add(article)
db.session.commit()

3. Использование hybrid_property для виртуальных полей

Пример
from sqlalchemy.ext.hybrid import hybrid_property

class Employee(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(50))
    last_name = db.Column(db.String(50))
    salary = db.Column(db.Integer)

    @hybrid_property
    def full_name(self):
        return f'{self.first_name} {self.last_name}'

    @hybrid_property
    def annual_salary(self):
        return self.salary * 12

emp = Employee(first_name='Иван', last_name='Петров', salary=50000)
db.session.add(emp)
db.session.commit()
print(emp.full_name, emp.annual_salary)
Иван Петров 600000

4. Массовая вставка с использованием bulk_insert

Пример
data = [{'name': 'Товар1', 'price': 100},
        {'name': 'Товар2', 'price': 200}]
db.session.execute(db.insert(Product).values(data))
db.session.commit()
# Для SQLAlchemy Core:
from sqlalchemy import insert
stmt = insert(Product).values(data)
with engine.connect() as conn:
    conn.execute(stmt)
    conn.commit()

Массовая вставка быстрее, чем добавление по одному объекту, но не вызывает событий ORM.

5. Выполнение raw SQL внутри сессии

Пример
result = db.session.execute('SELECT * FROM user WHERE id=:uid', {'uid': 1})
row = result.fetchone()
print(row.username)

# Или с использованием текстового выражения:
from sqlalchemy import text
result = db.session.execute(text('SELECT * FROM user WHERE id=:uid'), {'uid': 1})
john

6. Каскадное удаление

Пример
class Parent(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    children = db.relationship('Child', cascade='all, delete-orphan', backref='parent')

class Child(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    parent_id = db.Column(db.Integer, db.ForeignKey('parent.id'))

parent = Parent(name='Родитель')
child = Child(name='Ребенок', parent=parent)
db.session.add(parent)
db.session.commit()
db.session.delete(parent)  # Каскадно удалит и child
db.session.commit()

7. Использование контекстного менеджера для сессий (чистый SQLAlchemy)

Пример
from contextlib import contextmanager

@contextmanager
def session_scope():
    session = Session()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

with session_scope() as s:
    user = s.query(User).get(1)
    user.name = 'new_name'
# Автоматический commit/rollback

8. Использование декоратора для упрощения работы с сессией в Flask (без расширения)

Пример
from functools import wraps
from flask import g, request, current_app

def with_session(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not hasattr(g, 'session'):
            g.session = Session()
        try:
            result = f(*args, **kwargs)
            g.session.commit()
            return result
        except Exception as e:
            g.session.rollback()
            raise e
        finally:
            g.session.close()
    return decorated_function

@app.route('/session_example')
@with_session
def session_example():
    user = g.session.query(User).first()
    return f'Hello {user.username}'

Интеграция Flask и SQLAlchemy - comments

En
Python flask sqlalchemy (python)