从零开始学习嵌入式P35----数据库

从零开始学习嵌入式P35----数据库

上一篇我们把 TCP 服务器改造成了能同时接待成百上千个客户端的并发服务器。程序能"跑起来"了,可一个更现实的问题浮出水面:数据放哪儿 ?学生的成绩、单词的释义、设备的采集记录------关机就没了可不行。用文件自己存?增删改查都得自己写,数据一多就乱套。这一篇我们请出专门干这件事的软件------数据库 :先说清数据库是什么、分哪几类,然后以轻量到可以直接嵌进程序的 SQLite 为主角,学会它的命令行操作和常用 SQL 语句(建表、增、删、改、查、排序、多表联合查询),最后用三个 C 语言接口 sqlite3_open / sqlite3_exec / sqlite3_close 把数据库真正用进我们的代码里。

本篇目标

学完这一篇,我们应该能够:

  • 说清数据库是什么,关系型和非关系型数据库的区别;
  • 会安装 SQLite,会用 .tables.schema 等常用命令行指令;
  • 说清 SQLite 支持的四种数据类型;
  • 会写 create tableinsert intoselectupdatedeleteorder bydrop table 语句;
  • 会用 where 子句配合比较、BETWEENLIKE 做条件查询;
  • 说清 cross joininner joinouter join 三种多表查询的区别;
  • 会用 sqlite3_opensqlite3_execsqlite3_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 设置查询结果的显示形式(如 columnlist
.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 时,每查到一行数据,这个回调就被调用一次;不需要处理结果(如 insertupdate)时传 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);
  • argsqlite3_exec 第四个参数透传过来的值;
  • colnum:本次查询结果这一行的列数
  • pcontent:字符串指针数组,各列的内容
  • ptitle:字符串指针数组,各列的列名

两个铁律:

  1. select 每查到一行 数据,回调就被调用一次------查回 10 行就调 10 次;
  2. 如果 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;
}

这份代码把三个接口的用法串成了标准流程,五个细节值得停下来看:

  1. 建表语句里的 if not existsprimary keycreate table if not exists 保证表已存在时不报错;ID integer primary key asc 把 ID 设为主键,插入时给 NULL 数据库会自动分配递增的编号------所以 insert 的第一个值写的是 NULL
  2. SQL 是用 sprintf 拼出来的 :C 程序里的 SQL 就是一个字符串,字符串值别忘了在 SQL 里加引号------\"%s\" 生成 "张三",少了这对引号语句就错了;
  3. 增删改传 NULL 回调,查传 callbackfuncreateinsert 没有结果要处理,第三、四个参数都是 NULLselect 必须给回调,否则查到的数据拿不到;
  4. perrmsg 用完要 sqlite3_freesqlite3_exec 出错时会给 errmsg 分配一块内存存错误信息,打印完不释放就是泄漏;
  5. 打开失败也能拿到错误信息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 应用开发的主干技能全部集齐------把这些拼在一起,一个能联网、能并发、能持久化的完整设备程序已经在眼前了。

相关推荐
sunoo-2291 小时前
【网络编程 + 数据库】select 与 epoll 核心区别详解 + SQLite 入门到 C 接口全攻略
linux·网络·数据库·vscode·学习·sqlite
TDengine (老段)1 小时前
TDengine 错误处理 — 错误码、异常传播、故障恢复
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
zbyyd1 小时前
Linux 系统编程 :select/epoll 多路复用与 SQLite 数据库实战
linux·数据库·sqlite
俊昭喜喜里1 小时前
c#中的构造函数
开发语言·数据库·c#
养生技术人1 小时前
Oracle OCP认证考试题目详解082系列第17题
运维·数据库·sql·oracle·ocp
单片机仿真设计1 小时前
【Proteus 仿真】基于 STM32 单片机智能楼宇照明控制系统设计(定制仿真咨询)
stm32·单片机·嵌入式硬件·proteus·毕设
koi77u1 小时前
嵌入式学习———sqlite数据表
linux·数据库·学习·sqlite
老王爱玩车1 小时前
汉诺塔 + 青蛙跳台阶问题
学习
xx~t1 小时前
嵌入式——Linux软件编程——数据库
linux·c语言·数据库·oracle·sqlite