Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 5 из 20 Средний 45 мин 120 XP

База данных в Flask: SQLite и Flask-SQLAlchemy

Пятый урок курса Flask: данные переживают перезапуск сервера. Подключаем SQLite, описываем модели Flask-SQLAlchemy, проходим CRUD и собираем гостевую книгу, которая помнит всех гостей.

Редакция Питоники

Отзывы, которые гости оставили через форму из урока 4, живут ровно до первого перезапуска сервера. Мы приняли данные, поблагодарили автора и забыли: переменные в Python умирают вместе с процессом. Утром кофейня открывается с чистого листа, как будто ни одного отзыва и не было. Приложению нужен постоянный документ, а не оперативная память — то есть база данных.

Сегодня подключим SQLite — базу в одном файле, поверх неё — Flask-SQLAlchemy, и превратим отзывы в строки таблицы: опишем модель, создадим таблицу, пройдём весь цикл CRUD (создание, чтение, изменение, удаление) и соберём гостевую книгу целиком. Серверные примеры, как обычно в этом курсе, показаны с реальным выводом — Flask и SQLAlchemy в браузерной песочнице не поднять. А вот рендер списка записей из базы — настоящий Jinja2, и он запустится прямо на странице.

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

SQLite: база данных без сервера

Когда слышите «база данных», воображение рисует отдельную программу-сервер: PostgreSQL, MySQL, порты, пользователи, пароли. SQLite устроена иначе: это библиотека, а вся база — один файл на диске рядом с вашим кодом. Ни демона, ни сети, ни учётных записей: подключился к файлу — работаешь. Для учебных проектов, прототипов, локальных инструментов и небольших сайтов это идеальный вариант, и он уже встроен в Python — отдельная установка не нужна.

SQLite — самая развёрнутая СУБД в мире: она живёт в каждом смартфоне, внутри браузеров и в десятках миллиардов устройств. Если вы ищете базу, проверенную производством сильнее всех остальных, — это один файл на диске.

Голый sqlite3 — чтобы почувствовать разницу
import sqlite3

conn = sqlite3.connect("guestbook.db")
conn.execute(
    "CREATE TABLE IF NOT EXISTS entries ("
    "id INTEGER PRIMARY KEY, name TEXT, text TEXT)"
)
conn.execute(
    "INSERT INTO entries (name, text) VALUES (?, ?)",
    ("Марина", "Лучший раф в городе"),
)
conn.commit()

for row in conn.execute("SELECT name, text FROM entries"):
    print(row)

conn.close()
Вывод
('Марина', 'Лучший раф в городе')
Модуль sqlite3 встроен в Python, но в браузерной песочнице нет ни файловой системы, ни движка SQLite — блок приведён с реальным выводом с локальной машины. Запустите его дома: рядом со скриптом появится файл guestbook.db.

Это «голый» SQL: текст запросов в кавычках, данные подставляются через плейсхолдеры со знаками вопроса (никогда не склеивайте SQL из строк — так попадают в SQL-инъекции). Работает отлично, но для сайта получается каша: строки SQL вперемешку с логикой, опечатка в имени колонки вылезет только при выполнении запроса. Чтобы писать на Python и не думать о синтаксисе SQL, придумали ORM.

ORM: таблица становится классом

ORM (Object-Relational Mapping, объектно-реляционное отображение) — переводчик между двумя мирами: таблицами базы данных и объектами Python. Таблица описывается классом, строка — экземпляром этого класса, колонка — атрибутом. Вместо INSERT INTO вы пишете db.session.add(entry), вместо SELECT — Entry.query.all(). Стандарт де-факто в мире Flask — SQLAlchemy, а Flask-SQLAlchemy — тонкая обвязка, которая привязывает её к приложению: настройки, сессии, контексты.

Мир базы данныхМир Python (SQLAlchemy)
Таблица entriesКласс Entry(db.Model)
Строка таблицыЭкземпляр Entry(name=..., text=...)
КолонкаАтрибут класса: name, text, id
INSERT / SELECT / UPDATE / DELETEadd() / query / присваивание / delete()
Опечатка в SQL всплывает при запросеОшибка в модели видна на старте приложения

Ставится расширение одной командой в уже знакомое виртуальное окружение:

Терминал: установка Flask-SQLAlchemy
(venv) $ pip install flask-sqlalchemy
Вывод
Collecting flask-sqlalchemy
  Downloading flask_sqlalchemy-3.1.1-py3-none-any.whl.metadata (3.2 kB)
...
Successfully installed flask-sqlalchemy-3.1.1 sqlalchemy-2.0.36
Реальный вывод pip; версии на момент вашего чтения будут новее. SQLAlchemy ставится как зависимость — это ядро, а Flask-SQLAlchemy лишь интегрирует его с приложением.

