强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Qt 与 GTK 图形框架教程 / 13 - 数据库集成 / Database Integration

数据库集成 / Database Integration

构建完整的数据库驱动应用:ORM 模式、表单验证、CRUD 操作和模型-视图绑定。 Build complete database-driven apps: ORM patterns, form validation, CRUD, and Model-View binding.


13.1 架构模式 / Architecture Patterns

分层架构 / Layered Architecture

┌─────────────────────────────────────┐
│  View Layer (UI)                    │  ← QWidget / QML / GTK Widget
│  表单、表格、按钮                    │
├─────────────────────────────────────┤
│  Controller / ViewModel Layer       │  ← 业务逻辑、数据转换
│  验证、转换、格式化                  │
├─────────────────────────────────────┤
│  Model Layer (Data)                 │  ← QAbstractTableModel
│  数据结构、CRUD 接口                 │
├─────────────────────────────────────┤
│  Repository Layer                   │  ← SQL 查询、缓存
│  数据库访问、查询构建                │
├─────────────────────────────────────┤
│  Database (SQLite / PostgreSQL)     │  ← 实际存储
└─────────────────────────────────────┘

13.2 Repository 模式 / Repository Pattern (Qt C++)

基础 Repository / Base Repository

// repository.h - 通用 Repository 基类
#ifndef REPOSITORY_H
#define REPOSITORY_H

#include <QSqlQuery>
#include <QSqlError>
#include <QVariantMap>
#include <QVariantList>
#include <QDebug>

class BaseRepository {
public:
    virtual ~BaseRepository() = default;

protected:
    bool executeQuery(const QString &sql, const QVariantList &bindings = {}) {
        QSqlQuery query;
        query.prepare(sql);
        for (const auto &val : bindings) {
            query.addBindValue(val);
        }
        if (!query.exec()) {
            qCritical() << "SQL Error:" << query.lastError().text()
                        << "Query:" << sql;
            return false;
        }
        return true;
    }

    QVariantList queryAll(const QString &sql,
                          const QVariantList &bindings = {}) {
        QVariantList results;
        QSqlQuery query;
        query.prepare(sql);
        for (const auto &val : bindings) {
            query.addBindValue(val);
        }
        if (query.exec()) {
            while (query.next()) {
                QVariantMap row;
                for (int i = 0; i < query.record().count(); ++i) {
                    row[query.record().fieldName(i)] = query.value(i);
                }
                results.append(row);
            }
        }
        return results;
    }

    QVariantMap queryOne(const QString &sql,
                         const QVariantList &bindings = {}) {
        auto results = queryAll(sql, bindings);
        return results.isEmpty() ? QVariantMap() : results.first().toMap();
    }
};

#endif // REPOSITORY_H

User Repository / 用户仓库

// userrepository.h
#ifndef USERREPOSITORY_H
#define USERREPOSITORY_H

#include "repository.h"

class UserRepository : public BaseRepository {
public:
    struct User {
        int id = 0;
        QString name;
        QString email;
        int age = 0;
        QString createdAt;
    };

    // CREATE
    int create(const User &user) {
        QSqlQuery query;
        query.prepare("INSERT INTO users (name, email, age) VALUES (?, ?, ?)");
        query.addBindValue(user.name);
        query.addBindValue(user.email);
        query.addBindValue(user.age);
        if (!query.exec()) {
            qCritical() << "Create error:" << query.lastError().text();
            return -1;
        }
        return query.lastInsertId().toInt();
    }

    // READ by ID
    User findById(int id) {
        auto row = queryOne("SELECT * FROM users WHERE id = ?", {id});
        return rowToUser(row);
    }

    // READ ALL with pagination
    QList<User> findAll(int offset = 0, int limit = 50,
                        const QString &orderBy = "id DESC") {
        QString sql = QStringLiteral(
            "SELECT * FROM users ORDER BY %1 LIMIT ? OFFSET ?")
            .arg(orderBy);
        auto rows = queryAll(sql, {limit, offset});
        QList<User> users;
        for (const auto &row : rows) {
            users.append(rowToUser(row.toMap()));
        }
        return users;
    }

