Работа с базой данных во Flask
База данных во Flask подключается либо напрямую через модуль sqlite3, либо через расширение Flask-SQLAlchemy с ORM-моделями. В статье показаны оба способа на рабочих примерах: настройка подключения, описание моделей, создание таблиц, запросы на чтение и запись, связь один ко многим и миграции схемы без потери данных.

Содержание статьи
Какой способ выбрать
Flask сам по себе не содержит слоя для работы с базой: это микрофреймворк, и хранилище подключается отдельно. На практике используют два подхода.
| Критерий | sqlite3 напрямую | Flask-SQLAlchemy |
|---|---|---|
| Зависимости | Только стандартная библиотека | Flask-SQLAlchemy и SQLAlchemy |
| Запросы | SQL-строки вручную | Python-классы и методы, SQL генерируется |
| Смена СУБД | Переписывать запросы | Поменять строку подключения |
| Миграции | Вручную | Flask-Migrate (Alembic) |
| Когда подходит | Учебный проект, пара таблиц | Приложение, которое будет расти |
Если с маршрутами и шаблонами вы еще не работали, начните с основ Flask, а затем возвращайтесь к подключению хранилища.
Подключение SQLite без расширений
Соединение с базой не стоит открывать глобально при запуске: Flask обрабатывает запросы в разных потоках, а объект sqlite3 по умолчанию нельзя передавать между ними. Правильный шаблон - открывать соединение один раз на запрос, хранить его в специальном объекте g и закрывать после ответа.
import sqlite3
from flask import Flask, g
app = Flask(__name__)
DATABASE = 'app.db'
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(DATABASE)
g.db.row_factory = sqlite3.Row # доступ к столбцам по имени
return g.db
@app.teardown_appcontext
def close_db(exc):
db = g.pop('db', None)
if db is not None:
db.close()Функция, помеченная teardown_appcontext, вызывается после каждого запроса, даже если в обработчике произошла ошибка. Благодаря sqlite3.Row строку результата можно читать как словарь: row['title'].
@app.route('/tasks')
def tasks():
rows = get_db().execute('SELECT id, title FROM task ORDER BY id').fetchall()
return [dict(r) for r in rows] # Flask сам превратит список в JSONСами SQL-запросы при таком подходе ничем не отличаются от обычного скрипта, их синтаксис разобран в уроке о модуле sqlite3.
Установка и настройка Flask-SQLAlchemy
Расширение ставится одной командой pip install flask-sqlalchemy, SQLAlchemy подтянется как зависимость. Адрес базы задается в конфигурации ключом SQLALCHEMY_DATABASE_URI.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///project.db'
db.init_app(app)Объект db создается отдельно от приложения и связывается с ним через init_app(). Так удобнее разносить код по модулям и использовать фабрику приложений. В Flask-SQLAlchemy 3.x относительный путь к SQLite считается от папки instance рядом с проектом, поэтому файл project.db появится именно там.
Строки подключения к разным СУБД
Формат адреса: диалект+драйвер://пользователь:пароль@хост:порт/база. Для серверных СУБД нужно дополнительно установить драйвер.
- SQLite:
sqlite:///project.db, драйвер встроен в Python; - PostgreSQL:
postgresql+psycopg2://user:pass@localhost/shop, пакет psycopg2-binary; - MySQL:
mysql+pymysql://user:pass@localhost/shop, пакет PyMySQL.
Пароль от боевой базы не хранят в коде: его читают из переменной окружения через os.environ.
Модели и создание таблиц
Модель - это класс, унаследованный от db.Model, где каждый атрибут описывает столбец. В SQLAlchemy 2.x тип столбца задают аннотацией Mapped[...], а параметры через mapped_column(). Имя таблицы расширение выведет из имени класса само (User станет user), но его можно задать явно через __tablename__.
from datetime import datetime
from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
class User(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(80), unique=True)
email: Mapped[str | None]
tasks: Mapped[list['Task']] = relationship(back_populates='owner')
class Task(db.Model):
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
done: Mapped[bool] = mapped_column(default=False)
created: Mapped[datetime] = mapped_column(default=datetime.now)
user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))
owner: Mapped[User] = relationship(back_populates='tasks')
with app.app_context():
db.create_all()Аннотация str | None делает столбец необязательным, без нее столбец получает ограничение NOT NULL. Пара relationship с back_populates описывает связь один ко многим: у пользователя появляется список user.tasks, у задачи ссылка task.owner. Как устроены такие связи на уровне таблиц, объясняет статья про первичный и внешний ключ.
Метод create_all() создает только отсутствующие таблицы и не меняет существующие. Вызывать его нужно внутри контекста приложения, иначе расширение не узнает, к какой базе подключаться.
Запросы: создание, чтение, изменение, удаление
Все изменения проходят через сессию db.session: объекты добавляются в нее, а в базу записываются при вызове commit(). Внутри обработчиков маршрутов контекст приложения уже активен, поэтому with app.app_context() там не нужен.
with app.app_context():
# создание
ann = User(username='anna', email='anna@mail.test')
ann.tasks.append(Task(title='Купить хлеб'))
db.session.add(ann)
db.session.commit()
# чтение
user = db.session.execute(
db.select(User).filter_by(username='anna')
).scalar_one()
open_tasks = db.session.scalars(
db.select(Task).where(Task.done.is_(False)).order_by(Task.created)
).all()
# изменение
open_tasks[0].done = True
db.session.commit()
# удаление
db.session.delete(open_tasks[0])
db.session.commit()Для поиска по первичному ключу есть короткая форма db.session.get(User, 1), которая вернет None, если записи нет. В обработчиках удобнее db.get_or_404(User, user_id): при отсутствии строки клиент сразу получит ответ 404. Старый синтаксис User.query.filter_by(...) по-прежнему работает, но в документации SQLAlchemy 2.x он отмечен как устаревший.
Сохранение данных из формы
Обработчик POST-запроса берет поля из request.form, проверяет их и создает объект модели. После записи делают перенаправление, чтобы повторное обновление страницы не отправило форму второй раз.
from flask import request, redirect, url_for, abort
@app.post('/users/<int:user_id>/tasks')
def add_task(user_id):
title = request.form.get('title', '').strip()
if not title:
abort(400)
user = db.get_or_404(User, user_id)
user.tasks.append(Task(title=title))
db.session.commit()
return redirect(url_for('index')) # главная страница со списком задачПроверку полей и защиту от подделки запросов удобно поручить Flask-WTF, подробности в статье о формах во Flask.
Миграции схемы с Flask-Migrate
Когда приложение уже работает и в базе есть данные, добавить столбец через create_all() не получится: метод не трогает существующие таблицы. Для изменения схемы используют расширение Flask-Migrate, построенное на Alembic.
from flask_migrate import Migrate
migrate = Migrate(app, db)
# в терминале:
# flask db init - один раз, создает папку migrations
# flask db migrate -m "add phone" - сравнивает модели с базой и пишет скрипт
# flask db upgrade - применяет изменения к базеСгенерированный скрипт миграции стоит просматривать перед применением: Alembic не всегда распознает переименование столбца и может предложить удалить старый и создать новый, что приведет к потере данных. У SQLite есть свои ограничения на ALTER TABLE, поэтому для нее в Migrate часто включают параметр render_as_batch=True.
Полное описание настроек и методов есть в документации Flask-SQLAlchemy.
Частые вопросы
Здесь собраны ошибки, которые чаще всего возникают при подключении базы к Flask.
Что значит ошибка Working outside of application context?
Код обращается к db вне запроса, например в скрипте или в консоли. Оберните его в with app.app_context():.
Почему не появился новый столбец после изменения модели?
create_all() не изменяет уже созданные таблицы. Используйте миграции Flask-Migrate или, в учебном проекте, удалите файл базы и создайте таблицы заново.
Где лежит файл SQLite, если путь указан как sqlite:///project.db?
В Flask-SQLAlchemy 3.x относительный путь отсчитывается от папки instance проекта. Чтобы положить файл в другое место, укажите абсолютный путь.
Можно ли выполнить обычный SQL через Flask-SQLAlchemy?
Да: db.session.execute(db.text('SELECT count(*) FROM task')).scalar(). Параметры передают через метки вида :name и словарь.