Подключение к приложению и модель Entry

Подключение — две строки: адрес базы в конфигурации (URI) и создание объекта db. Для SQLite URI выглядит как sqlite:///guestbook.db:

app.py — подключение базы
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///guestbook.db"
db = SQLAlchemy(app)
Сервер и база в браузере не поднимаются — это реальный код с ожидаемым результатом на локальной машине. В Flask 3.x файл базы появится в папке instance/ рядом с проектом, а не в корне.

Теперь сердце урока — модель. Опишем запись гостевой книги классом Entry:

app.py — модель записи гостевой книги
from datetime import datetime

class Entry(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    text = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.now)

    def __repr__(self):
        return f"<Entry {self.id} {self.name!r}>"
Классы моделей объявляют после создания db, рядом с app.py. В песочнице flask_sqlalchemy недоступна — блок с ожидаемым поведением.

Разбор по строкам. Класс наследуется от db.Model — так SQLAlchemy понимает, что перед ней таблица. primary_key=True делает колонку идентификатором строки: база сама присвоит 1, 2, 3 без вашего участия. String(100) — строка с лимитом в сто символов (имя), Text — без лимита (отзыв). nullable=False запрещает пустые значения: попытка сохранить запись без имени упадёт с ошибкой, а не просочится в базу мусором. default=datetime.now подставит время создания автоматически. Метод __repr__ необязателен, но благодарен: в консоли и логах запись печатается компактно.

Какие бывают типы колонок

  • db.Integer — целые числа: id, счётчики, суммы
  • db.String(100) — короткая строка с лимитом: имя, email, заголовок
  • db.Text — длинный текст без лимита: отзыв, статья, комментарий
  • db.Float — дробные числа: цена 249.90
  • db.Boolean — истина и ложь: опубликован, оплачен
  • db.DateTime — дата и время: created_at, published_at

Когда в уроке 9 появится модель пользователя, этот список дополнится колонкой с хешем пароля — той же String, только содержимое не светится даже вам. А сейчас создадим таблицу.

db.create_all(): превращаем класс в таблицу

Класс модели — только описание. Чтобы в файле базы появилась настоящая таблица, вызовите db.create_all(). И здесь на всех новичков ждёт один и тот же сюрприз: SQLAlchemy требует контекст приложения.

Создание таблиц
with app.app_context():
    db.create_all()
    print("Таблицы созданы")
Вывод
Таблицы созданы
Код реальный, выполняется локально: в песочнице нет flask_sqlalchemy. В приложении create_all обычно вызывают один раз при настройке или через собственную CLI-команду.

CRUD, буква C: сохраняем запись

CRUD — четыре базовых операции над данными: Create, Read, Update, Delete. Создание выглядит как работа с обычным объектом: сконструировали, положили в сессию, зафиксировали:

Добавление записи
with app.app_context():
    entry = Entry(name="Марина", text="Лучший раф в городе")
    db.session.add(entry)
    db.session.commit()
    print(entry.id, entry.created_at)
Вывод
1 2025-01-15 08:31:04.512930
Дата, конечно, будет вашей. Важно другое: после commit у объекта появился id — база присвоила его при вставке и отдала обратно в Python-объект.

CRUD, буква R: читаем записи

За чтение отвечает Query-объект: Entry.query — это выборка всех записей таблицы, которую фильтруют и сортируют цепочкой методов. Вот самые ходовые приёмы на заполненной базе:

Чтение: all, filter_by, get
with app.app_context():
    for e in Entry.query.order_by(Entry.id).all():
        print(e)

    print(Entry.query.filter_by(name="Марина").count())
    print(db.session.get(Entry, 2).text)
Вывод
<Entry 1 'Марина'>
<Entry 2 'Игорь'>
1
Кофе холодный, а обслуживание на высоте
Код выполняется локально на базе, заполненной предыдущими блоками. В браузерной песочнице SQLAlchemy недоступна — вывод реальный.

filter_by(name="Марина") — фильтр по точному совпадению, аналог WHERE name = 'Марина'. count() считает строки, order_by(Entry.id) сортирует, .all() возвращает список объектов. db.session.get(Entry, 2) — современный способ достать запись по первичному ключу; если записи нет, вернётся None. В старых туториалах встречается Entry.query.get(2) — это работает, но официально считается устаревшим стилем.

ВызовЧто делаетSQL-эквивалент
Entry.query.all()все записи спискомSELECT * FROM entries
Entry.query.filter_by(name="Марина")фильтр по точному совпадениюWHERE name = "Марина"
Entry.query.order_by(Entry.id.desc())сортировка по убываниюORDER BY id DESC
Entry.query.first()первая запись или NoneLIMIT 1
Entry.query.limit(10)первые десять записейLIMIT 10
Entry.query.count()число записейSELECT COUNT(*)
db.session.get(Entry, 2)запись по первичному ключуWHERE id = 2

