iOS数据库的使用(二):sqlite的基本使用

Sqlite3的基本使用主要是对2个对象(sqlite3、pstmt)和8个方法(open、step、close等)的理解,本教程会以此为提纲作深入的讲解,全文以sqlite3的使用流程作为行文顺序并介绍其相关的一些方法的使用,have fun!

1. sqlite对象

每一个打开的数据库都使用一个不透明的sqlite对象来表示。其声明非常简单任性,只是使用typedef起了一个别名,如下:

typedef struct sqlite3 sqlite3;

2. sqlite3_initialize( )

对应的有4个方法:

int sqlite3_initialize(void);
int sqlite3_shutdown(void);
int sqlite3_os_init(void);// 可以忽略,initialize方法中已经根据不同的平台做了实现
int sqlite3_os_end(void);// 可以忽略,shutdown方法中已经根据不同的平台做了实现

initialize 方法和 shutdown 方法对应,os_init 和 os_end 对应,在 os 系统中使用,其意义如下:

  • 构造方法和销毁方法一一对应且只能对一个对象调用一次,调用多次不会做额外操作且无伤害(harmless)
  • 构造方法需要在使用任何其他的 sqlite api 之前调用(sqlite3_config()方法需要在init之前调用)
  • 销毁方法需要在所有连接已经关闭且所有资源被释放的情况下调用
  • init方法线程安全,销毁方法线程不安全,销毁方法只能在一个线程中调用
  • 编译阶段可以设置自动调用初始化方法,但是官方建议手动调用初始化方法,因为以后的发布版本中会逐步使用非自动初始化的模式。许多方法在内部会自动调用初始化方法,比如 open 方法。如果是非自动初始化的模式,则需要自己手动调用。
  • 不能主动调用 os_init 和 os_end,这两个函数已经被封装在了 initialize 方法中,会根据平台的不同进行不同的操作

总结:这个方法一般被封装在了 open 当中,一般不需要主动调用(至少在iOS中使用 sqlite3lib 库时),是一个不需要重点关注的方法
initialize方法官方文档

3. sqlite3_open

建立数据库连接的方法,open 方法应该有三个:

  • sqlite3_open
  • sqlite3_open16
  • sqlite3_open_v2。

其中前两个方法内部调用的就是 open_v2 方法,只是配置的参数不一样,其区别如下:

方法名 编码方式 第三个参数 第四个参数
sqlite3_open UTF-8 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE NULL对象(默认的sqlite3_vfs对象)
sqlite3_open16 UTF-16 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE NULL对象(默认的sqlite3_vfs对象)
sqlite3_open_v2 UTF-8 可配置 可配置

所以,重点关注 sqlite3_open_v2 方法即可;

两个注意点:

  • open 方法在建立连接时,无论成功还是失败,都会返回一个指针对象,唯一的例外是当内存不够分配给一个 sqlite3 对象时,open方法会返回一个NULL。所以,每次 open 最好判断是否成功,如果失败则主动 close 掉连接;

  • sqlite3_open_v2 会返回一个 result,以此来判断数据库连接是否建立成功;

  • 对于 open_v2 第三个参数的配置要求为:必选+可选。

必选为下列三个中的一个:

  1. SQLITE_OPEN_READONLY(只读);
  2. SQLITE_OPEN_READWRITE(可读写);
  3. SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE(可读写,没有就创建)

可选项为下表中的任意配置,相关的官方文档

4. sqlite3_open_v2

sqlite3_open_v2方法需要重点关注,其声明如下:

SQLITE_API int sqlite3_open_v2(
  const char *filename,   /* Database filename (UTF-8) */
  sqlite3 **ppDb,         /* OUT: SQLite db handle */
  int flags,              /* Flags */
  const char *zVfs        /* Name of VFS module to use */
);

参数的意义:
第一个参数:fineName,就是要连接的数据库的文件名地址
第二个参数:sqlite3 对象,sqlite3 中使用 sqlite3 对象来表示一个连接;
第三个参数:flags,打开数据库时配置的一些参数。下文会具体介绍。
第四个参数:vfs

