在开发MFC桌面应用时,数据库选型往往让人纠结。我经历过用Access的卡顿,也试过MySQL的繁琐配置,直到遇到SQLite3才发现这才是桌面应用的绝配。这个只有几百KB的轻量级数据库,却有着惊人的能量。
SQLite3最大的优势就是零配置。不像其他数据库需要安装服务端,它直接把整个数据库存储在单个文件中。我曾经做过一个客户管理系统,直接把.db文件放在程序目录下就能用。客户电脑上不需要安装任何额外组件,真正实现了"开箱即用"。
性能方面也让我惊喜。在测试插入10万条记录时,SQLite3比Access快了近5倍。它的原子提交和回滚机制保证了数据完整性,我在断电测试中从未遇到过数据损坏的情况。对于中小型应用,这样的表现完全够用。
与MFC的配合更是天作之合。由于SQLite3提供的是C语言API,和MFC的CString等类型转换非常方便。我经常在项目中直接用CString拼接SQL语句,通过简单的类型转换就能与SQLite3交互。下面这个例子展示了如何快速建立连接:
cpp复制sqlite3* db;
CString dbPath = _T("C:\\MyApp\\data.db");
if (sqlite3_open(CT2A(dbPath), &db) == SQLITE_OK) {
AfxMessageBox(_T("数据库连接成功"));
// 后续操作...
}
新手最容易卡在第一步。我推荐直接从官网下载合并版本的sqlite-amalgamation包,这个版本把所有源码合并成单个.c和.h文件,集成到项目特别方便。下载后只需要做三件事:
记得勾选项目属性的"使用多字节字符集",避免后续出现字符编码问题。有次我忘记设置这个选项,调试了整整一下午的乱码问题。
创建数据库文件时有些细节需要注意。我习惯在应用启动时检查数据库是否存在,不存在就自动创建并初始化表结构:
cpp复制CString dbFile = GetExePath() + _T("\\userdata.db");
if (!PathFileExists(dbFile)) {
sqlite3* db;
if (sqlite3_open(CT2A(dbFile), &db) == SQLITE_OK) {
char* errMsg;
const char* sql = "CREATE TABLE users("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL,"
"age INTEGER,"
"phone TEXT);";
if (sqlite3_exec(db, sql, NULL, NULL, &errMsg) != SQLITE_OK) {
AfxMessageBox(CString("创建表失败:") + errMsg);
sqlite3_free(errMsg);
}
sqlite3_close(db);
}
}
这里GetExePath()是我封装的获取程序路径的函数。注意SQLite3默认使用UTF-8编码,所以要用CT2A宏将CString转换为ANSI字符串。
插入数据看似简单,但有些坑我不得不提醒。首先是参数绑定,新手喜欢直接拼接SQL字符串,这很容易导致SQL注入。正确的做法是使用预处理语句:
cpp复制sqlite3_stmt* stmt;
const char* sql = "INSERT INTO users (name, age, phone) VALUES (?, ?, ?)";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
CString name = _T("张三");
int age = 25;
CString phone = _T("13800138000");
sqlite3_bind_text(stmt, 1, CT2A(name), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 2, age);
sqlite3_bind_text(stmt, 3, CT2A(phone), -1, SQLITE_TRANSIENT);
if (sqlite3_step(stmt) != SQLITE_DONE) {
AfxMessageBox(_T("插入数据失败"));
}
sqlite3_finalize(stmt);
}
SQLITE_TRANSIENT参数告诉SQLite在需要时复制字符串,避免指针失效问题。我曾经因为忽略这个参数导致随机内存错误,调试起来非常痛苦。
查询数据时,我推荐使用回调函数方式。下面这个例子将查询结果显示在MFC的ListCtrl中:
cpp复制int CALLBACK QueryCallback(void* pListCtrl, int colCount, char** colValues, char** colNames) {
CListCtrl* pList = (CListCtrl*)pListCtrl;
int nIndex = pList->InsertItem(pList->GetItemCount(), CString(colValues[0]));
for (int i = 1; i < colCount; i++) {
pList->SetItemText(nIndex, i, CString(colValues[i]));
}
return 0;
}
void CUserDlg::OnBtnQuery() {
CString sql = _T("SELECT * FROM users WHERE age > ?");
char* errMsg;
int minAge = 20;
char* sqlStr = CT2A(sql);
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db, sqlStr, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, minAge);
while (sqlite3_step(stmt) == SQLITE_ROW) {
CString name = CA2T((char*)sqlite3_column_text(stmt, 1));
int age = sqlite3_column_int(stmt, 2);
CString phone = CA2T((char*)sqlite3_column_text(stmt, 3));
int nIndex = m_list.InsertItem(m_list.GetItemCount(), name);
m_list.SetItemText(nIndex, 1, CString(age));
m_list.SetItemText(nIndex, 2, phone);
}
sqlite3_finalize(stmt);
}
}
MFC程序使用Unicode字符集,而SQLite3使用UTF-8,这中间的转换是个大坑。我封装了两个转换函数:
cpp复制CString UTF8ToCString(const char* utf8Str) {
int len = MultiByteToWideChar(CP_UTF8, 0, utf8Str, -1, NULL, 0);
wchar_t* wstr = new wchar_t[len];
MultiByteToWideChar(CP_UTF8, 0, utf8Str, -1, wstr, len);
CString result(wstr);
delete[] wstr;
return result;
}
void CStringToUTF8(const CString& str, CStringA& utf8Str) {
int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
char* buffer = new char[len];
WideCharToMultiByte(CP_UTF8, 0, str, -1, buffer, len, NULL, NULL);
utf8Str = buffer;
delete[] buffer;
}
批量操作时一定要用事务,否则性能会让你怀疑人生。我曾经测试过,开启事务后插入1000条记录从5秒降到0.1秒:
cpp复制BOOL CUserDB::BatchInsert(const CArray<UserInfo>& users) {
BOOL bSuccess = FALSE;
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL);
sqlite3_stmt* stmt;
const char* sql = "INSERT INTO users (name, age) VALUES (?, ?)";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
for (int i = 0; i < users.GetSize(); i++) {
CStringA name;
CStringToUTF8(users[i].name, name);
sqlite3_bind_text(stmt, 1, name, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 2, users[i].age);
if (sqlite3_step(stmt) != SQLITE_DONE) {
sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
break;
}
sqlite3_reset(stmt);
}
bSuccess = TRUE;
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
}
sqlite3_finalize(stmt);
return bSuccess;
}
记得每次循环都要调用sqlite3_reset()重置语句状态,否则下次绑定会失败。这个细节坑过不少开发者。