从零开始学习嵌入式P35----数据库
上一篇我们把 TCP 服务器改造成了能同时接待成百上千个客户端的并发服务器。程序能"跑起来"了,可一个更现实的问题浮出水面:数据放哪儿 ?学生的成绩、单词的释义、设备的采集记录------关机就没了可不行。用文件自己存?增删改查都得自己写,数据一多就乱套。这一篇我们请出专门干这件事的软件------数据库 :先说清数据库是什么、分哪几类,然后以轻量到可以直接嵌进程序的 SQLite 为主角,学会它的命令行操作和常用 SQL 语句(建表、增、删、改、查、排序、多表联合查询),最后用三个 C 语言接口
sqlite3_open/sqlite3_exec/sqlite3_close把数据库真正用进我们的代码里。
本篇目标
学完这一篇,我们应该能够:
- 说清数据库是什么,关系型和非关系型数据库的区别;
- 会安装 SQLite,会用
.tables、.schema等常用命令行指令; - 说清 SQLite 支持的四种数据类型;
- 会写
create table、insert into、select、update、delete、order by、drop table语句; - 会用
where子句配合比较、BETWEEN、LIKE做条件查询; - 说清
cross join、inner join、outer join三种多表查询的区别; - 会用
sqlite3_open、sqlite3_exec、sqlite3_close在 C 程序里操作数据库,理解select回调函数的触发时机。
一、为什么需要数据库
数据库就是能够对大批量数据进行增、删、改、查管理的软件。 回想一下:如果没有数据库,我们想持久化保存一批学生信息,只能自己开文件、定格式、逐条读写------查"成绩 90 分以上的都有谁"得自己遍历,改一条记录得重写半个文件,两个人同时写还得自己处理冲突。这些通用又麻烦的事,数据库软件全都替我们做好了,我们只要用一句 SQL 把"想要什么"说清楚就行。
数据库分两大类:
| 分类 | 数据存放位置 | 代表产品 | 特点 |
|---|---|---|---|
| 关系型数据库 | 文件(硬盘) | Oracle、MySQL、SQLite | 数据以"表"的形式组织,表和表之间能建立关系,用 SQL 操作 |
| 非关系型数据库 | 内存(内存数据库) | Redis、NoSQL | 读写极快,常用于缓存,断电数据易失 |
嵌入式领域的首选是 SQLite:它不需要单独跑一个服务器进程,整个数据库就是一个文件,引擎直接以库的形式嵌进我们的程序里------设备上跑再合适不过。
二、SQLite 的安装与命令行
Ubuntu 下一条命令装好:
bash
sudo apt-get install sqlite3
装好后,在终端输入 sqlite3 数据库文件名 就能进入交互式命令行(文件不存在会自动创建):
bash
sqlite3 student.db
进入 sqlite> 提示符后,以点号开头的是 SQLite 自己的控制命令(不是 SQL 语句,不用加分号),常用的有:
| 命令 | 作用 |
|---|---|
.tables |
查看当前数据库里的表名称 |
.databases |
列出数据库的名称和对应文件 |
.headers on/off |
开启 / 关闭查询结果的列名显示 |
.mode |
设置查询结果的显示形式(如 column、list) |
.schema 表名 |
查看表的创建语句 |
.width |
设置各列的显示宽度 |
想让查询结果整齐好看,组合技是 .headers on + .mode column,列名和列对齐就都出来了。
三、SQL 语句:增删改查
SQL(结构化查询语言)是和关系型数据库打交道的通用语言。语句以分号结尾,关键字不区分大小写(习惯上关键字大写)。
1. SQLite 支持的数据类型
SQLite 的数据类型非常简单,记住四种就够:
INTEGER:整型;REAL:浮点型;TEXT:字符串;BLOB:二进制大块数据(图片、音频之类)。
2. create table:建表
sql
create table 表名 (列名称1 数据类型1, 列名称2 数据类型2, ...);
示例------建一张学生表:
sql
create table student (id integer, name text, sex text, age integer, score integer);
3. insert into:插入数据
sql
insert into 表名 values (值1, 值2, ...);
示例:
sql
insert into student values (1, "张三", "男", 18, 90);
insert into student values (3, "王二", NULL, NULL, NULL); -- 不确定的列填 NULL
insert into student (id, name, sex) values (4, "赵五", "男"); -- 只插指定列
注意第三句:可以只给部分列赋值,前面必须把列名写清楚,没赋值的列自动为 NULL。
4. select:查询
sql
select 列名称1, 列名称2, ... from 表名;
* 表示所有列:
sql
select * from student;
5. where:条件筛选
select 后面接 where 子句,只挑出满足条件的行。常用匹配条件:
| 条件 | 含义 |
|---|---|
= |
等于 |
<> |
不等于 |
> < >= <= |
大小比较 |
BETWEEN |
在某个范围内 |
LIKE |
模糊匹配(% 通配任意多个字符) |
示例:
sql
select name,score from student where name like "赵%"; -- 姓赵的
select name from student where score >= 90; -- 90 分以上
select name,score from student where name like "赵%" and score > 95; -- 多条件用 and
6. update:修改
sql
UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值;
示例:
sql
update student set score=91 where name="赵五";
切记带 where ------不带条件的 update 会把整张表的所有行都改掉。
7. delete:删除
sql
delete from 表名称 where 列名称 = 值;
示例:
sql
delete from student where name = "王二";
同样,where 不能省,省了就是清空整张表。
8. order by:排序
在查询末尾加 order by 列名,asc 升序(默认)、desc 降序:
sql
select name,score from student where sex="男" order by score desc;
这句的意思是:找出所有男生的姓名和成绩,按成绩从高到低排。
9. drop table:删表
sql
drop table 表名;
示例:
sql
drop table student;
drop 是把整张表连结构带数据一起删掉,和 delete(只删数据、表还在)是两回事。
四、多表联合查询
实际的数据往往分放在几张表里:学生信息一张表、课程信息一张表、成绩信息一张表。想把"谁、哪门课、考了多少分"一次查出来,就要把几张表连接 起来查,关键字是 join,共三种。
1. cross join:交叉连接
两张表做笛卡尔积------左表的每一行配右表的每一行,全部组合都列出来:
sql
sqlite> select 学生信息.姓名 as 姓名, 课程信息.名称 as 课程, 课程信息.学分 as 学分 from
...> 学生信息 cross join 课程信息;
as 是给列起别名,让结果表头更好看。
2. inner join:内连接
只保留两张表里能配对上的行------最常用地一种连接:
sql
sqlite> select 学生信息.姓名 as 姓名, 成绩信息.成绩 as 成绩 from
...> 学生信息 inner join 成绩信息 on 学生信息.ID = 成绩信息.学生ID;
on 后面写配对条件。三张表也能连着查:
sql
sqlite> select 学生信息.姓名 as 姓名, 课程信息.名称 as 课程, 成绩信息.成绩 as 成绩 from
...> 学生信息 inner join 成绩信息 on 学生信息.ID = 成绩信息.学生ID
...> inner join 课程信息 on 成绩信息.课程ID = 课程信息.ID;
3. outer join:外连接
内连接里配不上对的行会被丢掉;左外连接 (left outer join)则保证左表的行一行不少 ,右表配不上的位置填 NULL:
sql
sqlite> select 学生信息.姓名 as 姓名,成绩信息.成绩 as 成绩 from
...> 学生信息 left outer join 成绩信息 on 学生信息.ID = 成绩信息.学生ID;
一句话区分:cross 全组合,inner 只留配对成功的,left outer 左表全留、配不上补 NULL。
五、实战:C 程序操作 SQLite
命令行里敲 SQL 是给人用的;让程序自动读写数据库,得用 SQLite 提供的 C 接口。核心接口只有三个,学起来非常快。
1. sqlite3_open:打开数据库
c
int sqlite3_open(
const char *filename, /* 数据库文件名 (UTF-8) */
sqlite3 **ppDb /* OUT: 存放数据库句柄空间的首地址 */
);
- 功能:打开数据库文件,文件不存在则创建;
- 参数 :
filename是数据库文件名;ppDb是句柄的二级指针------成功后句柄写好,之后所有操作都靠它; - 返回值 :成功返回
SQLITE_OK。
2. sqlite3_exec:执行 SQL 语句
c
int sqlite3_exec(
sqlite3*, /* 数据库句柄 */
const char *sql, /* 要执行的 SQL 语句 */
int (*callback)(void*,int,char**,char**), /* 回调函数(select 时用) */
void *, /* 传给回调函数的第一个参数 */
char **errmsg /* 错误信息存放空间的首地址 */
);
- 功能:执行一条 SQL 语句------增删改查全靠它;
- 参数 :
- 第一个参数:
sqlite3_open拿到的数据库句柄; sql:SQL 语句字符串;callback:查询到结果时的处理函数 。执行select时,每查到一行数据,这个回调就被调用一次;不需要处理结果(如insert、update)时传NULL;- 第四个参数:原样透传给回调函数的第一个参数,不用传
NULL; errmsg:出错时错误信息的存放地址,用完要sqlite3_free释放;
- 第一个参数:
- 返回值 :成功返回
SQLITE_OK,失败返回错误码。
3. sqlite3_close:关闭数据库
c
int sqlite3_close(sqlite3*);
用完关闭连接,释放资源。
4. 回调函数:select 的结果从这儿出来
回调函数的原型是固定的:
c
int callbackfun(void *arg, int colnum, char **pcontent, char **ptitle);
arg:sqlite3_exec第四个参数透传过来的值;colnum:本次查询结果这一行的列数;pcontent:字符串指针数组,各列的内容;ptitle:字符串指针数组,各列的列名。
两个铁律:
select每查到一行 数据,回调就被调用一次------查回 10 行就调 10 次;- 如果
select一行都没查到,回调一次都不会被调用。
返回值必须是 0------返回非 0 会被当成出错,sqlite3_exec 会中止并报错。
5. 完整实例:建表、插入、查询
下面的程序打开 student.db,建一张学生表,插入三名学生的信息,然后把全表查出来打印。建表和插入执行过一次后就可以注释掉(重复执行会反复插入相同数据),所以用 #if 0 包起来,需要时打开:
c
#include "sqlite3.h"
#include <stdio.h>
typedef struct student
{
char name[32];
char sex[8];
int age;
int score;
}stu_t;
/********************************************
* 功能:callbackfun
* 参数:
* arg:sqlite3_exec传递的参数
* colnum:select生成的表的列数
* ptitle:列名称指针数组
* pcontent:列内容指针数组
* 返回值:
* 成功返回0
* 如果不返回0,sqlite3_exec会报错
* 注意事项:
* 1.该回调函数会随着select每找到一个数据就会被调用一次
* 2.如果select找不到匹配数据,则一次回调都不会调用
********************************************/
int callbackfun(void *arg, int colnum, char **pcontent, char **ptitle)
{
int i = 0;
for (i = 0; i < colnum; i++)
{
printf("ptitle[%d] = %s\t\t", i, ptitle[i]);
}
printf("\n");
for (i = 0; i < colnum; i++)
{
printf("pcontent[%d] = %s\t\t", i, pcontent[i]);
}
printf("\n");
return 0;
}
int main(void)
{
sqlite3 *pdb = NULL;
int ret = 0;
char cmd[1024] = {0};
char *perrmsg = NULL;
stu_t s[3] = {
{"张三", "男", 17, 90},
{"李四", "女", 16, 80},
{"王二", "男", 15, 40},
};
int i = 0;
ret = sqlite3_open("student.db", &pdb);
if (ret != SQLITE_OK)
{
fprintf(stderr, "sqlite3_open failed:%s\n", sqlite3_errmsg(pdb));
return -1;
}
#if 0
sprintf(cmd, "create table if not exists student (ID integer primary key asc, name text, sex tex, age integer, score integer);");
ret = sqlite3_exec(pdb, cmd, NULL, NULL, &perrmsg);
if (ret != SQLITE_OK)
{
fprintf(stderr, "sqlite3_exec failed:%s\n", perrmsg);
sqlite3_free(perrmsg);
return -1;
}
for (i = 0; i < 3; i++)
{
sprintf(cmd, "insert into student values (NULL, \"%s\", \"%s\", %d, %d);",
s[i].name, s[i].sex, s[i].age, s[i].score);
ret = sqlite3_exec(pdb, cmd, NULL, NULL, &perrmsg);
if (ret != SQLITE_OK)
{
fprintf(stderr, "sqlite3_exec failed:%s\n", perrmsg);
sqlite3_free(perrmsg);
return -1;
}
}
#endif
sprintf(cmd, "select * from student;");
ret = sqlite3_exec(pdb, cmd, callbackfun, NULL, &perrmsg);
if (ret != SQLITE_OK)
{
fprintf(stderr, "sqlite3_exec failed:%s\n", perrmsg);
return -1;
}
sqlite3_close(pdb);
return 0;
}
这份代码把三个接口的用法串成了标准流程,五个细节值得停下来看:
- 建表语句里的
if not exists和primary key:create table if not exists保证表已存在时不报错;ID integer primary key asc把 ID 设为主键,插入时给NULL数据库会自动分配递增的编号------所以insert的第一个值写的是NULL; - SQL 是用
sprintf拼出来的 :C 程序里的 SQL 就是一个字符串,字符串值别忘了在 SQL 里加引号------\"%s\"生成"张三",少了这对引号语句就错了; - 增删改传
NULL回调,查传callbackfun:create、insert没有结果要处理,第三、四个参数都是NULL;select必须给回调,否则查到的数据拿不到; perrmsg用完要sqlite3_free:sqlite3_exec出错时会给errmsg分配一块内存存错误信息,打印完不释放就是泄漏;- 打开失败也能拿到错误信息 :
sqlite3_open失败时,用sqlite3_errmsg(pdb)可以直接问出原因。
6. 编译
SQLite 的源码就是一个 sqlite3.c 加一个 sqlite3.h,最省事的方式是把它们和自己的代码一起编译(SQLite 用到线程和动态加载,链接时要带 -lpthread -ldl)。Makefile 如下:
makefile
OBJ:=a.out
OBJS+=main.o
OBJS+=sqlite3.o
LDFLAGS:=-lpthread -ldl
$(OBJ):$(OBJS)
gcc $^ -o $@ $(LDFLAGS)
%.o:%.c
gcc -c $^ -o $@
.PHONY:
clean:
rm $(OBJ)
distclean:
rm *.o
rm $(OBJ)
make 之后运行 ./a.out,终端会逐行打印学生表的列名和内容------每行数据对应一次回调。如果系统装了 SQLite 开发库,也可以直接 gcc main.c -o main -lsqlite3,不必编译源码。
7. 练手作业
手里有 dict.txt(每行一个单词加释义)的话,试试这个综合练习:把单词和含义逐行解析出来插入数据库,然后从终端输入单词,用程序在数据库里查出对应含义打印到终端------正好把文件 IO、字符串解析和 select 条件查询串成一条线。
六、常见错误
1. SQL 字符串里的字符串忘加引号
c
sprintf(cmd, "insert into student values (NULL, %s, ...)", s[i].name); // 错
生成的是 values (NULL, 张三, ...),数据库把 张三 当成列名,直接报语法错误。文本值必须用引号包起来:\"%s\"。
2. 回调函数返回了非 0 值
回调返回非 0,SQLite 认为处理出错,sqlite3_exec 立即中止并返回错误。回调正常走完一定要 return 0。
3. errmsg 用完不释放
sqlite3_exec 失败后 errmsg 指向一块它分配的内存,打印完错误必须 sqlite3_free(perrmsg),否则每次出错漏一块。
4. 建表语句重复执行
第二次运行程序时 create table student (...) 会因表已存在而报错。两个办法:语句里加 if not exists,或者像示例一样把建表、插入代码包在 #if 0 里,跑通一次就关掉。
5. 链接报 undefined reference to 'sqlite3_open'
只 #include "sqlite3.h" 不够------头文件只有声明。要么把 sqlite3.c 一起编译(记得 -lpthread -ldl),要么链接 -lsqlite3。
6. update / delete 忘写 where
update student set score=91; 会把所有人的成绩改成 91。凡是改和删,先想清楚条件再动手。
七、数据库速查
| SQL 语句 | 作用 | 示例 |
|---|---|---|
create table |
建表 | create table student (id integer, name text, ...); |
insert into |
插入 | insert into student values (1, "张三", "男", 18, 90); |
select ... from |
查询 | select * from student; |
where |
条件筛选 | where score >= 90 / where name like "赵%" |
update ... set |
修改 | update student set score=91 where name="赵五"; |
delete from |
删除数据 | delete from student where name = "王二"; |
order by |
排序 | order by score desc(降序)/ asc(升序) |
drop table |
删整张表 | drop table student; |
| C 接口 | 作用 |
|---|---|
sqlite3_open(filename, &pdb) |
打开 / 创建数据库,拿到句柄 |
sqlite3_exec(pdb, sql, callback, arg, &errmsg) |
执行 SQL;select 时回调每行调一次 |
sqlite3_close(pdb) |
关闭数据库连接 |
sqlite3_errmsg(pdb) |
查询最近一次的错误信息 |
sqlite3_free(p) |
释放 errmsg 等 SQLite 分配的内存 |
| 易混淆点 | 结论 |
|---|---|
| 关系型 vs 非关系型 | 关系型存文件、用 SQL(SQLite/MySQL);非关系型存内存、速度快(Redis) |
delete vs drop |
delete 删数据表还在;drop 连表带数据一起删 |
| 三种 join | cross 全组合;inner 只留配对成功;left outer 左表全留、配不上补 NULL |
| 回调触发时机 | select 每查到一行调一次;一行没查到则一次不调 |
| 回调返回值 | 必须返回 0,否则 sqlite3_exec 报错中止 |
小结
这一篇我们给程序装上了"记性"。先弄明白数据库是专门做大批量数据增删改查的软件,分关系型(存文件,Oracle/MySQL/SQLite)和非关系型(存内存,Redis)两类,嵌入式场景选轻量免服务的 SQLite。接着练了两层功夫:命令行层 ------.tables、.schema 等点命令看结构,create table 建表、insert into 插入、select + where 条件查询、update 改、delete 删、order by 排序、drop table 删表,再用 cross/inner/outer join 把多张表连起来查;代码层 ------三个接口走天下:sqlite3_open 拿句柄,sqlite3_exec 执行一切 SQL(增删改传空回调,select 靠回调函数逐行收数据),sqlite3_close 收尾,外加 errmsg 用完记得 sqlite3_free。至此,从 C 语言、数据结构、文件 IO、进程线程、网络编程再到数据库,嵌入式 Linux 应用开发的主干技能全部集齐------把这些拼在一起,一个能联网、能并发、能持久化的完整设备程序已经在眼前了。