Хранение инвентаря в SQLite
HTML-таблица уже показывает модель оборудования, но исходный срез неудобен как постоянное хранилище. Любое изменение требует редактирования программы. В этом уроке перенесём карточки в SQLite и будем читать их через database/sql. Таблица, артикулы и смысл количества сохраняются; меняется граница получения данных. Теперь сервер должен учитывать открытие соединения, ошибку SQL и прекращение чтения результата.
SQLite хранит учебные данные в файле inventory.sqlite внутри выбранного состояния проекта. Для доступа используем Go-драйвер modernc.org/sqlite версии 1.60.1. Он не требует отдельного запущенного сервера базы, однако остаётся внешней зависимостью модуля. В архиве нет установленного пакета и готовой пользовательской базы. При самостоятельном прохождении читатель загружает зависимости и получает собственный файл данных.
Ограничения таблицы
В таблице items внутренний ID является первичным ключом, SKU имеет ограничение уникальности, строки названия и места обязательны, количество проверяется условием quantity>=0. Значение версии по умолчанию равно единице. Здесь база повторяет часть предметного договора из второго урока. Это полезно даже при корректной форме: данные могут попадать в хранилище через будущий импорт, служебную программу или новую функцию, которая ещё не прошла проверку интерфейса.
NOT NULL не запрещает пустую строку. Это разные состояния: отсутствующее значение SQL и существующая строка длиной ноль. Поэтому база на данном шаге не заменяет будущую проверку формы. Мы фиксируем ограничения, необходимые для согласованности хранения, и не обещаем, что любой возможный прямой INSERT автоматически соответствует всем требованиям интерфейса. Полноту предметных правил нужно оценивать отдельно для каждого пути записи.
Инициализация использует CREATE TABLE IF NOT EXISTS и две начальные строки с INSERT OR IGNORE. Их ID и SKU фиксированы. Повторное открытие учебной базы не должно добавлять ещё две копии оборудования. Однако этот способ не является общей системой миграций: изменение SQL исходника не преобразует уже существующие столбцы. Если позже меняется схема, понадобится версия миграции и явный переход, а не надежда на повторный вызов создания таблицы.
func openStore()(*sql.DB,error){
db,err:=sql.Open("sqlite","inventory.sqlite?_pragma=busy_timeout(3000)&_pragma=foreign_keys(1)")
if err!=nil{return nil,err}
db.SetMaxOpenConns(1)
if _,err=db.Exec(`CREATE TABLE IF NOT EXISTS items(
id INTEGER PRIMARY KEY,sku TEXT NOT NULL UNIQUE,name TEXT NOT NULL,
location TEXT NOT NULL,quantity INTEGER NOT NULL CHECK(quantity>=0),version INTEGER NOT NULL DEFAULT 1);
INSERT OR IGNORE INTO items(id,sku,name,location,quantity) VALUES
(1,'KB-001','Клавиатура','офис',12),(2,'MON-001','Монитор','склад',6); `);err!=nil{db.Close();return nil,err}
return db,nil
}
Приведённый блок находится в main.go состояния lesson-04; полный файл и отдельный view.html входят в архив. sql.Open создаёт объект доступа, а выполнение команд и запросов обнаруживает ошибки конкретного соединения. Поэтому отсутствие ошибки только после вызова Open не подтверждает, что таблица готова. Наше создание схемы служит первой реальной операцией и возвращает ошибку вызывающему коду, если подготовка не удалась.
Пул и время запроса
Объект sql.DB представляет пул соединений, а не одну навсегда открытую SQL-сессию. Для небольшого локального стенда устанавливаем SetMaxOpenConns(1). Это ограничивает конкуренцию и упрощает дальнейшие транзакции. Ограничение не заменяет согласованности: два последовательных запроса по-прежнему могут прочитать старую версию и попытаться сохранить противоречивые изменения. Для этого понадобятся условия обновления и транзакция на следующих шагах.
В строке подключения указан busy_timeout(3000). Когда SQLite временно занята другой записью, ожидание не должно становиться бесконечным. При этом SQL-таймаут, контекст запроса и таймаут HTTP-сервера имеют разные причины срабатывания. В обработчиках используем QueryContext(r.Context(), ...): отмена обращения получает путь к операции базы. Не следует смешивать все виды ожидания в одно сообщение «страница долго грузится».
Файл базы привязан к рабочей папке процесса. Если запустить тот же исходник из другого каталога, можно получить другой inventory.sqlite и решить, что данные пропали. На самом деле приложение открыло другой путь. Поэтому команды серии выполняются из конкретного lesson-NN. Перед переносом настоящих данных нужно отдельно выбрать постоянное место хранения и сделать резервную копию; копирование папок исходников не определяет судьбу пользовательской базы.
Чтение строк и представление
Обработчик списка выполняет SELECT с явным набором столбцов и ORDER BY id. Порядок значений в Scan должен совпадать с порядком столбцов запроса. Использование SELECT * делает этот договор менее заметным и зависимым от изменения схемы. Именованные столбцы читаются чуть длиннее, зато позволяют увидеть, какое поле модели получает конкретное значение хранилища и почему версия карточки не теряется при выдаче.
Результат запроса содержит итератор строк. Его нужно закрыть, а ошибку проверить как после Scan, так и после окончания Next. Отрицательный результат Next означает либо завершение данных, либо прекращение чтения из-за ошибки. Без rows.Err() эти ситуации могут выглядеть одинаково, и сервер выдаст неполную таблицу как правильный список. Сначала собираем модель страницы, затем выполняем шаблон — ошибка базы не должна превращаться в тихое исчезновение последних карточек.
Ожидаемая выдача сохраняет две знакомые строки. Новое наблюдаемое свойство — источник данных теперь находится в файле базы и сохраняется между самостоятельными запусками. Это описание следует из устройства программы; сервер и база при подготовке урока не запускались. В терминале ошибка содержит подробности для разработчика, а посетителю отправляется короткое сообщение без внутреннего пути и текста SQL. Разные аудитории требуют разной формы диагностики.
Попробуйте изменить начальное количество в исходном INSERT и представить повторный запуск уже заполненной базы. Существующая строка не обязана обновиться: команда игнорирует конфликт ключа. Такой пример объясняет отличие начальных данных от изменения пользовательского состояния. В следующем уроке добавим явный POST-запрос для создания новой карточки, а затем научимся возвращать ошибки формы так, чтобы человек не терял свой ввод.
Подключение драйвера выполнено импортом с подчёркиванием. Нам не нужен отдельный символ из его пакета: требуется регистрация реализации, которую стандартный пакет найдёт по имени sqlite. Если убрать этот импорт, наличие строки подключения не поможет выбрать неизвестный драйвер. Такой механизм полезно отличать от открытия базы: регистрация происходит при инициализации программы, а конкретное хранилище выбирается позже. В большом приложении драйвер обычно подключают на границе запуска, чтобы внутренние предметные функции зависели от договора репозитория, а не от способа регистрации конкретного SQL-пакета.
Имя файла хранилища не является адресом страницы: браузер получает HTML от процесса, а не прямой доступ к SQLite. Поэтому публиковать файл базы вместе со статическими материалами курса не требуется и не следует. Исходники и пользовательские данные остаются разными видами содержимого.
Исходники урока
Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-04. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.
Первичные источники: документация 1.