65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C++ 中开始使用 SQLite

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (9投票s)

2009年6月24日

CPOL

13分钟阅读

viewsIcon

93814

downloadIcon

2723

在本文中,我将分享我使用 SQLite 创建和编译一个简单的 C++ 程序来存储数据的经验。

引言

存储数据是大多数程序的重要组成部分,在我发现 SQLite 之前,我曾经花费大量开发时间来编写文件处理代码。大约一年前,我发现了一个名为 SQLite 的库。它就像一个微型数据库,您可以将其嵌入到您的程序中。这不仅节省了时间,而且还允许您使用 SQL 来访问正确的数据。

在本文中,我将展示我如何

  • 下载 SQLite
  • 编写一个 SQLite 处理程序类
  • 编写一个执行某些操作的简单 SQLite 程序
  • 下载一个查看 SQLite 数据库的程序

背景

已经有许多关于将 SQLite 编译到 C++ 代码中的教程,而且并不难做到。我写这篇文章是为了解释我的方法并展示我使用的工具。

遵循本教程的先决条件

我创建了一篇 CodeProject 文章,其中完整地展示了我如何设置我用于编译 C++ 程序的工具链。可在此处找到:opensource_tool.aspx

下载 SQLite

要获取 SQLite,请访问:http://www.sqlite.org/download.html 并下载 amalgamation。 (我下载的是sqlite-amalgamation-3_6_14_2.zip。)

SQLite 以源代码文件的形式提供,我们可以将其作为程序构建过程的一部分进行编译。为此,我们必须设置我们的项目。启动 MSYS 并按照以下命令操作

  • cd /c/code/
  • mkdir sqlite_hello_world

