在 C 中访问 SQLite
在我上一篇关于在 C/C++ 中访问 PostgreSQL 数据库的文章中,我写了关于在 C/C++ 中访问 PostgreSQL 数据库的内容。在本文中,我将讨论在 C 中针对 SQLite 数据库执行相同的功能。与 Postgresql 及其支持库不同,SQLite 创建完全独立的数据库,这些数据库不依赖于客户端-服务器架构。此功能使 SQLite 非常适合在不需要服务器复杂性,但可以从 SQL 语言的灵活性中获益的应用程序中使用。
与上一篇文章一样,我们需要一个数据库来使用,我们将使用上次使用的同一个数据库
create table people ( id integer, firstname varchar(20), lastname varchar(20), phonenumber char(10) ); insert into people (id, firstname, lastname, phonenumber) values (1, 'Fred', 'Flintstone', '5055551234'); insert into people (id, firstname, lastname, phonenumber) values (2, 'Wilma', 'Flintstone', '5055551234'); insert into people (id, firstname, lastname, phonenumber) values (3, 'Barny', 'Rubble', '5055554321');
令人欣慰的是看到与 Postgresql 一起使用的相同语法也适用于 SQLite。事实上,SQLite 支持 SQL 92 标准的很多内容。
假设这些 SQL 语句在一个名为 create.sql 的文件中,我们将使用如下命令创建数据库并填充数据:
sqlite3 ljdata.sl3 < create.sql
SQLite 网站建议使用 “sl3” 扩展名来表示我们正在使用 SQLite 版本 3 数据库。
如果我们使用 strings 命令检查生成的文件 ljdata.sl3,我们会看到不仅所有数据都在那里,而且创建表结构所需的所有信息也在那里。
# strings < ljdata.sl3 SQLite format 3 atablepeoplepeople CREATE TABLE people ( id integer, firstname varchar(20), lastname varchar(20), phonenumber char(10) BarnyRubble5055559999 WilmaFlintstone5055551234 FredFlintstone5055551234
所以就像在上一篇文章中一样,现在是时候看看一些代码了。
1 #include <stdio.h> 2 #include <sqlite3.h> 3 4 int main(void) { 5 sqlite3 *conn; 6 sqlite3_stmt *res; 7 int error = 0; 8 int rec_count = 0; 9 const char *errMSG; 10 const char *tail; 11 12 error = sqlite3_open("ljdata.sl3", &conn); 13 if (error) { 14 puts("Can not open database"); 15 exit(0); 16 } 17 18 error = sqlite3_exec(conn, 19 "update people set phonenumber=\'5055559999\' where id=3", 20 0, 0, 0); 21 22 error = sqlite3_prepare_v2(conn, 23 "select lastname,firstname,phonenumber,id from people order by id", 24 1000, &res, &tail); 25 26 if (error != SQLITE_OK) { 27 puts("We did not get any data!"); 28 exit(0); 29 } 30 31 puts("=========================="); 32 33 while (sqlite3_step(res) == SQLITE_ROW) { 34 printf("%s|", sqlite3_column_text(res, 0)); 35 printf("%s|", sqlite3_column_text(res, 1)); 36 printf("%s|", sqlite3_column_text(res, 2)); 37 printf("%u\n", sqlite3_column_int(res, 3)); 38 39 rec_count++; 40 } 41 42 puts("=========================="); 43 printf("We received %d records.\n", rec_count); 44 45 sqlite3_finalize(res); 46 47 sqlite3_close(conn); 48 49 return 0; 50 }
在这里,我尝试重现上一篇文章程序中的所有功能,但正如您将看到的,这指出了两个库之间的一个重要区别。
编译此程序只需像这样链接 sqlite3 库即可
gcc ./db.c -o db -lsqlite3
第 1-10 行只是样板声明和定义。我将 main 声明为 main(void) 是因为这是一个演示,并非旨在接受任何命令行参数。
在第 12-16 行,我们开始与数据库交互。在这里我们看到,我已经打开了当前目录中的数据库文件 ljdata.sl3。我们传入一个指向 sqlite3 结构指针的指针,以便 sqlite3_open 函数可以分配内存并为我们填充结构。当函数返回时,此变量包含指向此结构的指针。此函数还会返回一个错误值,我们在第 13 行中对其进行了测试。
第 18-20 行执行一个简单的数据库更新,就像在上一篇文章中一样。在这里,sqlite3_exec() 接收我们在 sqlite3_open() 函数中讨论过的 conn 变量,当然还有要执行的 SQL 语句。但您还会注意到 3 个附加参数。第三个参数是指向回调函数的指针,该回调函数将为 SQL 查询返回的每一行调用。第四个参数是指向将成为回调的第一个参数的指针(如果我们选择使用该功能)。能够使用回调来处理查询返回的行很好,但对于这个只对数据库执行更新的特定查询来说,这有点过分了。
sqlite3_exec() 函数的最后一个参数是我作弊的地方。此参数是指向 char 数组的指针,旨在包含错误消息,以防我们的查询生成错误。同样,这是一个简单的查询,我不希望出现任何错误。在生产代码中,我会更加谨慎一些。
第 22-29 行的事情变得更有趣了。这些行开始了几乎所有其他数据库访问库都实现的准备、执行、循环、关闭范例。顺便说一句,有趣的是,sqlite3_exec() 函数实际上是一个便利函数,它是根据我们将在本程序其余部分讨论的函数实现的。
因此,我们首先通过在第 22 行调用 sqlite3_prepare_v2() 来准备我们的查询以供执行。这会导致 SQLite 库将我们的查询编译成字节码以供稍后执行;我们的查询此时不会运行。
直观上,您可能会期望此函数被称为 sqlite3_prepare() 之类的名称,这指出了 SQLite 和大多数其他数据库库之间的一个区别。API 多年来经历了一些变化。SQLite 开发人员没有通过细微更改库的行为方式来破坏现有代码,而是简单地添加到 API,同时保持向后兼容性。因此,实际上存在一个 sqlite3_prepare() 函数,但它的行为与 “v2” 函数略有不同。“v2” 函数是新代码的推荐函数。
我们对 sqlite3_prepare_v2() 的调用接受连接句柄和一个 SQL 查询字符串,正如我们所期望的那样。但是,sqlite3_prepare_v2() 调用可用于在同一查询字符串中准备多个 SQL 语句,因此我们传入查询字符串的最大长度,很像 strncmp() 函数。接下来,我们传入一个指向 sqlite3_stmt 指针的指针。此指针将包含一个 “语句” 对象,我们稍后将使用它来获取查询结果,就像我们在 Postgresql 版本中所做的那样。
最后,我们传入一个 “tail” 指针,sqlite3_prepare_v2() 将使用它来在我们的查询中保持它的位置,以防我们的查询字符串包含多个 SQL 语句。这肯定比像 strtok() 那样破坏查询字符串要好。
在第 26-29 行中,我们检查错误并在出现任何问题的迹象时立即无情地退出。
第 31 行指出了 Postgresql 版本和此程序的 SQLite 版本之间的一个细微差别。在 Postgresql 版本中,我们能够确定我们的查询检索了多少条记录,而无需像在 SQLite 版本中那样循环遍历数据集。事实证明,SQLite 版本实际上并没有一次检索所有数据。相反,我们有一个类似于游标的东西,我们必须循环遍历它才能一次获取一行结果。
我们在第 33 行中使用重复调用 sqlite3_step() 循环遍历此 “游标”;每次调用 sqlite3_step() 函数都会返回 SQLITE_ROW 常量,直到我们到达数据集的末尾。当这种情况发生时,我们的循环终止。
在 while 循环内部,我们看到了与其他常见数据库访问库的另一个偏差。在这里,我们看到了特定类型的 “列访问器” 函数。在第 34 行,看到了对 sqlite3_column_text() 的调用。此函数接受我们的语句对象,以及一个整数,用于指定我们要检索的列。正如您所看到的,前 3 列 0-2 是文本、char 或 varchar 值。在第 37 行,我们看到了几乎相同的对 sqlite_column_int() 的调用,以便从我们的数据集中获取整数值的 id 字段。
我们在第 39 行中计算我们检索的记录,以便我们可以重现上一篇文章程序的所有功能。
第 42-43 行结束报告并打印记录计数。
第 45 行对 sqlite3_finalize() 的调用返回我们的语句对象正在使用的任何资源。最后,我们在第 47 行调用 sqlite3_close() 关闭我们的数据库连接。
奇迹般地,这个程序的代码行数几乎与等效的 Postgresql 版本完全相同!
而且,就像上次一样,输出看起来像这样。
# ./db ========================== Flintstone|Fred|5055551234|1 Flintstone|Wilma|5055551234|2 Rubble|Barny|5055559999|3 ========================== We received 3 records.
我发现 Postgresql 库比 SQLite 库更平易近人。Postgresql 网站上有示例代码。SQLite 网站有很多写得很好的文档,但由于 API 的更改,它一开始有点令人却步。但正如您所看到的,这两个库都不难使用。每个库都解决了另一个库可能无法很好解决的特定数据库访问问题。对于客户端-服务器模型有意义的大型数据库,Postgresql 库是首选。另一方面,对于客户端或独立的数据库,SQLite 非常简洁。无论哪种方式,这对全家人来说都很有趣!