    // SEARCH
    QList<User> search(const QString &keyword) {
        auto rows = queryAll(
            "SELECT * FROM users WHERE name LIKE ? OR email LIKE ? ORDER BY id DESC",
            {"%" + keyword + "%", "%" + keyword + "%"});
        QList<User> users;
        for (const auto &row : rows) {
            users.append(rowToUser(row.toMap()));
        }
        return users;
    }

    // UPDATE
    bool update(const User &user) {
        return executeQuery(
            "UPDATE users SET name=?, email=?, age=? WHERE id=?",
            {user.name, user.email, user.age, user.id});
    }

    // DELETE
    bool remove(int id) {
        return executeQuery("DELETE FROM users WHERE id=?", {id});
    }

    // COUNT
    int count() {
        auto row = queryOne("SELECT COUNT(*) as cnt FROM users");
        return row["cnt"].toInt();
    }

private:
    User rowToUser(const QVariantMap &row) {
        User u;
        u.id = row["id"].toInt();
        u.name = row["name"].toString();
        u.email = row["email"].toString();
        u.age = row["age"].toInt();
        u.createdAt = row["created_at"].toString();
        return u;
    }
};

#endif // USERREPOSITORY_H

13.3 表单验证 / Form Validation

验证器框架 / Validator Framework

// validator.h - 表单验证器
#ifndef VALIDATOR_H
#define VALIDATOR_H

#include <QString>
#include <QRegularExpression>
#include <QMap>
#include <QStringList>

class ValidationResult {
public:
    bool isValid() const { return m_errors.isEmpty(); }
    QStringList errors() const { return m_errors; }
    QString errorMessage() const { return m_errors.join("\n"); }

    void addError(const QString &field, const QString &message) {
        m_errors.append(QStringLiteral("%1: %2").arg(field, message));
    }

    void addError(const QString &message) {
        m_errors.append(message);
    }

private:
    QStringList m_errors;
};

class Validator {
public:
    // 非空验证
    static bool required(const QString &value, const QString &field,
                         ValidationResult &result) {
        if (value.trimmed().isEmpty()) {
            result.addError(field, "此字段不能为空 / This field is required");
            return false;
        }
        return true;
    }

    // 邮箱验证
    static bool email(const QString &value, const QString &field,
                      ValidationResult &result) {
        static QRegularExpression regex(
            R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)");
        if (!regex.match(value).hasMatch()) {
            result.addError(field, "邮箱格式无效 / Invalid email format");
            return false;
        }
        return true;
    }

    // 长度范围
    static bool length(const QString &value, const QString &field,
                       int min, int max, ValidationResult &result) {
        int len = value.length();
        if (len < min || len > max) {
            result.addError(field,
                QStringLiteral("长度必须在 %1 - %2 之间 / Length must be %1-%2")
                .arg(min).arg(max));
            return false;
        }
        return true;
    }

    // 数值范围
    static bool range(int value, const QString &field,
                      int min, int max, ValidationResult &result) {
        if (value < min || value > max) {
            result.addError(field,
                QStringLiteral("值必须在 %1 - %2 之间 / Value must be %1-%2")
                .arg(min).arg(max));
            return false;
        }
        return true;
    }

    // 唯一性验证(数据库检查)
    static bool uniqueEmail(const QString &email, int excludeId,
                            ValidationResult &result) {
        QSqlQuery query;
        query.prepare("SELECT COUNT(*) FROM users WHERE email = ? AND id != ?");
        query.addBindValue(email);
        query.addBindValue(excludeId);
        if (query.exec() && query.next() && query.value(0).toInt() > 0) {
            result.addError("邮箱", "该邮箱已被使用 / Email already in use");
            return false;
        }
        return true;
    }
};

#endif // VALIDATOR.h

13.4 完整 CRUD 表单 / Complete CRUD Form (Qt)

// userform.h - 带验证的 CRUD 表单
#ifndef USERFORM_H
#define USERFORM_H

#include <QWidget>
#include <QFormLayout>
#include <QLineEdit>
#include <QSpinBox>
#include <QPushButton>
#include <QMessageBox>
#include <QLabel>
#include "userrepository.h"
#include "validator.h"