open_v2可配置的参数:

参数名 使用限制 意义
SQLITE_OPEN_READONLY 0x00000001 /* Ok for sqlite3_open_v2() */ 只读
SQLITE_OPEN_READWRITE 0x00000002 /* Ok for sqlite3_open_v2() */ 可读可写
SQLITE_OPEN_CREATE 0x00000004 /* Ok for sqlite3_open_v2() */ 没有则创建
SQLITE_OPEN_URI 0x00000040 /* Ok for sqlite3_open_v2() */ 使用URI模式
SQLITE_OPEN_MEMORY 0x00000080 /* Ok for sqlite3_open_v2() */ 内存存储
SQLITE_OPEN_NOMUTEX 0x00008000 /* Ok for sqlite3_open_v2() */ 并发模式
SQLITE_OPEN_FULLMUTEX 0x00010000 /* Ok for sqlite3_open_v2() */ 串行模式
SQLITE_OPEN_SHAREDCACHE 0x00020000 /* Ok for sqlite3_open_v2() */ 共享缓存模式
SQLITE_OPEN_PRIVATECACHE 0x00040000 /* Ok for sqlite3_open_v2() */ 私有缓存模式

总表:Flags For File Open Operations

详细说说第三个参数:

  • 如果第三个参数中设置了 SQLITE_OPEN_URI,则第一个参数,也就是 filename 就会采用URI而不是单纯的文件路劲,具体可参考本文后面的URI部分;
  • 第三个参数读写模式是必选项,有三个选项:SQLITE_OPEN_READONLY(只读)、SQLITE_OPEN_READWRITE(可写可读)、SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE(可写可读,没有数据库文件则主动创建)。第三个参数必须是这三个选项中的一个,然后再和上表中的其他的参数进行组合。
  • mutex 即锁,影响 sqlite 的线程模式;
  • cache 选项是设置共享缓存,可参考sqlite系列文章第三篇:https://www.jianshu.com/p/f036a947853b

5. sqlite3_prepare_v2

sqlite3中的 prepare 方法的意义: sql 的执行需要一个字节码程序。而 prepare 方法就是将 sql 语句生成一个二进制的程序等待被执行。

另一个角度而言,prepare 方法是生成一个 prepared statement 对象,而这个对象就代表一个已经被编译成二进制格式的程序。

prepare方法:prepare方法众多,其中 sqlite3_prepare 是历史遗留方法,不能使用,一般使用 sqlite3_prepare_v2 方法,其他方法可以参考官方文档

参数的意义:第一第二个参数不做解释。第三个参数为负数时,从第一个字符开始读取到第一个终止符(也就是'\0'),所以为负数时就是指读取 sql 参数中的所有。为正数时指从第几个 char 字符开始读取,为 0 时不读取。pzTail 指向被解析的字符串的末尾,一般为 null,具体意义可参考官方文档;

ppStmt:指向一个已经编译过的 prepared statement,用于给将要执行的 sql 添加参数

以插入的 sql 为例,具体使用如下:

