Статья Автор: Деникина Н.В., Деникин А.В.

База данных SQLite

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-запросы. Логика каждого маршрута одинаковая:

  1. Открыть соединениеconn = get_db()
  2. Выполнить запросconn.execute('SQL ...')
  3. Если что-то изменили (INSERT, UPDATE, DELETE) — сохранить: conn.commit()
  4. Закрыть соединение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Проверяем

Запусти, добавь привычки, отметь одну. Затем:

  1. Ctrl+C — сервер остановится.
  2. python app.py — запусти заново.
  3. Открой /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». Хорошая практика самостоятельного поиска.

 

© SilverTests.ru · Курс Flask для школьников · Глава 8 из 10
Печать