class UserForm : public QWidget {
    Q_OBJECT

public:
    explicit UserForm(QWidget *parent = nullptr) : QWidget(parent) {
        auto *layout = new QFormLayout(this);
        layout->setLabelAlignment(Qt::AlignRight);

        // 错误提示
        m_errorLabel = new QLabel;
        m_errorLabel->setStyleSheet(
            "color: #e74c3c; background: #fce4ec; padding: 8px; "
            "border-radius: 4px; font-size: 12px;");
        m_errorLabel->hide();
        layout->addRow("", m_errorLabel);

        // 输入字段
        m_nameEdit = new QLineEdit;
        m_nameEdit->setPlaceholderText("请输入姓名 (2-50 字符)");
        layout->addRow("姓名 *:", m_nameEdit);

        m_emailEdit = new QLineEdit;
        m_emailEdit->setPlaceholderText("user@example.com");
        layout->addRow("邮箱 *:", m_emailEdit);

        m_ageSpin = new QSpinBox;
        m_ageSpin->setRange(0, 150);
        m_ageSpin->setValue(25);
        layout->addRow("年龄:", m_ageSpin);

        // 按钮
        auto *btnLayout = new QHBoxLayout;
        m_submitBtn = new QPushButton("保存 / Save");
        m_submitBtn->setObjectName("submitButton");
        auto *clearBtn = new QPushButton("清空 / Clear");

        btnLayout->addStretch();
        btnLayout->addWidget(clearBtn);
        btnLayout->addWidget(m_submitBtn);
        layout->addRow("", btnLayout);

        // 连接信号
        connect(m_submitBtn, &QPushButton::clicked, this, &UserForm::onSubmit);
        connect(clearBtn, &QPushButton::clicked, this, &UserForm::onClear);

        // 实时验证(输入时清除错误)
        connect(m_nameEdit, &QLineEdit::textChanged, this, [this]() {
            m_errorLabel->hide();
            m_nameEdit->setStyleSheet("");
        });
        connect(m_emailEdit, &QLineEdit::textChanged, this, [this]() {
            m_errorLabel->hide();
            m_emailEdit->setStyleSheet("");
        });
    }

    void loadUser(int userId) {
        m_editingId = userId;
        auto user = m_repo.findById(userId);
        if (user.id > 0) {
            m_nameEdit->setText(user.name);
            m_emailEdit->setText(user.email);
            m_ageSpin->setValue(user.age);
            m_submitBtn->setText("更新 / Update");
        }
    }

signals:
    void userSaved();

private slots:
    void onSubmit() {
        // 验证
        ValidationResult validation;
        Validator::required(m_nameEdit->text(), "姓名", validation);
        Validator::length(m_nameEdit->text(), "姓名", 2, 50, validation);
        Validator::required(m_emailEdit->text(), "邮箱", validation);
        Validator::email(m_emailEdit->text(), "邮箱", validation);
        Validator::range(m_ageSpin->value(), "年龄", 0, 150, validation);

        if (!validation.isValid()) {
            m_errorLabel->setText(validation.errorMessage());
            m_errorLabel->show();
            return;
        }

        // 构建用户对象
        UserRepository::User user;
        user.id = m_editingId;
        user.name = m_nameEdit->text().trimmed();
        user.email = m_emailEdit->text().trimmed();
        user.age = m_ageSpin->value();

        bool success;
        if (m_editingId > 0) {
            success = m_repo.update(user);
        } else {
            success = m_repo.create(user) > 0;
        }

        if (success) {
            QMessageBox::information(this, "成功", "操作成功完成");
            onClear();
            emit userSaved();
        } else {
            m_errorLabel->setText("保存失败,请重试");
            m_errorLabel->show();
        }
    }

    void onClear() {
        m_nameEdit->clear();
        m_emailEdit->clear();
        m_ageSpin->setValue(25);
        m_errorLabel->hide();
        m_editingId = 0;
        m_submitBtn->setText("保存 / Save");
    }

private:
    QLineEdit *m_nameEdit;
    QLineEdit *m_emailEdit;
    QSpinBox *m_ageSpin;
    QPushButton *m_submitBtn;
    QLabel *m_errorLabel;
    UserRepository m_repo;
    int m_editingId = 0;
};

