Объединение Flask с SQLAlchemy: практические варианты и их реализация
Интеграция Flask и SQLAlchemy: методы и код
Эта статья описывает несколько путей интеграции Flask с SQLAlchemy. Каждый вариант сопровождается вопросом, который он решает, примерами кода и указанием возможных проблем.
Как создать типичное Flask-приложение с SQLAlchemy через расширение Flask-SQLAlchemy?
Наиболее распространённый способ - использование Flask-SQLAlchemy. Этот пакет упрощает настройку, управление сессиями и связывание с контекстом приложения Flask.
Установка
pip install flask flask-sqlalchemyPython 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}'