Python: уроки, справочник и задачи

Работа с базой данных во Flask

Раздел: Flask и Tkinter · Обновлено 31.10.2026

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

Работа с базой данных во Flask
Фото: BalticServers.com / CC BY-SA 3.0, Wikimedia Commons
Содержание статьи
  1. Какой способ выбрать
  2. Подключение SQLite без расширений
  3. Установка и настройка Flask-SQLAlchemy
    1. Строки подключения к разным СУБД
  4. Модели и создание таблиц
  5. Запросы: создание, чтение, изменение, удаление
    1. Сохранение данных из формы
  6. Миграции схемы с Flask-Migrate
  7. Частые вопросы
    1. Что значит ошибка Working outside of application context?
    2. Почему не появился новый столбец после изменения модели?
    3. Где лежит файл SQLite, если путь указан как sqlite:///project.db?
    4. Можно ли выполнить обычный SQL через Flask-SQLAlchemy?

Какой способ выбрать

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 появится именно там.

Строки подключения к разным СУБД

Формат адреса: диалект+драйвер://пользователь:пароль@хост:порт/база. Для серверных СУБД нужно дополнительно установить драйвер.

Пароль от боевой базы не хранят в коде: его читают из переменной окружения через 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 и словарь.