#endif // USERFORM_H

13.5 Python 集成示例 / Python Integration Example

"""数据库 CRUD + 表单验证 - PySide6 + SQLite"""

import re
import sqlite3
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
    QLineEdit, QSpinBox, QPushButton, QTableView,
    QLabel, QMessageBox, QGroupBox
)
from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtGui import QStandardItemModel, QStandardItem


@dataclass
class User:
    id: int = 0
    name: str = ""
    email: str = ""
    age: int = 0


class ValidationResult:
    def __init__(self):
        self.errors: list[str] = []

    @property
    def is_valid(self) -> bool:
        return len(self.errors) == 0

    def add_error(self, field_name: str, message: str):
        self.errors.append(f"{field_name}: {message}")

    @property
    def error_message(self) -> str:
        return "\n".join(self.errors)


class Validator:
    @staticmethod
    def required(value: str, field_name: str, result: ValidationResult) -> bool:
        if not value.strip():
            result.add_error(field_name, "此字段不能为空")
            return False
        return True

    @staticmethod
    def email(value: str, field_name: str, result: ValidationResult) -> bool:
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(pattern, value):
            result.add_error(field_name, "邮箱格式无效")
            return False
        return True

    @staticmethod
    def length(value: str, field_name: str,
               min_len: int, max_len: int, result: ValidationResult) -> bool:
        if not (min_len <= len(value) <= max_len):
            result.add_error(field_name, f"长度必须在 {min_len}-{max_len} 之间")
            return False
        return True