Красивое свойство Query: каждый filter_by возвращает новый Query, ничего не спрашивая у базы. Запрос уйдёт только при «финализаторах» — all, first, count. Поэтому фильтры собирают по частям: q = Entry.query.filter_by(...); если пользователь выбрал сортировку — q = q.order_by(...). Именно так строят каталоги с чекбоксами, параметры которых приходят через request.args.

CRUD, буквы U и D: изменяем и удаляем

Изменение и удаление записи
with app.app_context():
    e = db.session.get(Entry, 2)
    e.text = "Кофе доехал горячим, спасибо!"
    db.session.commit()

    e = db.session.get(Entry, 2)
    db.session.delete(e)
    db.session.commit()
    print(Entry.query.count())
Вывод
1
Изменение - это присваивание атрибута плюс commit; удаление - delete() плюс commit. После удаления в гостевой книге осталась одна запись.

Обновление поразительно скучное: нашли объект, изменили атрибут, commit — SQLAlchemy сама превратит это в UPDATE с условием WHERE id = 2. С удалением так же. В веб-маршрутах поиск по ключу удобно делать через db.get_or_404(Entry, 2): вернёт запись или честную страницу 404 — вместо ветвления с проверкой на None после get.

Гостевая книга целиком: связываем форму с базой

Теперь соединим оба урока: форма из урока 4 посылает POST, мы создаём Entry, сохраняем и перенаправляем гостя на страницу со списком. Полный app.py, в котором всё уже привычно:

app.py — гостевая книга
from datetime import datetime

from flask import Flask, request, redirect, render_template
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///guestbook.db"
db = SQLAlchemy(app)

class Entry(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    text = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.now)

@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        name = request.form.get("name", "").strip()
        text = request.form.get("text", "").strip()
        if name and text:
            db.session.add(Entry(name=name, text=text))
            db.session.commit()
        return redirect("/")

    entries = Entry.query.order_by(Entry.id.desc()).all()
    return render_template("guestbook.html", entries=entries)

if __name__ == "__main__":
    with app.app_context():
        db.create_all()
    app.run()
Вывод
$ curl -X POST -d "name=Марина&text=Лучший раф в городе" http://127.0.0.1:5000/
$ curl http://127.0.0.1:5000/
<h1>Гостевая книга</h1>
<article>
  <strong>Марина</strong>
  <p>Лучший раф в городе</p>
</article>
Реальный код с ожидаемым результатом: первый запрос добавил запись и вернул 302, второй отдал страницу со списком. HTML сокращён до содержимого шаблона.

Порядок действий важен: POST сохраняет запись и делает redirect — паттерн Post/Redirect/Get из урока 4 в деле. GET достаёт записи из базы и передаёт их в шаблон списком. А как этот список превращается в HTML — вопрос к Jinja2, и он работает прямо здесь:

Шаблон гостевой книги (можно запустить)
from jinja2 import Template

# Так выглядит список, который вернул Entry.query.order_by(...).all()
entries = [
    {"name": "Марина", "text": "Лучший раф в городе"},
    {"name": "Игорь", "text": "Приду ещё с коллегами"},
]

template = Template("""<h1>Гостевая книга</h1>
{% for e in entries %}
<article>
  <strong>{{ e.name }}</strong>
  <p>{{ e.text }}</p>
</article>
{% endfor %}
{% if not entries %}
<p>Пока никто ничего не написал. Будьте первым!</p>
{% endif %}""", trim_blocks=True, lstrip_blocks=True)

print(template.render(entries=entries))
print(template.render(entries=[]))
Вывод
<h1>Гостевая книга</h1>
<article>
  <strong>Марина</strong>
  <p>Лучший раф в городе</p>
</article>
<article>
  <strong>Игорь</strong>
  <p>Приду ещё с коллегами</p>
</article>

<h1>Гостевая книга</h1>
<p>Пока никто ничего не написал. Будьте первым!</p>

Когда записей нет, срабатывает заглушка «будьте первым» — ветка {% if not entries %} из урока 3. В настоящем приложении этот шаблон лежит в templates/guestbook.html, а entries приходит из view-функции, но механика рендера ровно та же, что вы запустили выше.

Миграции: когда структура таблицы меняется

