Flask для школьниковГлава 8 из 10
SilverTests.ru · Курс веб-разработкиБаза данных SQLite
База данных SQLite
Привычки теперь переживут перезапуск сервера
Ты умеешь делать Flask-сервер с маршрутами, формами, JSON-API и JavaScript с polling. Твой трекер работает, но данные исчезают при перезапуске.
Цель главы
Подключить к трекеру привычек базу данных SQLite. После перезапуска сервера привычки останутся на месте. Никаких новых библиотек — SQLite встроен в Python.
Что изменится
Было: habits_list = [...] — Python-список в памяти. Перезапуск = потеря данных.
Станет: файл habits.db на диске. Перезапуск = данные целы.
1Проблема: данные в памяти
Вспомни трекер. Где хранятся привычки?
habits_list = [
{"name": "Бег", "done": True},
{"name": "Чтение", "done": False},
]
Обычная переменная в оперативной памяти. Ctrl+C или перезагрузка — всё исчезает. Как записал домашку на запотевшем зеркале.
Решение: записывать данные на диск. Для этого есть базы данных.
2Что такое SQLite
SQLite — база данных в одном файле. Не нужен отдельный сервер, не нужно устанавливать — модуль sqlite3 уже встроен в Python.
Список Python (было)
[{"name":"Бег", "done":True}]
# Хранение: RAM
# Перезапуск: теряются
# Поиск: перебор
SQLite (станет)
SELECT * FROM habits;
-- Хранение: файл habits.db
-- Перезапуск: целы
-- Поиск: мгновенный
SQLite используют повсюду: Android, iOS, Chrome (история, куки), Telegram (сообщения), Firefox, Skype. Самая распространённая БД в мире.
3Таблицы, строки, столбцы
База данных — набор таблиц. Каждая таблица — как Excel: столбцы и строки.
Для трекера нужна одна таблица habits:
| id |
name |
done |
| 1 |
Бег |
1 |
| 2 |
Чтение |
0 |
| 3 |
Йога |
1 |
id — уникальный номер (автоматический). name — текст. done — число: 1 = да, 0 = нет (в SQLite нет Boolean).
4Четыре команды SQL
CREATE TABLE — создать таблицу
CREATE TABLE IF NOT EXISTS habits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
done INTEGER DEFAULT 0
);
IF NOT EXISTS — не падать, если уже есть. AUTOINCREMENT — id сам. NOT NULL — имя обязательно. DEFAULT 0 — по умолчанию не выполнена.
INSERT — добавить
INSERT INTO habits (name) VALUES ('Бег');
SELECT — прочитать
SELECT * FROM habits; -- все
SELECT * FROM habits WHERE done = 1; -- выполненные
SELECT * FROM habits WHERE id = 3; -- одна по id
UPDATE — изменить
UPDATE habits SET done = 1 WHERE id = 2;
Важно: всегда указывай WHERE в UPDATE и DELETE! Без него команда изменит ВСЕ строки.
5SQLite из Python
import sqlite3
# 1. Подключиться (файл создастся автоматически)
conn = sqlite3.connect('habits.db')
# 2. Выполнить SQL
cur = conn.cursor()
cur.execute('SELECT * FROM habits')
# 3. Получить результат
rows = cur.fetchall() # [(1,'Бег',1), (2,'Чтение',0)]
# 4. Закрыть
conn.close()
Важно: после INSERT/UPDATE/DELETE нужен conn.commit() — иначе не сохранится!
cur.execute("INSERT INTO habits (name) VALUES (?)", ("Йога",))
conn.commit() # без этого — данные не запишутся!
Почему (?), а не f-строка? Это параметризованный запрос — защита от SQL-инъекций. Если пользователь введёт '; DROP TABLE habits; --, с f-строкой база удалится. С ? — нет.
6Подключаем к Flask
Здесь начинается самое важное. Разберём почему код написан именно так.
Где писать код? Всё пишем в том же файле app.py, где уже живёт Flask-сервер. Никаких новых файлов создавать не нужно. Просто добавляем import sqlite3 в начало и вспомогательные функции перед маршрутами.
Почему мы создаём функцию get_db(), а не просто пишем conn = sqlite3.connect(...) один раз в начале файла?
Представь: 5 учеников одновременно открыли твой трекер. Flask обрабатывает их запросы параллельно — как 5 кассиров в магазине, каждый обслуживает своего покупателя. Если у всех кассиров одна касса на пятерых — будет очередь и путаница. Поэтому каждому кассиру нужна своя касса.
То же самое с базой данных: если все запросы используют одно соединение — они будут мешать друг другу. Поэтому каждый запрос открывает своё собственное соединение, работает с ним и закрывает.
import sqlite3
DATABASE = 'habits.db'
def get_db():
"""Открыть своё соединение с БД для этого запроса."""
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row # строки как словари
return conn
Что тут происходит по строкам:
DATABASE = 'habits.db' — имя файла базы данных. Если файла нет — Python создаст его автоматически при первом подключении.
sqlite3.connect(DATABASE) — открываем соединение. Это как «открыть файл для работы». Быстрая операция.
conn.row_factory = sqlite3.Row — без этой строки результат SELECT выглядит так: (1, 'Бег', 1) — просто кортеж, непонятно что есть что. С этой строкой: row['name'], row['done'] — как словарь, по имени столбца. Гораздо удобнее.
7Создание таблицы при старте
Вопрос: когда создавать таблицу? Нельзя же при каждом запросе пользователя — это глупо. Один раз при установке — но как?
Ответ: напишем функцию init_db() и вызовем её прямо в app.py. Она выполнится один раз при запуске сервера (python app.py). Никаких отдельных файлов — всё в том же app.py.
def init_db():
"""Создать таблицу, если её ещё нет."""
conn = get_db()
conn.execute("""
CREATE TABLE IF NOT EXISTS habits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
done INTEGER DEFAULT 0
)
""")
conn.commit()
conn.close()
# Эта строка выполнится при запуске python app.py
init_db()
Почему это безопасно вызывать каждый раз?
- Первый запуск: файла
habits.db нет → Python создаёт файл → создаёт таблицу внутри. Всё пусто, готово к работе.
- Второй запуск: файл
habits.db уже есть, таблица уже есть → IF NOT EXISTS видит это и ничего не делает. Данные не трогает. Привычки на месте.
- Сотый запуск: то же самое — ничего не ломает.
Это стандартный приём: «создай если нет, не трогай если есть».
Частый вопрос: «А нужно ли создавать отдельный файл init.py и запускать его один раз?» Нет! Для учебного проекта всё живёт в app.py. Отдельные файлы для инициализации БД — это практика больших проектов. Нам пока не нужно.
8Переписываем маршруты
Теперь самое интересное: заменяем habits_list на SQL-запросы. Логика каждого маршрута одинаковая:
- Открыть соединение —
conn = get_db()
- Выполнить запрос —
conn.execute('SQL ...')
- Если что-то изменили (INSERT, UPDATE, DELETE) — сохранить:
conn.commit()
- Закрыть соединение —
conn.close()
Почему commit()? SQLite не записывает изменения на диск сразу. Он накапливает их в памяти и ждёт команды «сохранить». Это как в Word: ты печатаешь текст, но пока не нажмёшь Ctrl+S — файл не обновится. commit() — это Ctrl+S для базы данных.
Почему close()? Открытое соединение держит файл базы заблокированным. Если забыть закрыть — другие запросы не смогут получить доступ. Это как занять единственный туалет и забыть выйти.
GET /habits — показать список
@app.route('/habits')
def habits():
conn = get_db()
rows = conn.execute('SELECT * FROM habits').fetchall()
conn.close()
return render_template('habits.html', habits=rows)
В шаблоне ничего менять не надо: sqlite3.Row поддерживает {{ habit['name'] }}.
POST /add — добавить
@app.route('/add', methods=['POST'])
def add_habit():
name = request.form['habit_name']
if name.strip():
conn = get_db()
conn.execute('INSERT INTO habits (name) VALUES (?)', (name,))
conn.commit()
conn.close()
return redirect('/habits')
POST /toggle — переключить
@app.route('/toggle/<int:habit_id>', methods=['POST'])
def toggle(habit_id):
conn = get_db()
habit = conn.execute(
'SELECT done FROM habits WHERE id = ?', (habit_id,)
).fetchone()
if habit:
conn.execute(
'UPDATE habits SET done = ? WHERE id = ?',
(0 if habit['done'] else 1, habit_id))
conn.commit()
conn.close()
return redirect('/habits')
9API-маршруты тоже
GET /api/habits
@app.route('/api/habits')
def api_habits():
conn = get_db()
rows = conn.execute('SELECT * FROM habits').fetchall()
conn.close()
habits = [
{"id": r["id"], "name": r["name"],
"done": bool(r["done"])}
for r in rows
]
return jsonify({"habits": habits, "count": len(habits)})
bool(r["done"]) превращает 0/1 из SQLite в true/false в JSON.
POST /api/add
@app.route('/api/add', methods=['POST'])
def api_add():
data = request.json
name = data.get('name', '')
if name.strip():
conn = get_db()
conn.execute('INSERT INTO habits (name) VALUES (?)', (name,))
conn.commit()
conn.close()
return jsonify({"ok": True})
return jsonify({"ok": False, "msg": "Пустое имя"})
10Проверяем
Запусти, добавь привычки, отметь одну. Затем:
- Ctrl+C — сервер остановится.
python app.py — запусти заново.
- Открой
/habits — привычки на месте!
Рядом с app.py появился файл habits.db. Один файл — вся информация.
Как заглянуть в базу: DB Browser for SQLite (бесплатная) или в терминале:
sqlite3 habits.db и SQL-команды.
11Паттерн: connect, execute, commit, close
# 1. Открыть
conn = get_db()
# 2. Выполнить
conn.execute('...', (...,))
# 3. Если меняем — сохранить
conn.commit()
# 4. Закрыть
conn.close()
Типичные ошибки:
- Забыл commit — данные вроде есть, но при перезапуске пропали.
- Забыл close — БД заблокирована, запросы не пройдут.
- f-строка вместо ? — уязвимость к SQL-инъекции!
- Забыл запятую —
(name) это скобки, (name,) это кортеж.
12Сводка изменений
| Что |
Было (список) |
Стало (SQLite) |
| Хранение |
RAM |
Файл habits.db |
| Перезапуск |
Теряются |
Сохранены |
| Получить все |
habits_list |
SELECT * FROM habits |
| Добавить |
.append({...}) |
INSERT INTO ... |
| Изменить |
habits[i][...]= |
UPDATE ... SET ... |
| Библиотека |
нет |
sqlite3 (встроена) |
13Задание: добавь своих фичи
Трекер работает, данные сохраняются. Теперь сделай его по-настоящему удобным. Попробуй реализовать другие функции, описанные ниже.
Правило: каждая фича — это маршрут + SQL-запрос + (иногда) кусочек HTML. Всё, что ты уже умеешь. Подсказки внизу каждого задания — только направление, не готовый код.
Уровень 1 — базовый
Удаление привычки
Рядом с каждой привычкой — кнопка «Удалить». По нажатию привычка исчезает из базы навсегда.
Что понадобится:
- Новый маршрут
POST /delete/<int:habit_id>
- SQL:
DELETE FROM habits WHERE id = ?
- В шаблоне: маленькая форма с кнопкой рядом с каждой привычкой
Подсказка: не забудь WHERE id = ?. Без WHERE удалишь ВСЕ привычки!
Счётчик прогресса
Вверху страницы — полоска прогресса и текст: «Выполнено 3 из 7 (43%)».
Что понадобится:
- В маршруте
/habits: два SQL-запроса — общее количество и количество выполненных
- SQL:
SELECT COUNT(*) FROM habits и SELECT COUNT(*) FROM habits WHERE done = 1
- Передать числа в шаблон:
total=..., done_count=...
- В HTML:
<progress value="{{ done_count }}" max="{{ total }}">
Подсказка: можно сделать красивее через CSS-полоску с процентом внутри.
Уровень 2 — средний
Редактирование имени
Клик на имя привычки — открывается поле ввода с текущим именем. Можно изменить и сохранить.
Что понадобится:
- Новая страница (или маршрут)
GET /edit/<int:habit_id> — форма с текущим именем
- Маршрут
POST /edit/<int:habit_id> — сохранение
- SQL:
UPDATE habits SET name = ? WHERE id = ?
Подсказка: можно без отдельной страницы — через JavaScript и fetch на /api/edit.
Дата добавления
Каждая привычка показывает, когда была создана: «Бег — добавлено 15 апреля».
Что понадобится:
- Новый столбец в таблице:
created_at TEXT
- При INSERT:
INSERT INTO habits (name, created_at) VALUES (?, datetime('now'))
- В шаблоне:
{{ habit['created_at'] }}
Подсказка: если таблица уже существует, нужен ALTER TABLE habits ADD COLUMN created_at TEXT или удали habits.db и запусти сервер заново.
Поиск и фильтрация
Вверху страницы — поле поиска. Ввёл «бег» — показываются только привычки с «бег» в названии. Кнопки «Все / Выполненные / Невыполненные».
Что понадобится:
- Параметры в URL:
/habits?q=бег или /habits?filter=done
- В Python:
request.args.get('q', '')
- SQL с LIKE:
SELECT * FROM habits WHERE name LIKE ? с параметром f'%{q}%'
- Три ссылки-фильтра в шаблоне
Подсказка: для LIKE передавай ('%' + q + '%',) через параметр, не через f-строку!
Уровень 3 — продвинутый
Приоритеты (перетаскивание)
Добавить столбец position INTEGER. Привычки выводятся по порядку. Кнопки «вверх» и «вниз» меняют позицию.
Что понадобится:
- Столбец
position INTEGER DEFAULT 0
SELECT * FROM habits ORDER BY position
- Маршрут
POST /move/<int:habit_id>/<direction> — поменять позиции двух соседних записей
Подсказка: при перемещении вверх надо поменять position текущей записи и записи выше. Это два UPDATE в одной транзакции.
Экспорт в CSV
Кнопка «Скачать CSV» — браузер скачивает файл habits.csv с таблицей привычек.
Что понадобится:
- Маршрут
GET /export
- Модуль
io.StringIO и csv.writer
- Вернуть ответ с заголовком
Content-Type: text/csv и Content-Disposition: attachment; filename=habits.csv
- Использовать
flask.Response для кастомного ответа
Подсказка: это новое — придётся загуглить «flask return csv file». Хорошая практика самостоятельного поиска.