База данных в 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 — самая развёрнутая СУБД в мире: она живёт в каждом смартфоне, внутри браузеров и в десятках миллиардов устройств. Если вы ищете базу, проверенную производством сильнее всех остальных, — это один файл на диске.
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()
('Марина', 'Лучший раф в городе')Это «голый» 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 / DELETE | add() / query / присваивание / delete() |
| Опечатка в SQL всплывает при запросе | Ошибка в модели видна на старте приложения |
Ставится расширение одной командой в уже знакомое виртуальное окружение:
(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
Подключение к приложению и модель Entry
Подключение — две строки: адрес базы в конфигурации (URI) и создание объекта db. Для SQLite URI выглядит как sqlite:///guestbook.db:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///guestbook.db"
db = SQLAlchemy(app)
Теперь сердце урока — модель. Опишем запись гостевой книги классом Entry:
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.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.90db.Boolean— истина и ложь: опубликован, оплаченdb.DateTime— дата и время: created_at, published_at
Когда в уроке 9 появится модель пользователя, этот список дополнится колонкой с хешем пароля — той же String, только содержимое не светится даже вам. А сейчас создадим таблицу.
db.create_all(): превращаем класс в таблицу
Класс модели — только описание. Чтобы в файле базы появилась настоящая таблица, вызовите db.create_all(). И здесь на всех новичков ждёт один и тот же сюрприз: SQLAlchemy требует контекст приложения.
with app.app_context():
db.create_all()
print("Таблицы созданы")
Таблицы созданы
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
CRUD, буква R: читаем записи
За чтение отвечает Query-объект: Entry.query — это выборка всех записей таблицы, которую фильтруют и сортируют цепочкой методов. Вот самые ходовые приёмы на заполненной базе:
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 Кофе холодный, а обслуживание на высоте
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() | первая запись или None | LIMIT 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 — SQLAlchemy сама превратит это в UPDATE с условием WHERE id = 2. С удалением так же. В веб-маршрутах поиск по ключу удобно делать через db.get_or_404(Entry, 2): вернёт запись или честную страницу 404 — вместо ветвления с проверкой на None после get.
Гостевая книга целиком: связываем форму с базой
Теперь соединим оба урока: форма из урока 4 посылает POST, мы создаём Entry, сохраняем и перенаправляем гостя на страницу со списком. Полный 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>
Порядок действий важен: 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=[]))
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() на пустой базе?
Словари вместо SQLAlchemy: напишите функцию filter_by(items, **kwargs), которая возвращает только те записи, у которых ВСЕ пары kwargs совпадают, — как настоящий Query. Отфильтруйте посты по автору «Марина» и напечатайте их id и текст.
Как подключить базу данных к 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Flask · Урок 4
Формы в Flask: приём данных от пользователя
Четвёртый урок курса Flask: сайт начинает слушать гостя. HTML-формы, request.form и request.args, серверная валидация, flash-сообщения и защита от повторной отправки формы.
Flask · Урок 6
Сессии, куки и flash-сообщения во Flask: память о пользователе
Шестой урок курса Flask: session как словарь, куки set_cookie, секретный ключ и flash-сообщения. Учим приложение помнить пользователя между запросами.
Flask · Урок 10
Проект: блог на Flask с постами и комментариями
Финальный проект курса: блог на Flask с постами, комментариями и пагинацией. Модели, CRUD-маршруты, наследование шаблонов, деплой на PythonAnywhere и разбор каждого решения.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
FastAPI · Урок 7
Подключаем базу данных SQLite к FastAPI
Словарь задач из урока 5 умирает при перезапуске. Ставим на его место SQLite через SQLAlchemy: движок, сессии, Depends и CRUD, который переживает uvicorn --reload.
fastapi база данныхsqlalchemy fastapi
aiogram · Урок 6
База данных в телеграм-боте: SQLite от первого лица
Даём боту настоящую память: таблица пользователей с chat_id, запись через INSERT OR IGNORE, чтение по chat_id и параметр ? против SQL-инъекций.
sqlite телеграм боттелеграм бот база данных
Flask · Урок 9
Регистрация и вход: аутентификация на Flask
Девятый урок курса Flask: модель User, хеширование паролей, маршруты register/login/logout и декоратор login_required. Демо хеширования sha256 с солью запускается прямо на странице.
регистрация и авторизация flaskflask login пример входа пользователя
Проверьте знания по Flask
В челлендже — 20 задач по Flask, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.
Тест по Flask: 20 задач