- (void)sqliteInsert {
    sqlite3 *sqlite;
    int result = sqlite3_open_v2(DATABASEPATH, &sqlite, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
    if(result != SQLITE_OK) {
        NSLog(@"error opening!: %d", result);
    } else {
        // 无论是否报错,database connection(也就是sqlite3对象)都会被分配内存,需要close来释放掉内存
        sqlite3_close(sqlite);
    }
    
    // 使用prepare插入
    NSString *insertSql = @"replace INTO ClientTable (name, no, signature) values ( ?, ?, ?);";
    
    sqlite3_stmt *statment;
    // prepared方法的作用是使用sql生成一个可执行的程序
    sqlite3_prepare_v2(sqlite, [insertSql UTF8String], -1, &statment, NULL);
    
    // 绑定句柄
    sqlite3_bind_text(statment, 1,[@"测试" UTF8String], -1, NULL);
    sqlite3_bind_text(statment, 2,[@"01" UTF8String], -1, NULL);
    sqlite3_bind_text(statment, 3,[@"测试签名" UTF8String], -1, NULL);
    
    // step是执行sql语句
    int errorNo = sqlite3_step(statment);
    if (errorNo != SQLITE_DONE) {
        NSLog(@"错误");
    }
    sqlite3_finalize(statment);
    sqlite3_close(sqlite);
}

6.sqlite3_bind()

bind 方法据绑定参数的类型分为主要的三种,

  • sqlite3_bind _text表示字符串;
  • sqlite3_bind _blob 表示二进制文件,比如图片、音频;
  • sqlite3_bind _double/int/int64等都表示数字。

当然还有其他的,比如null,这里不做细说。因为 oc 中最长使用的是字符串,所以需要重点关注 sqlite3_bind_text();

sqlite3_bind_text的参数:

  • 第一个参数:从 prepare 函数中分配好内存的 statement 对象
  • 第二个参数:所绑定参数在表中对应的索引
  • 第三个参数:绑定的值
  • 第四个参数:第三个参数需要取到的结尾的偏移量。如果是text的话,传负数代表取到第一个/0终止符,即取所有字符串。可以为正数,代表只取部分,但是如果取到的字符串中有nul,则会出现不可预知的问题。如果是blob类型,该值不能为负数。
    第五个参数:对于blob和text而言,这个参数是处理完成后的回调函数

bind 函数的使用:

- (BOOL)insertWithName:(NSString *)name no:(NSString *)no signature:(NSString *)signature {
    sqlite3 *sqlite;
    int openFlag = sqlite3_open_v2(DATABASEPATH, &sqlite, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE, NULL);
    if (openFlag != SQLITE_OK) {
        NSLog(@"数据库打开失败!");
        sqlite3_close(sqlite);
    }
    
    NSString *sql = @"replace into ClientTable (name,no,signature) values (?, ?, ?);";
    sqlite3_stmt *statement;
    int prepareFlag = sqlite3_prepare_v2(sqlite, [sql UTF8String], -1, &statement, NULL);
    if (prepareFlag !=SQLITE_OK) {
        NSLog(@"数据库prepare失败");
    }
    
    sqlite3_bind_text(statement, 1, [name UTF8String], -1, NULL);
    sqlite3_bind_text(statement, 2, [no UTF8String], -1, NULL);
    sqlite3_bind_text(statement, 3, [signature UTF8String], -1, NULL);
//    // 获取参数的index,第二个参数怎么传??暂未找到方法
//    int index = sqlite3_bind_parameter_index(statement, "name");
    
    int stepFlag = sqlite3_step(statement);
    if (stepFlag != SQLITE_DONE) {
        NSLog(@"插入执行失败!");
    }
    
    sqlite3_finalize(statement);
    sqlite3_close(sqlite);
    if (stepFlag != SQLITE_DONE) {
        return NO;
    } else {
        return YES;
    }
}

7. sqlite3_step()

当 sql 被 prepare、bind 完成后,相当于 sql 语句被变异成了一个可执行程序且给这个程序的执行准备好了参数。

所以,接下来的步骤就是调用 sqlite3_step() 方法来执行 sql 语句。执行完 step 方法后,有三种结果:

  • 报错
    错误号包括busy、misuse等,根据错误号进行处理即可

  • SQLITE_ROW
    执行完成一次但是还有结果需要返回,需要继续调用step方法获取后续的结果

  • SQLITE_DONE
    表示执行完成并已经返回了所有结果

几个注意点

  • 首先出现SQLITE_ROW的时,是有数据返回的情况,也就是执行SELECT语句时。
  • 每执行一次step()方法返回一行数据
  • 必须执行到SQLITE_DONE时才表示所有结果都已经返回,且SQLITE_DONE时不返回数据。

sqlite3_step() 使用步骤,如下:

- (void)query {
    sqlite3 *sqlite;
    NSLog(@"%@",[NSString stringWithUTF8String:DATABASEPATH]);
    int openFlag = sqlite3_open_v2(DATABASEPATH, &sqlite, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, NULL);
    if (openFlag != SQLITE_OK) {
        NSLog(@"数据库打开失败!");
        sqlite3_close(sqlite);
    }

    // +0表示将'no'转化成数字进行比较
    NSString *sql = @"select * from ClientTable where no+0 > ?";
    sqlite3_stmt *statement;
    int prepareFlag = sqlite3_prepare_v2(sqlite, [sql UTF8String], -1, &statement, NULL);
    if (prepareFlag !=SQLITE_OK) {
        NSLog(@"数据库prepare失败");
    }
    
    // 获取sql中的参数个数
    int paramCount = sqlite3_bind_parameter_count(statement);

//    sqlite3_bind_text(statement, 1, "100", -1, nil);
    // 绑定int类型的参数
    sqlite3_bind_int(statement, 1, 100);

//    // 执行一次就要获取一次结果,只执行而不使用column获取结果将会导致数据丢失
//    int result = sqlite3_step(statement);
    
    while (sqlite3_step(statement) == SQLITE_ROW) {
        NSUInteger num_cols = (NSUInteger)sqlite3_data_count(statement);
        int columnCount = sqlite3_data_count(statement);
        const unsigned char *c1 = sqlite3_column_text(statement, 0);
        const unsigned char *c2 = sqlite3_column_text(statement, 1);
        const unsigned char *c3 = sqlite3_column_text(statement, 2);
        NSLog(@"%s-%s-%s",c1,c2,c3);
    }
    sqlite3_finalize(statement);
    sqlite3_close(sqlite);
}

sqlite3_step()中的结果处理有关的几个方法
sqlite3_bind_parameter_count:获取sql中需要绑定的参数的个数
sqlite3_column_type:获取对应列名的类型,用于动态绑定/获取参数
sqlite3_column_count:获取step执行完毕后的结果行中的列数,用于将数据进行序列化从而输出数据
sqlite3_data_count:和sqlite3_column_count大同小异

8. sqlite3_finalize()

关闭虚拟机并释放所有由prepare()函数产生的内存

9. sqlite3_close()

关闭数据库

10. sqlite3_reset()

用户一般不会多次执行完全相同的sql语句,但是会经常执行格式差不多但是参数不一样的sql语句。按照常规操作,bind后需要finalize对应的statement然后再次sqlite_prepare新的statement。但是sqlite_reset方法的效率是大于等于sqlite_prepare方法的,而sqlite_reset相当于将statement回滚到了最初的状态。所以使用reset可以显著地提高程序的性能表现。

FMDB中正是使用了这个特性,将bind之前的sql语句对应的pstmt存储起来,每次使用之前进行reset,如果没有才创建,代码如下:

if (_shouldCacheStatements && !cachedStmt) {
        cachedStmt = [[FMStatement alloc] init];
        
        [cachedStmt setStatement:pStmt];
        
        [self setCachedStatement:cachedStmt forQuery:sql];
        
        FMDBRelease(cachedStmt);
    }

if (cachedStmt) {
        [cachedStmt setUseCount:[cachedStmt useCount] + 1];
        closeErrorCode = sqlite3_reset(pStmt);
    } else {
        /* Finalize the virtual machine. This releases all memory and other
         ** resources allocated by the sqlite3_prepare() call above.
         */
        closeErrorCode = sqlite3_finalize(pStmt);
    }

备注
1、FMDB中的 reset 模式默认关闭,可以通过 self.db.shouldCacheStatements = YES;来开启 pstmt 缓存模式
2、在 [database close] 时会清空所有数据,所以 reset 的优化效果仅限于一次 open-close 之内的操作

11. sqlite3_exec

exec 方法是对 sqlite3_prepare_v2()、sqlite3_step()、和 sqlite3_finalize()方法的封装。可以一步到位执行多个sql语句,但是无法绑定参数。使用如下:
注意点:
1、要根据返回值判断是否执行成功,因为执行错误不一定会有errMsg(具体如何使用errMsg??)
2、如果使用了errMsg,那么需要手动调用sqlite_free()方法来释放内存
代码如下:

- (void)createDB {
    sqlite3 *sqlite;
    int openFlage = sqlite3_open_v2(DATABASEPATH, &sqlite, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
    if (openFlage != SQLITE_OK) {
        sqlite3_close(sqlite);
        NSLog(@"数据库打开失败!");
    }
    NSLog(@"path:%@",[NSString stringWithUTF8String:DATABASEPATH]);
    
    NSString *deleteSql = @"delete from ClientTable";
    char *err = 0;
    int deleteFlag = sqlite3_exec(sqlite, [deleteSql UTF8String], NULL, NULL, &err);
    if (deleteFlag != SQLITE_OK) {
        NSLog(@"数据库表创建失败!");
        NSLog(@"%s",err);
    }
    
    NSString *sql = @"CREATE table if not exists ClientTable (name text, no text, signature text,PRIMARY KEY(no));delete from ClientTable";
    
    int createFlag = sqlite3_exec(sqlite, [sql UTF8String], NULL, NULL, NULL);
    if (createFlag != SQLITE_OK) {
        NSLog(@"数据库表创建失败!");
    }
    
    sqlite3_free(err);
    int closeFlag = sqlite3_close(sqlite);
    
    if (closeFlag != SQLITE_OK) {
        NSLog(@"close错误");
    }
}
12. vfs是什么?


仅vfs可设置的参数(sqlite3_vfs.xOpen方法):

参数名 使用限制 意义
SQLITE_OPEN_DELETEONCLOSE 0x00000008 /* VFS only */ ???
SQLITE_OPEN_EXCLUSIVE 0x00000010 /* VFS only */ ???
SQLITE_OPEN_AUTOPROXY 0x00000020 /* VFS only */ ???
SQLITE_OPEN_MAIN_DB 0x00000100 /* VFS only */ ???
SQLITE_OPEN_TEMP_DB 0x00000200 /* VFS only */ ???
SQLITE_OPEN_TRANSIENT_DB 0x00000400 /* VFS only */ ???
SQLITE_OPEN_MAIN_JOURNAL 0x00000800 /* VFS only */ ???
SQLITE_OPEN_TEMP_JOURNAL 0x00001000 /* VFS only */ ???
SQLITE_OPEN_SUBJOURNAL 0x00002000 /* VFS only */ ???
SQLITE_OPEN_MASTER_JOURNAL 0x00004000 /* VFS only */ ???

总表:Flags For File Open Operations

URI是什么?

URI:统一资源标识符
URL:统一资源定位符,带访问方式的统一资源标识符
URN:统一资源名

三者关系:URL和URN都是是URI的子集。URN上中加上了对应的访问方式就成了URL。因为是子集,URL、URN都可以叫做URI

例如8985321是一串电话,可以认为是URN,是URN必定是URI了。但是你不用用http协议去访问电话吧?应当使用tel:+0278985321这种方式去访问,所以这里的“tel:+0278985321”就是一个URL,也可以说成是一个URI。类似的还有https:www.baidu.com,其实是访问了服务器中的index.html这种文件,只是代理吧IP处理成了域名,并将端口号等信息进行了封装

sqlite3中的URI

URI说白了就是告诉程序open的是哪个数据库文件,同时sqlite3中可以在URI中添加很多参数,这样就直接省去了在第三个参数中设置的麻烦

开启URI的三种方式:

URI的参数设置:
vfs:vfs设置
mode:设置数据库的打开方式,也就是read、write、create那几个参数
cache:缓存模式,也就是share-cache和private-cache,共享缓存模式
psow:存储介质相关,略
nolock:设置 rollback journal modes模式,参考sqlite3中的文件锁
immutable:布尔值,为yes时代表当前使用的数据库是只读类的媒体文件(例如只读光盘的刻录)。此时程序认为数据库只为以只读的方式打开,所有的锁都会失效。

官方文档:2个对象和8个方法的概览 或者说是 sqlite中的核心接口和对象

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345