class UserRepository:
    def __init__(self, db_path: str = "users.db"):
        self.conn = sqlite3.connect(db_path)
        self.conn.row_factory = sqlite3.Row
        self._create_table()

    def _create_table(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL,
                age INTEGER DEFAULT 0,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        """)
        self.conn.commit()

    def create(self, user: User) -> int:
        cur = self.conn.execute(
            "INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
            (user.name, user.email, user.age))
        self.conn.commit()
        return cur.lastrowid

    def find_all(self, limit: int = 100, offset: int = 0) -> list[User]:
        rows = self.conn.execute(
            "SELECT * FROM users ORDER BY id DESC LIMIT ? OFFSET ?",
            (limit, offset)).fetchall()
        return [User(r["id"], r["name"], r["email"], r["age"]) for r in rows]

    def find_by_id(self, user_id: int) -> Optional[User]:
        row = self.conn.execute(
            "SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
        return User(row["id"], row["name"], row["email"], row["age"]) if row else None

    def update(self, user: User) -> bool:
        self.conn.execute(
            "UPDATE users SET name=?, email=?, age=? WHERE id=?",
            (user.name, user.email, user.age, user.id))
        self.conn.commit()
        return True

    def delete(self, user_id: int) -> bool:
        self.conn.execute("DELETE FROM users WHERE id=?", (user_id,))
        self.conn.commit()
        return True


class UserCrudWidget(QWidget):
    """完整 CRUD 界面"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.repo = UserRepository()
        self.editing_id = 0
        self._setup_ui()
        self._load_data()

    def _setup_ui(self):
        layout = QHBoxLayout(self)

        # 左:表单
        form_group = QGroupBox("用户信息")
        form = QFormLayout(form_group)

        self.error_label = QLabel()
        self.error_label.setStyleSheet(
            "color: #e74c3c; background: #fce4ec; padding: 8px; border-radius: 4px;")
        self.error_label.hide()
        form.addRow("", self.error_label)

        self.name_edit = QLineEdit()
        self.name_edit.setPlaceholderText("请输入姓名 (2-50 字符)")
        form.addRow("姓名 *:", self.name_edit)

        self.email_edit = QLineEdit()
        self.email_edit.setPlaceholderText("user@example.com")
        form.addRow("邮箱 *:", self.email_edit)

        self.age_spin = QSpinBox()
        self.age_spin.setRange(0, 150)
        form.addRow("年龄:", self.age_spin)

        btn_layout = QHBoxLayout()
        self.save_btn = QPushButton("保存")
        self.save_btn.clicked.connect(self._on_save)
        clear_btn = QPushButton("清空")
        clear_btn.clicked.connect(self._on_clear)
        btn_layout.addStretch()
        btn_layout.addWidget(clear_btn)
        btn_layout.addWidget(self.save_btn)
        form.addRow("", btn_layout)

        form_group.setFixedWidth(300)
        layout.addWidget(form_group)

        # 右:表格
        right = QVBoxLayout()

        self.search_edit = QLineEdit()
        self.search_edit.setPlaceholderText("搜索...")
        right.addWidget(self.search_edit)

        self.table = QTableView()
        self.model = QStandardItemModel()
        self.model.setHorizontalHeaderLabels(["ID", "姓名", "邮箱", "年龄"])
        self.table.setModel(self.model)
        self.table.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
        self.table.setAlternatingRowColors(True)
        self.table.clicked.connect(self._on_row_clicked)
        right.addWidget(self.table)

        # 表格操作按钮
        table_btns = QHBoxLayout()
        del_btn = QPushButton("删除选中")
        del_btn.clicked.connect(self._on_delete)
        table_btns.addStretch()
        table_btns.addWidget(del_btn)
        right.addLayout(table_btns)

        layout.addLayout(right)

    def _load_data(self):
        self.model.removeRows(0, self.model.rowCount())
        for user in self.repo.find_all():
            row = [
                QStandardItem(str(user.id)),
                QStandardItem(user.name),
                QStandardItem(user.email),
                QStandardItem(str(user.age)),
            ]
            self.model.appendRow(row)

    @Slot()
    def _on_save(self):
        result = ValidationResult()
        Validator.required(self.name_edit.text(), "姓名", result)
        Validator.length(self.name_edit.text(), "姓名", 2, 50, result)
        Validator.required(self.email_edit.text(), "邮箱", result)
        Validator.email(self.email_edit.text(), "邮箱", result)

        if not result.is_valid:
            self.error_label.setText(result.error_message)
            self.error_label.show()
            return

        user = User(
            id=self.editing_id,
            name=self.name_edit.text().strip(),
            email=self.email_edit.text().strip(),
            age=self.age_spin.value()
        )

        if self.editing_id > 0:
            self.repo.update(user)
        else:
            self.repo.create(user)

        self._on_clear()
        self._load_data()
        QMessageBox.information(self, "成功", "操作成功完成")

    @Slot()
    def _on_delete(self):
        idx = self.table.currentIndex()
        if not idx.isValid():
            return
        user_id = int(self.model.item(idx.row(), 0).text())
        if QMessageBox.question(self, "确认", "确定要删除吗?") == QMessageBox.StandardButton.Yes:
            self.repo.delete(user_id)
            self._load_data()

    @Slot()
    def _on_clear(self):
        self.name_edit.clear()
        self.email_edit.clear()
        self.age_spin.setValue(0)
        self.error_label.hide()
        self.editing_id = 0
        self.save_btn.setText("保存")

    @Slot()
    def _on_row_clicked(self, index):
        row = index.row()
        self.editing_id = int(self.model.item(row, 0).text())
        self.name_edit.setText(self.model.item(row, 1).text())
        self.email_edit.setText(self.model.item(row, 2).text())
        self.age_spin.setValue(int(self.model.item(row, 3).text()))
        self.save_btn.setText("更新")

注意事项 / Important Notes

⚠️ 永远参数化查询 / Always Parameterize

// ❌ 错误:SQL 注入风险
query.exec("SELECT * FROM users WHERE name = '" + name + "'");
// ✅ 正确:参数化
query.prepare("SELECT * FROM users WHERE name = ?");
query.addBindValue(name);

⚠️ 事务使用 / Use Transactions 批量操作务必使用事务。SQLite 的无事务批量插入极慢。

⚠️ 连接管理 / Connection Management 长期运行的应用需定期检查连接状态,处理断连重连。


扩展阅读 / Further Reading

资源 / Resource链接 / Link
Qt SQL 文档https://doc.qt.io/qt-6/sql-programming.html
SQLite 文档https://www.sqlite.org/docs.html
Repository 模式https://martinfowler.com/eaaCatalog/repository.html

12 - 主题与样式 | 14 - UI 测试