db.create_all() создаёт отсутствующие таблицы, но не изменяет существующие. Допишете в модель колонку rating — таблица останется старой, и первый же запрос упадёт с OperationalError: no such column: rating. Для эволюции схемы существуют миграции: расширение Flask-Migrate (обёртка над инструментом Alembic) сравнивает модели с фактической структурой базы и генерирует скрипты изменения. Команды запоминаются легко: flask db init один раз на проект, затем при каждом изменении моделей — flask db migrate -m "add rating" и flask db upgrade. До поры до времени учебному проекту хватает create_all с ручным удалением файла базы, но в большой проект из урока 10 миграции стоит встроить сразу.

Итоги: приложение получило память

  • SQLite — файл вместо сервера: идеальная первая база, встроена в Python
  • ORM переводит таблицы в классы: Entry(db.Model), строки — в объекты с атрибутами
  • Подключение — две строки: SQLALCHEMY_DATABASE_URI в config и db = SQLAlchemy(app)
  • db.create_all() под with app.app_context() создаёт таблицы из моделей
  • Создание: add() + commit(); чтение: query.filter_by / order_by / all; изменение и удаление — присваивание и delete() + commit
  • Схема меняется — Flask-Migrate и Alembic вместо ручного пересоздания

База есть, форма есть, гостевая книга помнит гостей между перезапусками. Половина курса позади: самоучитель Flask уже провёл вас от hello world до базы данных — впереди сессии, blueprint'ы и финальный блог. Следующий вопрос — личный: кто оставил отзыв? Когда в курсе появится вход по паролю, записи должны знать своего автора, а сервер — узнавать гостя между запросами. Для этого у Flask есть сессии и куки — разбираем их в уроке 6.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

class FakeQuery:
    def __init__(self, items):
        self.items = items

    def filter_by(self, **kw):
        key, value = list(kw.items())[0]
        return FakeQuery(
            [i for i in self.items if i.get(key) == value]
        )

    def count(self):
        return len(self.items)

entries = [
    {"author": "Марина", "score": "5"},
    {"author": "Игорь", "score": "4"},
    {"author": "Марина", "score": "4"},
]
print(FakeQuery(entries).filter_by(author="Марина").count())
from jinja2 import Template

t = Template(
    "{% for e in entries %}{{ e.name }}; {% endfor %}"
    "{% if not entries %}Пока пусто{% endif %}"
)
print(t.render(entries=[]))
Проверь себя
0 / 5

1. Чем SQLite отличается от PostgreSQL?

2. Что в ORM SQLAlchemy соответствует строке таблицы?

3. Зачем db.create_all() оборачивают в with app.app_context()?

4. Что делает db.session.commit()?

5. Что вернёт Entry.query.filter_by(name="Марина").all() на пустой базе?

Карточки терминов
Запомнено: 0 / 6
Практика

Словари вместо SQLAlchemy: напишите функцию filter_by(items, **kwargs), которая возвращает только те записи, у которых ВСЕ пары kwargs совпадают, — как настоящий Query. Отфильтруйте посты по автору «Марина» и напечатайте их id и текст.

practice.py
Вопросы и ответы по уроку

Как подключить базу данных к Flask?

Установите расширение (pip install flask-sqlalchemy), задайте app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///guestbook.db", создайте db = SQLAlchemy(app) и опишите модели классами от db.Model. Таблицы создаёт db.create_all() внутри with app.app_context().

Что означает ошибка Working outside of application context?

Вы обращаетесь к SQLAlchemy (create_all, запросы) вне контекста приложения, который существует только внутри view-функций или блока with app.app_context():. Оберните код скрипта или консоли в with app.app_context(): — и ошибка исчезнет.

Нужно ли знать SQL, если есть SQLAlchemy?

Базовый SQL желательно: он нужен для отладки запросов, оптимизации и чтения логов, а понимание JOIN и индексов объясняет, почему Query ведёт себя именно так. Но в повседневном Flask-коде 90 процентов операций покрывается filter_by, order_by и get — писать SQL руками не придётся.

Как исправить OperationalError: no such column?

Ошибка означает, что модель изменилась, а таблица в файле базы — нет: db.create_all() создаёт только недостающие таблицы, ALTER TABLE он не делает. В учебном проекте удалите файл базы и создайте заново. В проекте с живыми данными используйте миграции: flask db migrate -m "add rating" и flask db upgrade через Flask-Migrate (Alembic).

Чем SQLite отличается от PostgreSQL и когда переходить на серверную базу?

SQLite — файл на диске без отдельного сервера: идеальна для разработки, прототипов и сайтов с редкой записью. PostgreSQL — сервер с пользователями, правами и настоящей конкурентностью. Переходите, когда появляются несколько процессов-воркеров с активной записью, длинные транзакции или требования к репликации и бэкапам.

Понравился урок? Сошлитесь на него

«Пишите add и commit парой, пока это не станет рефлексом.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по Flask

В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по Flask: 20 задач