Глава 22 · Веб-разработка с Python

Flask и SQLite: сохраняем данные в базе

Список Python из раздела 22.5 становится таблицей SQLite, которая переживает перезапуск.

Теперь список Python из раздела 22.5 заменяется базой данных SQLite. Схема простая — одна таблица:

schema.sql
CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    done INTEGER NOT NULL DEFAULT 0 CHECK (done IN (0, 1))
);

CHECK (done IN (0, 1)) — дополнительное ограничение: SQLite не даст записать в столбец done ничего, кроме 0 или 1, даже если в коде приложения где-то закрадётся ошибка.

Одно подключение на запрос

Открывать соединение с базой на каждый маленький вызов — расточительно, а держать одно общее соединение на всё приложение без осторожности — рискованно при параллельных запросах. Flask предлагает удобное место для данных, которые должны жить ровно один запрос, — объект g:

app.py
import sqlite3
from flask import Flask, g

app = Flask(__name__)
app.config["DATABASE"] = "zadachi.db"


def get_db():
    if "db" not in g:
        g.db = sqlite3.connect(app.config["DATABASE"])
        g.db.row_factory = sqlite3.Row
    return g.db


@app.teardown_appcontext
def close_db(exception=None):
    db = g.pop("db", None)
    if db is not None:
        db.close()
g привязан к контексту приложения, а не буквально к запросу
g хранит данные в рамках контекста приложения Flask. При обычной обработке HTTP-запроса этот контекст живёт ровно с начала обработки запроса до его конца и создаётся заново для каждого следующего запроса — поэтому на практике g ведёт себя как хранилище на один запрос, а не как общая переменная, из которой данные могут случайно «утечь» в другой запрос. teardown_appcontext вызывается при закрытии контекста приложения, даже если обработчик завершился с ошибкой, — соединение всегда закрывается.

Маршруты поверх базы данных

app.py
@app.route("/")
def glavnaya():
    db = get_db()
    zadachi = db.execute("SELECT id, title, done FROM tasks ORDER BY id").fetchall()
    return render_template("index.html", zadachi=zadachi)


@app.route("/dobavit", methods=["POST"])
def dobavit():
    title = clean_title(request.form.get("zadacha", ""))
    if title is None:
        flash("Название задачи не может быть пустым и не длиннее 200 символов.")
        return redirect(url_for("glavnaya"), code=303)
    db = get_db()
    db.execute("INSERT INTO tasks (title) VALUES (?)", (title,))
    db.commit()
    return redirect(url_for("glavnaya"), code=303)

Обратите внимание: ? в SQL-запросе и значение отдельным аргументом — тот же параметризованный запрос из раздела 22.22, теперь внутри рабочего приложения. flash(...) сохраняет сообщение, которое покажется на следующей странице после редиректа — как именно это устроено, показывает следующий раздел, 22.30. clean_title(...) — простая функция проверки из раздела 22.31: пустое или слишком длинное название отклоняется ещё до обращения к базе.

Пустой список задач: заголовок «Мой список задач», подпись «Задач пока нет — добавьте первую ниже» и поле ввода
Список задач сразу после init_db() — таблица создана, но пуста.
Список задач с одной строкой «Купить хлеб» и пустым круглым чекбоксом слева
После отправки формы: INSERT прошёл, редирект вернул на / — задача видна в списке.
Тот же список: «Купить хлеб» зачёркнут, чекбокс слева с галочкой
После клика по кружку: UPDATE tasks SET done = 1 — строка помечена как выполненная.

Полный рабочий файл — со всеми маршрутами, обработкой ошибок и API — лежит здесь:

projects/flask/todo-app/app.py

Проверьте сами: перезапустите — задачи никуда не делись
Запустите python app.py, добавьте пару задач, остановите процесс (Ctrl+C) и запустите заново. Список останется тем же самым — именно то, чего не хватало версии из раздела 22.5.
Список задач в браузере после перезапуска процесса: «Купить хлеб» по-прежнему отмечен выполненным, ниже добавлена новая строка «Написать тесты для API»
Реальная проверка: процесс Flask был полностью остановлен и запущен заново на том же файле базы данных. «Купить хлеб» остался отмеченным, а новая задача, добавленная уже новым процессом, сохранилась рядом — файл SQLite пережил перезапуск, обычный список Python не пережил бы.
Практика: Flask поверх SQLite
Модуль flask не установлен в браузерном окружении Pyodide — выполните локально в VS Code, PyCharm или Jupyter
Практика выполняется локально
Открыть практику →