DataProvider - 一个用于从C++处理数据库的静态库





5.00/5 (4投票s)
一个可用于各种数据库相关需求的静态库
引言
在我们 Secured Globe, Inc. 的工作中,我们经常处理数据库,主要是 SQLite。本文的目的是创建一个静态库来处理常见的数据库相关任务,其中包括:
我需要一个静态库,以便在任何我们想要将数据库集成到代码中时都可以使用它。其想法是能够做到以下几点:
- 创建临时数据库(如果尚不存在)。
- 用默认值填充数据库。
- 通过运行 SQL 查询来查找给定字段的值。
- 根据字段的存在与否,更新或插入一个新值到给定表中的给定字段。
- 处理设置,作为一种特殊类型的表,用于(读取和写入)程序的设置。
- 将数据从数据库读取到内存,并将数据从内存写入数据库。
函数和功能
以下是我们静态库包含的一些函数和功能。
使用 CppSQlite3U
多年来,我们使用了一个名为 CppSQlite3U 的包装器,它围绕 sqlite3 进行了封装以支持 Unicode。虽然您可以在没有它的情况下支持 Unicode,但它还提供了其他有用的函数和类,例如:CppSQLite3Exception
、CppSQLite3DB
、CppSQLite3Statement
和 CppSQLite3Query
。该库(.cpp 和 .h 文件)可以从 这里 下载。
全局变量
由于我们使用 CppSqlite3,我们定义了以下全局变量:
wchar_t DBFILENAME[1024]{ L"" }; // used to store the selected path for the database
CppSQLite3DB SG_DB_Handle; // a global handle for our database
BOOL DB_Open = FALSE; // Gives us indication if the database is open
static CCritSec SqlQueueCritSect; // Used to lock our database during processing / updating
SG_DBField 结构
我们开发了一个现代 C++ 结构,用于存储字段名及其值。为了简单起见,我们将值存储为字符串(LPCWSTR)。
首先,我们定义了字段的可能数据类型(当然,这可以更改或扩展)。
typedef enum _SG_DBDataType
{
SG_DBDataInt, // Integer
SG_DBDataReal, // Real / Double
SFDBDataString, // String
SG_DBDataBool, // Boolean
SG_DBDataErr // Error
}SG_DBDataType;
然后我们有了结构代码:
typedef struct _SG_DBField
{
LPCWSTR FieldName; // Field Name
LPCWSTR FieldValue; // Field Value (as String)
SG_DBDataType ValueType; // Field Type (SG_DBDataType)
_SG_DBField() // struct's constructors
{
ValueType = SFDBDataString;
};
_SG_DBField(LPCWSTR FN, LPCWSTR FV, SG_DBDataType FVType = SFDBDataString)
{
FieldName = FN;
FieldValue = FV;
ValueType = FVType;
};
} SG_DBField;
现在,让我们来看一个实际的例子——一个更新表中记录的函数,保持字段名和值的配对唯一。
void UpdateRecordUnique(LPCWSTR TableName, SG_DBField UniquePair, vector<SG_DBField> DBF);
进一步解释,如果您有一个停车场汽车表,您会希望将 **车牌号** 定义为一个唯一字段,限制不能有多个记录具有相同的车牌号。现在,假设我们向这个名为 **CARS** 的表中添加一辆新车。
LicensePlate: 356-RBF
Model: Chevrolet
Date: 8/15/2021 10:00 AM
我们将使用以下数据作为该函数的参数:
Tablename - _T("CARS:)
UniquePair - {_T("LicensePlate"), _T("356-RBF") }
然后是一个包含我们希望插入或更新到数据库的所有数据的向量。
UpdateRecordUnique(
L"CARS",
{
SG_DBField(L"LICENSEPLATE", L"356RBF")
},
{
SG_DBField(L"LICENSEPLATE", L"356RBF")
SG_DBField(L"MODEL", L"CHEVROLET"),
SG_DBField(L"DATE", L"8/15/2021 10:00 AM", SG_DBDataDate)
}
);
创建临时数据库
我们总是倾向于在数据库丢失时创建它(即,在程序首次运行时)。这符合我们“无需安装”的概念,允许用户只需双击即可启动软件,如果需要,软件将自行安装。为此,我们有:
void CreateDB();
此函数将从头开始创建数据库,但让我们先讨论何时调用该函数。
我们还有一个函数会先调用,它会打开数据库(如果存在),或者创建一个数据库(然后打开它)(如果不存在)。
bool OpenDB()
{
bool result = FALSE;
if (PathFileExists(DBFILENAME) == FALSE) // needs to create DB
{
CreateDB();
DBCreateDefaultSettings();
}
if (DB_Open) goto db_open;
// Open database
try
{
SG_DB_Handle.open(DBFILENAME);
}
catch (CppSQLite3Exception &e)
{
return false;
}
db_open:
DB_Open = true;
return true;
}
正如所讨论的,我们检查 DBFILENAME
是否存在,如果不存在,我们就创建一个全新的数据库。但是,如果有一个旧版本的数据库,我们想用新版本替换它呢?在这种情况下,我们使用:
#define DB_VER 4 // this number will increment from 0 whenever
// we update the structure of the database
我们如下使用 DB_VER
:
首先,让我们定义一些常量:
APP_REGISTRY_NAME <a unique name for your program>
我们将其存储在注册表中。我们读取其最后存储的值并将其与此定义(DB_VER
)进行比较,因此当数据库结构发生变化时,将创建一个新数据库,替换旧数据库。
// Load last stored DB version
WriteLogFile(_T("Get last DB version"));
LONG lResult;
HKEY hKey;
int DBVer = 0;
DWORD dwLen;
CString w_szRegPath;
// Open Registry key
w_szRegPath.Format(L"SOFTWARE\\%s\\%s", APP_REGISTRY_NAME, APP_REGISTRY_NAME);
lResult = RegOpenKeyEx(HKEY_CURRENT_USER, w_szRegPath, 0, KEY_ALL_ACCESS, &hKey);
if (lResult == ERROR_SUCCESS)
{
// Read key from Registry
dwLen = sizeof(DWORD);
lResult = RegGetValue(hKey, NULL, DBVER, RRF_RT_ANY, NULL, &DBVer, &dwLen);
if (lResult != ERROR_SUCCESS)
{
DBVer = 0;
}
}
RegCloseKey(hKey);
if (DBVer == 0)
{
// First run of our software
SG_FormatMessageBox(L"Welcome to <MY SOFTWARE> \nThis is your first run");
}
else if (DB_VER > DBVer)
{
if (PathFileExists(DBFILENAME))
{
BOOL fSuccess = FALSE;
while (-1)
{
fSuccess = DeleteFile(DBFILENAME);
if (!fSuccess) // Database may be opened by another program
{
CString Error;
Error.Format(L"Need to delete DB file '%s' Error %d.
Please close any programs that are using it", DBFILENAME, GetLastError());
SG_MessageBox(Error, L"Error", MB_OK);
}
else
break;
}
SG_MessageBox(L"Database structure has been changed,
so old DB was deleted", L"Info", MB_OK);
}
}
// Save DB Version
lResult = RegCreateKeyEx(HKEY_CURRENT_USER, w_szRegPath, 0, NULL,
REG_OPTION_NON_VOLATILE, KEY_READ | KEY_WRITE, NULL, &hKey, NULL);
if (lResult != ERROR_SUCCESS)
{
WriteLogFile(L"Error Storing DB ver in Registry (open)");
}
else
{
// set value
DBVer = DB_VER;
lResult = RegSetValueEx(hKey, DBVER, 0, REG_DWORD,
(const BYTE*)&DBVer, sizeof(DWORD));
if (lResult != ERROR_SUCCESS)
{
WriteLogFile(L"Error Storing DB ver in Registry");
}
RegCloseKey(hKey);
}
现在,让我们回到数据库的创建。为了本文的目的,假设我们想有一个名为 BATCH
的表,它看起来像这样:
我们定义一个 string
,将在每次需要创建此表时使用:
wchar_t CreateBatchTable[] = L"CREATE TABLE BATCH ( \
ID INTEGER PRIMARY KEY AUTOINCREMENT, \
DataSourceFileName TEXT NOT NULL, \
DATASOURCEID INTEGER, \
UniqueFieldName TEXT, \
DataSourceType INTEGER, \
ProcessingType INTEGER, \
Status INTEGER \
); ";
这将等同于从 DB Browser 运行以下表创建查询:
CREATE TABLE "BATCH" (
"ID" INTEGER,
"DataSourceFileName" TEXT NOT NULL,
"DATASOURCEID" INTEGER,
"UniqueFieldName" TEXT,
"DataSourceType" INTEGER,
"ProcessingType" INTEGER,
"Status" INTEGER,
PRIMARY KEY("ID" AUTOINCREMENT)
);
因此,我们的 CreateDB()
函数将如下所示:
void CreateDB()
{
WriteLogFile(L"DB Path '%s'", DBFILENAME);
SG_DB_Handle.close();
SG_DB_Handle.open(DBFILENAME);
BOOL result = TRUE;
BOOL DBCreated = FALSE;
try
{
if (!SG_DB_Handle.tableExists(L"BATCH"))
{
WriteLogFile(L"Creating table BATCH");
result = SG_DB_Handle.execDML((LPCTSTR)CreateBatchTable);
DBCreated = TRUE;
}
// Add here other code for other tables
}
catch (CppSQLite3Exception & e)
{
result = FALSE;
}
if (DBCreated)
WriteLogFile(L"Creating database %s", DBFILENAME);
return;
}
**注意** CreateDB()
可以根据其成功与否更改为返回 true
/false
。
WriteLogFile() 函数和 logfunc() 成员
WriteLogFile()
是我在 以下 文章中开发的一个函数。作为我们 DataProvider
命名空间的一部分,我们定义了以下成员,用于写入日志文件,或执行其他操作(如在屏幕上显示消息,或仅调用 wprintf()
)。为了实现这种灵活性,我们定义了以下函数数据类型:
typedef void(*logFunc)(LPCWSTR lpText, ...);
然后,我们只需将一个 logFunc
类型的成员添加到我们的命名空间中。
logFunc logfunc{ nullptr };
我们如下使用 logfunc()
:
if (logfunc) logfunc(L"SQL Error %s in query %s", e.errorMessage(), sql);
这样做,我们考虑了这样一种情况:使用我们库的程序员不想将任何内容存储在日志文件中或打印到任何地方,在这种情况下,logfunc
将等于 nullptr。
将数据存储在二维向量中
我觉得使用一个 std::wstring 的二维向量非常有用。
vector<vector<wstring>> data
我曾将它用于许多项目,主要是存储值的网格,就像 Excel 表格一样。
这里有一些有用的函数:
基于 std::wstring 的二维向量创建临时表
以下函数创建一个名为 'TEMP
' 的临时表,并将 std::wstring 的二维向量的值存储其中。我们区别对待网格的标题,假设它包含字段名。我们预定义 TABLE_TEMP
为表名(即“TEMP
”)。请注意,在我们所有的表中,都有一个通用的字段名,它用作唯一 ID,称为“UID
”。它通常是一个自动递增的整数。
例如,如果我们的表是 CLIENTS
,它将具有以下创建字符串:
CREATE TABLE "CLIENTS" (
"UID" INTEGER,
"LASTUPDATE" DATE,
"ClientID" TEXT,
"ClientName" TEXT,
PRIMARY KEY("UID" AUTOINCREMENT)
);
所以,正如您所见,UID
字段有一个特殊用途。
现在,函数本身看起来像这样:
bool CreateTempTableFromData(vector<vector<wstring>> data)
{
if (data.size() == 0) return false;
bool result = FALSE;
vector<wstring> header; // We treat the grid header differently
header = data[0];
wstring UniquePairName{ L"" }, UniquePairValue{ L"" };
wstring CreateTempTable;
if (logfunc) logfunc(L"Deleting old %s Table", TABLE_TEMP);
DeleteTable(TABLE_TEMP);
CreateTempTable = L"CREATE TABLE IF NOT EXISTS ";
CreateTempTable += TABLE_TEMP;
CreateTempTable +=L"(";
for (int i = 0; i < header.size(); i++)
{
wstring FieldType;
if (header[i] == L"UID") // Is this our Index field?
{
UniquePairName = header[i];
FieldType = L" INTEGER";
}
else
FieldType = L" TEXT";
CreateTempTable += L"\"";
CreateTempTable += header[i];
CreateTempTable += L"\"";
if (i == header.size() - 1)
CreateTempTable += FieldType + (wstring)L"); ";
else
CreateTempTable += FieldType + (wstring)L", ";
}
OpenDB();
try
{
SG_DB_Handle.execQuery(CreateTempTable.c_str());
}
catch (CppSQLite3Exception &e)
{
wstring err = e.errorMessage();
if(logfunc) logfunc(L"SQL Error: %s. Query = %s",
err.c_str(),
CreateTempTable.c_str());
return false;
}
wstring w_szSQL_insert = L"";
for (int i = 1; i < data.size(); i++)
{
vector<SG_DBField> Param;
int j;
vector<wstring>values = data[i];
for (j = 0; j < header.size(); j++)
{
if (header[j] == UniquePairName && values.size() > j)
{
UniquePairValue = values[j];
}
if (values.size() <= j)
break;
SG_DBField temp(header[j].c_str(), values[j].c_str());
Param.push_back(temp);
}
if (j == 0)
continue;
if (j < header.size())
{
for (j = values.size(); j < header.size(); j++)
{
SG_DBField temp{ header[j].c_str(), L"" };
Param.push_back(temp);
}
}
if (UniquePairName != L"")
{
CDataProvider::InsertOrUpdate(
TABLE_TEMP,
SG_DBField(UniquePairName.c_str(), UniquePairValue.c_str()),
Param.size(),
Param
);
}
else
{
CString Fields, Values;
wstring insertQuery;
MakeFieldsValues(Fields, Values, Param.size(), Param);
insertQuery = MakeInsertStatement(TABLE_TEMP, Fields, Values);
w_szSQL_insert.append(insertQuery.c_str());
w_szSQL_insert.append(L"; ");
if ((i % 100) == 0)
{
if (logfunc)
logfunc(L"%d / %d", i, data.size());
try
{
OpenDB();
string w_szaSQL_insert = w2string(w_szSQL_insert);
SQL_BEGIN_TRANSACTION;
SQL_EXECUTE(w_szaSQL_insert.c_str());
SQL_COMMIT;
}
catch (CppSQLite3Exception& e)
{
if (logfunc) logfunc(L"Error inserting record: %s\n", e.errorMessage());
Sleep(1500);
}
w_szSQL_insert.clear();
w_szSQL_insert = L"";
}
}
}
if (((data.size() - 1) % 100) != 0)
{
try
{
OpenDB();
string w_szaSQL_insert = w2string(w_szSQL_insert);
SQL_EXECUTE(w_szaSQL_insert.c_str());
}
catch (CppSQLite3Exception& e)
{
if (logfunc) logfunc(L"Error inserting record: %s\n", e.errorMessage());
Sleep(1500);
}
w_szSQL_insert.clear();
w_szSQL_insert = L"";
}
return result;
}
历史
- 2022 年 1 月 8 日:初始版本