将 zip 文件的内容提取到此目录。 (我使用了 7-zip。我们不需要 sqlite3ext.h 文件,所以将其删除。

SQLite 以可以直接编译到应用程序中的代码形式提供,因此接下来我们应该创建一个 makefile 并检查我们是否可以将 SQLite 编译成一个对象。 (您可以从网页复制命令,并使用 Shift + Ins 将命令粘贴到 MSYS 中。)

  • “/c/program files/notepad++/notepad++.exe” /c/code/sqlite_hello_world/makefile
  • 粘贴以下代码,然后保存并关闭 Notepad++
$(warning Starting Makefile)

CXX=g++

sqlite3.obj: sqlite3.c
    gcc -c sqlite3.c -o sqlite3.obj -DTHREADSAFE=1
    
clean: 
    -rm *.obj

现在,从 Notepad++ 菜单中选择 Run -> RUN_CODE,我们在先决条件教程中已创建此菜单。如果一切顺利,您将看到类似以下的输出

C:\Program Files\Notepad++>c:

C:\Program Files\Notepad++>cd\

C:\>cd c:\code\sqlite_hello_world

C:\code\sqlite_hello_world>make
makefile:1: Starting Makefile
gcc -c sqlite3.c -o sqlite3.obj -DTHREADSAFE=1

C:\code\sqlite_hello_world>pause
Press any key to continue . . .

C:\code\sqlite_hello_world>main.exe
'main.exe' is not recognized as an internal or external command,
operable program or batch file.

C:\code\sqlite_hello_world>pause
Press any key to continue . . .

我们还没有创建 main.exe,所以不用担心错误。如果您查看目录,您会看到 sqlite3.obj

注意:如果您想了解更多关于 SQLite 的信息,请访问网站:http://www.sqlite.org/

使用 SQLite 处理程序类

我现在要稍微“作弊”一下,使用一些我事先准备好的代码。在本节中,我将只提供说明,展示如何放置代码并进行编译。在下一节中,我将深入研究我提供的代码,并带您了解它的功能、原因和方法。

为了帮助我创建一个 SQLite 程序,我将使用两部分代码。首先,从本文的 zip 文件中取出以下文件,并将它们放在 c:\code\sqlite_hello_world 目录中。

  • Glob_Defs.h
  • RJMFTime.h
  • RJMFTime.cpp
  • RJM_SQLite_Resultset.h
  • RJM_SQLite_Resultset.cpp

复制完文件后,对 Make 文件进行一些更改以编译所有对象。

  • “/c/program files/notepad++/notepad++.exe” /c/code/sqlite_hello_world/makefile
  • 粘贴以下代码,然后保存并关闭 Notepad++
$(warning Starting Makefile)

CXX=g++

main.exe: sqlite3.obj RJM_SQLite_Resultset.obj RJMFTime.obj
    $(warning main bit not done)

sqlite3.obj: sqlite3.c
    gcc -c sqlite3.c -o sqlite3.obj -DTHREADSAFE=1
    
RJM_SQLite_Resultset.obj: RJM_SQLite_Resultset.cpp 
          RJM_SQLite_Resultset.h Glob_Defs.h RJMFTime.h sqlite3.h
    $(CXX) -c RJM_SQLite_Resultset.cpp -o RJM_SQLite_Resultset.obj

RJMFTime.obj: RJMFTime.cpp RJMFTime.h
    $(CXX) -c RJMFTime.cpp -o RJMFTime.obj
    
clean: 
    -rm *.obj
    -rm *.exe

这个新的 Make 文件展示了一些 Make 的特性。当您键入 Make 而不带任何参数时,Make 会尝试创建文件中找到的第一个对象。在这种情况下,第一部分什么也不做,因为我们还没有准备好创建 .exe (我们还没有程序)。我把它放在那里是为了列出它的依赖项。这个列表 (sqlite3.obj, RJM_SQLite_Resultset.obj, RJMFTime.obj) 告诉 Make 它需要创建这三个文件。如何在 makefile 程序中创建这些文件的说明。

通过单击 Notepad++ 菜单中的 RUN_CODE 来测试这一点,我们在先决条件文章中已创建该菜单。输出应如下所示

C:\Program Files\Notepad++>c:

C:\Program Files\Notepad++>cd\

C:\>cd C:\code\sqlite_hello_world

C:\code\sqlite_hello_world>make
makefile:1: Starting Makefile
gcc -c sqlite3.c -o sqlite3.obj -DTHREADSAFE=1
g++ -c RJM_SQLite_Resultset.cpp -o RJM_SQLite_Resultset.obj
g++ -c RJMFTime.cpp -o RJMFTime.obj
makefile:6: main bit not done

C:\code\sqlite_hello_world>pause
Press any key to continue . . .

C:\code\sqlite_hello_world>main.exe
'main.exe' is not recognized as an internal or external command,
operable program or batch file.

C:\code\sqlite_hello_world>pause
Press any key to continue . . .

同样,忽略错误消息。

SQLite 处理程序类解释

在本节中,我们不会对我们的 hello 应用程序做任何更改。相反,我打算带您游览我们已放置并编译的代码,并展示它的作用、原因和方式。我们使用了两个独立的代码片段,我将分别解释它们。

RJMFTime

它使用以下文件

  • RJMFTime.h
  • RJMFTime.cpp

我过去使用 MFC 编程,并使用其时间类来处理日期和时间的存储。当我迁移到不依赖专有软件的代码时,我改用 Unix 时间 (time_t)。这样做的问题是它无法处理 2038 年之后的时间,而且不利于记录出生日期 (它无法追溯那么久)。然后我创建了自己的 Time 类,就是这个。我创建了广泛的文档和一个测试程序 (我曾经写过一篇关于这个的 CodeProject 文章)。

此后,我学习了 GTK+ 和 glib。这可以取代这个类。一个好主意是学习如何让本文正常工作,并了解 SQLite 的工作原理,然后简单地用 GTK+ 的等价物替换 RJMFtime。我的类不算太差,它经过了充分的测试,并且具有一些特别为使用 SQLite 时间而设计的功能。

SQLite 结果集

它使用以下文件

  • Glob_Defs.h
  • RJM_SQLite_Resultset.h
  • RJM_SQLite_Resultset.cpp

SQLite 基于 SQL 语句,每条语句都返回一个结果集。RJMSQLite Resultset 类旨在保存结果并将其转换为程序使用的数据类型。

Glob Defs 解释

数据类型对该类很重要,有一个名为 Glob_Defs.h 的包含文件,其中包含宏定义。它有我使用的 (LONG, TIMESTAMP, and VARCHAR) 数据类型的宏。每种类型都有三个定义来显示数据类型、返回类型和用于初始化的默认值。然后它包含一些标准编程函数的宏定义,SAFE_DELETEASSERT。最后,一些杂项函数,包括一个 ID 生成器。我曾在一个分布式数据库上使用它来为 ID 赋予随机数,以防止所有节点生成会冲突的记录。

Resultset 解释

RJM_SQLite_Resultset 文件包含两个类,RJM_SQLite_ResultsetRowRJM_SQLite_Resultset。该文件的一些值得注意的点包括

static RJM_SQLite_Resultset* SQL_Execute(const char* cmd, sqlite3* db) {
    RJM_SQLite_Resultset* pRS;
    pRS = new RJM_SQLite_Resultset();
    
    int rc;
    char *zErrMsg = 0;
    rc = sqlite3_exec(db, cmd, sql_callback, pRS, &zErrMsg);
    if( rc!=SQLITE_OK ){
        pRS->SetError(zErrMsg);
        sqlite3_free(zErrMsg);
    }

    return pRS;
};

这是一个静态函数,用于执行查询。它需要查询字符串和一个指向 sqlite3 对象的指针,该对象应与已打开的数据库一起设置。 (稍后我们将看到如何做到这一点。) 您可以看到它创建了结果集类的新实例,并将其分派给 SQLite 执行。它将 SQLite 的引用传递给一个通用回调函数以及新创建的类的指针。使用的回调函数在头文件中,它是

static int sql_callback(void *NotUsed, int argc, char **argv, char **azColName){
    RJM_SQLite_Resultset *pRS = (RJM_SQLite_Resultset*) NotUsed;
    return pRS->sql_callback(argc,argv,azColName);
}

我使用了一个 SQLite 允许我拥有的参数 (称为 NotUsed),其中包含我类的实例的指针。我不能直接调用我类的回调函数。类中的回调函数会创建一个新的行集类实例,并将所有数据放入它创建的类中。然后将其添加到行向量的末尾 (RJM_RSRowVector m_rvRows),它只是一个指向所有行的指针列表。这样,结果集类就包含了 SQLite 提供给我们的所有数据。

注意:此方法意味着每次调用 SQL_Execute 时,您都会获得一个指向新创建的结果集对象的指针。不要忘记删除该对象。可能使用前面描述的 SAFE_DELETE 宏。

创建一个空的 shell 程序来使用它

好的,现在我们已经成功编译了 SQLite 和我的辅助类。我们现在需要编写一个程序来实际使用它们来做一些事情。我将构建一个简单的命令行应用程序,但它也可以构建到 GUI 和其他应用程序中。首先,让我们创建一个包含所有必需头文件的 main.cpp 文件,并进行编译以确保我们做对了

  • “/c/program files/notepad++/notepad++.exe” /c/code/sqlite_hello_world/main.cpp
  • 粘贴以下代码,然后保存并关闭 Notepad++
#include <iostream>
#include "sqlite3.h"
#include "Glob_Defs.h"


int main( int argc, char *argv[] )
{
    srand ( time(NULL) );
    printf("SQLite Demo program start\n");
    printf("SQLite Demo program end\n");
    return 0;
};

不要忘记文件末尾的新行,并确保 < 和 > 被正确复制。

这是演示程序。我们需要调整 Make 文件来构建它

  • “/c/program files/notepad++/notepad++.exe” /c/code/sqlite_hello_world/makefile
  • 粘贴以下代码,然后保存并关闭 Notepad++
$(warning Starting Makefile)

CXX=g++

main.exe: sqlite_demo.lib main.cpp
    $(CXX) -s main.cpp -o main.exe -Wl,sqlite_demo.lib

sqlite_demo.lib: sqlite3.obj RJM_SQLite_Resultset.obj RJMFTime.obj
    ar cq $@ RJMFTime.obj
    ar cq $@ RJM_SQLite_Resultset.obj
    ar cq $@ sqlite3.obj
    
sqlite3.obj: sqlite3.c
    $(CXX) -c sqlite3.c -o sqlite3.obj -DTHREADSAFE=1
    
RJM_SQLite_Resultset.obj: RJM_SQLite_Resultset.cpp 
               RJM_SQLite_Resultset.h Glob_Defs.h RJMFTime.h sqlite3.h
    $(CXX) -c RJM_SQLite_Resultset.cpp -o RJM_SQLite_Resultset.obj

RJMFTime.obj: RJMFTime.cpp RJMFTime.h
    $(CXX) -c RJMFTime.cpp -o RJMFTime.obj
    
clean: 
    -rm *.obj
    -rm *.exe
    -rm *.lib

正如您所见,Make 文件变得更复杂了。它创建一系列 .obj 文件,然后将这些 .obj 文件组合成一个 .lib 文件。然后,主应用程序链接到 lib 文件。在当前阶段这可能显得有些过度,但 SQLite 的好处之一是您可以编写许多与同一数据库一起工作的不同程序。我更进一步,将我的程序开发分成两部分。有一个后端 lib 和一个前端。我将我自己的验证代码等添加到后端标准函数中,这些函数会被编译到 .lib 文件中,然后每个单独的前端都会链接到 .lib 文件。

此 Make 文件的另一个要点是,您的代码中混合了 .obj.exe.lib 文件。我计划写一篇 CodeProject 文章来展示如何将这些文件制作到不同的目录中。

单击 Run -> Run code,我们的基本应用程序将与所有正确的链接部分一起运行。

在我们的 shell 中编写一个简单的程序

我创建了一个程序,它遍历数据库的几个基本函数并显示错误检查代码。我原本打算在这里提供它并给出通常的复制到 Notepad 的说明,但文件相当长,所以我把它放在了文档 zip 文件中。从 zip 文件中复制 main.cpp,并替换您刚刚创建的 main.cpp。打开它进行查看,然后在 Notepad++ 中单击 Run -> Run Code 来执行它。它应该能完美地与您已有的 Make 文件一起编译和运行。每次运行它时,您都会看到数据库中创建了越来越多的记录。我将在这里介绍 main 函数,解释每个部分的作用。

srand ( time(NULL) );
printf("SQLite Demo program start\n");

种子随机数生成器,这是必需的,因为稍后我将使用 rand() 函数。

std::string l_filename = "datafile.sqlite";
std::ostringstream l_query;
sqlite3* l_sql_db = NULL;

声明变量。我们需要一个字符串来保存文件名。 l_query 是一个字符串流,用于构建查询。 l_sql_db 将是指向我们 SQLite 数据库的指针。

printf("Opening DB\n");
int rc = sqlite3_open(l_filename.c_str(), &l_sql_db);
if( rc ){
    sqlite3_close(l_sql_db);
    printf("Error couldn't open SQLite database %s",l_filename.c_str());
    return 1;
};

接下来,我们打开数据库。如果文件不存在,它将被创建;否则,将使用现有的文件。如果选择了无效文件,此代码不会导致错误。 SQLite 仅在您开始运行 Select 查询时才会报错。我习惯于在所有数据库中创建一个 Info 表,并让我的程序在继续之前检查它。这还允许我有一个数据文件版本,并允许程序处理不同版本的数据文件。

RJM_SQLite_Resultset *pRS = NULL;
printf("Checking if table exists\n");
pRS = SQL_Execute("SELECT name FROM sqlite_master " + 
                  "WHERE type='table' and name='simple_table';", l_sql_db);    
if (!pRS->Valid()) {
    printf("Invalid result set returned (%s)\n",pRS->GetLastError());
    SAFE_DELETE(pRS);
    sqlite3_close(l_sql_db);
    return 0;
};
rc = pRS->GetRowCount();
SAFE_DELETE(pRS);

我编写的程序只有一个表。 (您的 SQLite 表中可以有多个表。) 此代码运行一个 Select 语句,如果 simple_table 存在,它将返回一行,如果不存在,则不返回行。在检查返回的行数之前,它会检查结果集对象 (*pRS) 是否有效,如果无效则报错。然后它会检查行数。

if (0!=rc) {
    printf("Table exists\n");
} else {

如果返回一行,则表存在,我们可以跳过下一部分。否则,我们需要在继续之前创建它

printf("Table dosn't exist creating it\n");
l_query.str("");
l_query << "CREATE TABLE [simple_table] (";
l_query << "[ID] INTEGER  NOT NULL PRIMARY KEY,";
l_query << "[some_text] VARCHAR(255)  NULL,";
l_query << "[some_number] INTEGER  NULL,";
l_query << "[created] TIMESTAMP DEFAULT CURRENT_TIMESTAMP NULL";
l_query << ")";
pRS = SQL_Execute(l_query.str().c_str(), l_sql_db);    
if (!pRS->Valid()) {
    printf("Invalid result set returned (%s)\n",pRS->GetLastError());
    SAFE_DELETE(pRS);
    sqlite3_close(l_sql_db);
    return 0;
};

现在我们创建表。与所有数据库一样,这只是通过运行 SQL 语句并根据返回的信息进行操作来完成的。我使用 l_query 来创建 SQL 语句。

printf("Checking if table exists\n");
pRS = SQL_Execute("SELECT name FROM sqlite_master WHERE " + 
                  "type='table' and name='simple_table';", l_sql_db);    
if (!pRS->Valid()) {
    printf("Invalid result set returned (%s)\n",pRS->GetLastError());
    SAFE_DELETE(pRS);
    sqlite3_close(l_sql_db);
    return 0;
};
rc = pRS->GetRowCount();
SAFE_DELETE(pRS);
if (0==rc) {
    printf("Error table still dosn't exist despite " + 
           "the fact I created it\n",l_sql_db);
    sqlite3_close(l_sql_db);
    return 0;
};

我重复了对表的检查,以确保它现在确实存在。

}; //End If table dosen't exist
SAFE_DELETE(pRS);

这是 Ccreate Table 部分的结尾。我添加了一个额外的 SAFE_DELETE,这并非严格必需。

printf("Add some data to the table\n");
l_query.str("");
l_query << "insert into simple_table (some_text, some_number)";
l_query << " values ('Some text'," << GEN_ID << ")";
pRS = SQL_Execute(l_query.str().c_str(), l_sql_db);    
if (!pRS->Valid()) {
    printf("Invalid result set returned (%s)\n",pRS->GetLastError());
    SAFE_DELETE(pRS);
    sqlite3_close(l_sql_db);
    return 0;
};
SAFE_DELETE(pRS);

添加数据只需执行另一个 SQL 查询。此程序中有很多重复的错误检测代码。在生产程序中,这可以隐藏在另一个函数中。

printf("Listing out some data\n");
l_query.str("");
l_query << "select ID, some_text, some_number, created FROM simple_table";
pRS = SQL_Execute(l_query.str().c_str(), l_sql_db);    
if (!pRS->Valid()) {
    printf("Invalid result set returned (%s)\n",pRS->GetLastError());
    SAFE_DELETE(pRS);
    sqlite3_close(l_sql_db);
    return 0;
};

又一个查询,这次是为了返回一些数据

rc = pRS->GetRowCount();
//Declare variables to hold data
DB_DT_LONG r_id;
DB_DT_VARCHAR r_some_text;
DB_DT_LONG r_some_number;
DB_DT_TIMESTAMP r_created;
char buf[1024] = "";

首先,获取返回的行数,然后声明我们将需要保存数据的变量。这使用了 Glob_defs.h 中声明的宏。

for (unsigned int c=0;c<rc;c++) {

遍历返回的行

pRS->GetColValueINTEGER(c, 0, &r_id);
pRS->GetColValueVARCHAR(c, 1, &r_some_text);
pRS->GetColValueINTEGER(c, 2, &r_some_number);
pRS->GetColValueTIMESTAMP(c, 3, &r_created);

调用结果集中的函数来收集数据。每种时间戳都有不同的函数。参数相同:行、列以及指向要放入数据的对象的指针。

printf("ID: %d, Text: %s, Num: %d, Created: %s\n", 
       r_id,r_some_text.c_str(),r_some_number,r_created.c_str());

这行输出数据。 DB_DT_LONG 可以直接放置,但 DB_DT_VARCHAR 需要 .c_str() 函数。我在我的时间对象中创建了一个 c_str(),它输出一个默认格式,但您可以使用不同的函数来获取特定格式。

};
SAFE_DELETE(pRS);

现在我们需要删除结果集对象

printf("Closing DB\n");
sqlite3_close(l_sql_db);

printf("SQLite Demo program end\n");
return 0;

最后,关闭数据库并结束程序。

下载一个查看 SQLite 数据库的程序

我们可以编写许多使用同一数据库的程序,其他人也可以。我使用一个名为 SQLite Administrator 的工具来查看我在编程和调试过程中创建的文件内容。正如您可能期望的那样,还有 SQLite ODBC 驱动程序,它们可以很好地将您的数据导入 Excel 或其他应用程序。

使用 SQLite Administrator 的另一个原因是,您可以使用它来测试 SQL 语句,在将其放入程序代码之前使其正常工作。要获取它,请访问 http://sqliteadmin.orbmu2k.de/ 并下载并安装程序 (页面底部的链接)。

我将 zip 文件提取到 C:\Program Files\,并在我的快速启动栏上创建了一个快捷方式。要使用它,请运行程序,转到 Database -> Open,然后选择 c:\code\sqlite_hello_world\datafile.sqlite

输入类似以下的查询

select * from simple_table

然后,选择 Query -> Execute with result。您可以使用此工具在将查询放入应用程序之前对其进行原型设计。您甚至可以右键单击表并选择“Show SQL”来显示 Create 语句。这样,您就可以使用此程序创建对象并将创建脚本复制到您的代码中。它不仅是一个简单的文件存储系统,而且您还可以使用数据库的所有功能:表、视图等。

使用说明

使用 Edit Data 时,在单击 Edit Data 选项卡后,您需要单击表名才能使其刷新。然后您需要持续单击表名来刷新数据。

出于某种原因,如果我在 SQLite Administrator 中创建数据库文件,它在我的代码中访问它时不会起作用。作为一种变通方法,我让我的程序创建数据库和表,它就能正常工作。管理程序访问这些数据没有任何问题。

关注点

SQLite 还有其他优点。我编写了各种程序,它们都同时使用同一个数据文件。这意味着我可以开发一套用于特定任务的简单程序,这些程序共享一个公共数据源。

您的数据文件命名约定会有所不同。有时,您可能希望人们知道这是一个 SQLite 文件,并且他们可以使用其他程序访问它。其他时候,您则不希望这样。不幸的是,在购买支持加密文件的特殊版本的 SQLite 库之前,无法限制对原始文件的访问。我建议我的用户使用 TrueCrypt 来存储他们的文件,因为在这个领域没有我的编程预算。

这个项目给我们留下了一个混乱的目录。除了我们的程序代码,我们还混合了 .obj.lib.exe 文件。Make Clean 选项可以为您清理它们,但最好将它们放在一个单独的地方。我计划创建一篇专门介绍此内容的文章。

历史

  • 2009 年 6 月 22 日 - 第一个版本。
© . All rights reserved.