Эта глава раскроет, как вернуть контроль над данными через SQL и QtSql: вы обнаружите простую логику «таблица → запрос → модель», узнаете секрет безопасной вставки данных без ручной конкатенации строк и раскроем, почему профессиональные Qt-разработчики почти всегда начинают с правильного драйвера и стратегии обновления — это экономит часы отладки и спасает релизы.
Здесь покажем 4 базовые команды SQL (CREATE/INSERT/SELECT/UPDATE+DELETE), разберём 3 уровня QtSql и потрогаем руками QSqlDatabase, QSqlQuery, QSqlTableModel и QSqlRelationalTableModel — включая prepare()+bindValue() и стратегии OnFieldChange / OnManualSubmit.
Если пропустить эту главу, легко пропустить и тот самый момент, когда «простая база» превращается в источник багов.
И да: в конце вас ждёт ссылка, где можно скачать архив со всеми исходниками примеров (готовыми к компиляции) и 16 бесплатных глав — чтобы сразу закрепить материал на практике.
Самопроверка по главе
Что такое первичный ключ и почему он важен для таблицы базы данных?Ответ
Правильный ответ: Первичный ключ — это уникальный идентификатор записи, который может состоять из одного или нескольких столбцов. Он гарантирует уникальность каждой записи и используется для связывания таблиц между собой.
Какие три уровня классов предоставляет модуль QtSql и за что каждый из них отвечает?Ответ
Правильный ответ: Уровень драйверов (физический доступ к данным), программный уровень (API для обращения к БД через классы типа QSqlDatabase и QSqlQuery) и уровень пользовательского интерфейса (модели для отображения данных в представлениях).
В чем разница между выборкой и проекцией в команде SELECT?Ответ
Правильный ответ: Выборка — это выбор строк таблицы (определяется в WHERE), а проекция — это выбор столбцов (указывается после SELECT). Вместе они формируют результирующую таблицу с нужными данными.
Зачем проверять результат выполнения метода QSqlDatabase::open()?Ответ
Правильный ответ: Метод может вернуть false при неудачном подключении (неверные учетные данные, недоступность БД, проблемы с драйвером). Проверка позволяет своевременно обнаружить ошибку и получить детали через lastError().
Почему использование prepare() и bindValue() предпочтительнее прямой подстановки данных в SQL-запрос?Ответ
Правильный ответ: Эти методы защищают от SQL-инъекций, автоматически экранируют специальные символы и обеспечивают корректную типизацию данных. Кроме того, подготовленные запросы могут выполняться быстрее при многократном использовании.
Почему при передаче приложения клиентам необходимо включать файлы расширений драйверов БД?Ответ
Правильный ответ: По умолчанию драйверы БД подключаются к Qt как plug-ins и не включаются автоматически в исполняемый файл. Без них приложение не сможет подключиться к базе данных на системе клиента.
Почему класс QSqlQueryModel предназначен только для чтения данных?Ответ
Правильный ответ: Модель запроса отображает результат произвольного SELECT-запроса, который может содержать JOIN, агрегацию и вычисляемые поля. Такие данные не всегда можно однозначно записать обратно в исходные таблицы.
В каких случаях SQLite предпочтительнее серверных СУБД типа MySQL или PostgreSQL?Ответ
Правильный ответ: SQLite идеальна для локальных данных одного пользователя, встраиваемых приложений и мобильных устройств. Она не требует установки отдельного сервера, драйвер включен в Qt6 по умолчанию, и БД хранится в одном файле.
Почему при стратегиях редактирования OnFieldChange и OnRowChange можно вставить только одну строку за раз?Ответ
Правильный ответ: Эти стратегии автоматически сохраняют изменения при смене ячейки или строки. При вставке нескольких пустых строк первая же попытка перехода вызовет сохранение незаполненной записи, что может нарушить ограничения БД.
Какую стратегию редактирования следует выбрать, если нужен полный контроль над моментом сохранения изменений?Ответ
Правильный ответ: Стратегию OnManualSubmit. Данные сохраняются только при вызове слота submitAll(), а отменить изменения можно через revertAll(). Это удобно для реализации функций «Сохранить» и «Отменить».
Как отобразить только определенные столбцы таблицы при использовании QSqlTableModel?Ответ
Правильный ответ: Нужно вызвать метод removeColumn() для каждого столбца, который не должен отображаться. Модель по умолчанию загружает все столбцы, поэтому ненужные необходимо явно скрыть.
Что произойдет, если забыть вызвать метод select() после setTable() в QSqlTableModel?Ответ
Правильный ответ: Модель не выполнит запрос к базе данных и останется пустой. Метод select() фактически загружает данные из указанной таблицы в модель — без него представление будет пустым.
В каком сценарии использование QSqlRelationalTableModel предпочтительнее QSqlTableModel?Ответ
Правильный ответ: Когда нужно отображать данные из связанных таблиц через внешние ключи. Например, вместо ID статуса показывать его текстовое описание из справочника. Реляционная модель автоматически выполняет JOIN и отображает читаемые значения.
Практические задания
Простой уровень
Записная книжка с SQLite
Создайте Qt-приложение с базой данных SQLite для хранения личных заметок. Таблица должна содержать поля: id (первичный ключ), title (заголовок), content (текст заметки), created_at (дата создания). Реализуйте добавление новой заметки и отображение всех заметок в QTableView.
Подсказки: Используйте QSqlTableModel для простого отображения. Добавьте в проектный файл QT += sql. Для вставки используйте insertRow() и setData(). Не забудьте вызвать submitAll() для сохранения изменений. Установите стратегию редактирования через setEditStrategy().
Средний уровень
Библиотека с поиском и фильтрацией
Разработайте приложение для учета книг с двумя таблицами: books (id, title, author, year, genre_id) и genres (id, name). Используйте QSqlRelationalTableModel для отображения жанра по названию вместо ID. Добавьте QLineEdit для поиска по автору или названию через setFilter() и QComboBox для фильтрации по жанрам. Реализуйте возможность редактирования данных с подтверждением сохранения.
Подсказки: Используйте setRelation() для связи с таблицей genres. Для поиска формируйте WHERE-условие с LIKE. Примените стратегию OnManualSubmit с кнопками «Сохранить»/«Отменить», вызывающими submitAll()/revertAll(). Обрабатывайте ошибки через lastError().
Сложный уровень
Система управления проектами с задачами
Создайте полнофункциональное приложение для управления проектами с тремя связанными таблицами: projects (id, name, description, status_id), tasks (id, project_id, title, description, priority, completed), statuses (id, name, color). Реализуйте: иерархическое представление проектов и задач, drag-and-drop для изменения приоритета задач, фильтрацию по статусу и выполнению, экспорт выбранных данных в CSV, валидацию при вводе (проверка обязательных полей), индикацию несохраненных изменений.
Подсказки: Используйте QTreeView с кастомной моделью, наследованной от QSqlRelationalTableModel. Для drag-and-drop переопределите методы dropMimeData() и mimeData(). Реализуйте флаг «грязных» данных при изменениях. Для экспорта пройдитесь по выбранным строкам через selectionModel(). Валидацию реализуйте через переопределение setData(). Примените транзакции (database().transaction(), commit(), rollback()) для атомарности операций.
💬 Присоединяйтесь к обсуждению!
Разобрались с модулем QtSql и тремя уровнями его классов? Возникли вопросы о выборе между QSqlQueryModel и QSqlTableModel?
Поделитесь своим опытом работы с разными СУБД, расскажите о подводных камнях при использовании реляционных моделей или задайте вопросы об оптимизации SQL-запросов!