iOS之Sqlite3封装

Posted 酒茶白开水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之Sqlite3封装相关的知识,希望对你有一定的参考价值。

一、代码下载

代码下载地址

二、实例效果展示

效果图一.png
效果图二.png
效果图三.png

三、实例项目简介

这个实例主要是封装sqlite3数据库工具,并用这个工具实现记录、删除、修改、查询学生信息的功能,另外一个附属的功能就是根据学生的姓名首字母分组并添加索引。

对于数据库,我做了两层封装。第一层是对数据库的基本操作进行抽象,第二层是对学生数据的操作进行抽象。这种分层的思想不仅能使处于底层的功能得到多次重用,调高效率;而且这样做逻辑清晰,易于定位问题,有益于维护与跟新。

四、实例项目分析

1、首先抽象出数据库基本操作工具类,而数据库的基本操作有:打开、关闭、建表、增、删、改、查……基于这些操作,我封装出如下五个方法来提供数据库的操作:

 *  打开数据库
 *
 *  @param path    数据库路径
 *  @param success 打开成功的回调
 *  @param falure  打开失败的回调
 */
+ (void)openDBPath:(NSString *)path succesefulBlock:(void (^)(sqlite3 *db))success andFailureBlock:(void (^)(NSString *msg))failure
{
    sqlite3 *database = NULL;
    int result = sqlite3_open(path.UTF8String, &database);
    if (result == SQLITE_OK) {
        if (success) {
            success(database);
        }
    }
    else
    {
        if (failure) {
            const char *msg = sqlite3_errmsg(database);
            failure([NSString stringWithUTF8String:msg]);
        }

        if (database) {
            [self closeDB:database succesefulBlock:nil andFailureBlock:nil];
        }
    }
}

/**
 *  关闭数据库
 *
 *  @param database 数据库链接
 *  @param succese  成功的回调
 *  @param failure  失败的回调
 */
+ (void)closeDB:(sqlite3 *)database succesefulBlock:(void (^)())succese andFailureBlock:(void (^)(NSString *msg))failure
{
    int result = sqlite3_close(database);
    if (result == SQLITE_OK) {
        if (succese) {
            succese();
        }
    }
    else
    {
        if (failure) {
            failure([NSString stringWithUTF8String:sqlite3_errmsg(database)]);
        }
    }
}

/**
 *  执行SQL语句(不适应查询语句和blob(NSData)二进制数据类型的操作)
 *
 *  @param sqStr    SQL语句
 *  @param database 数据库链接
 *  @param succese  成功的回调
 *  @param failure  失败的回调
 */
+ (void)executeSql:(NSString *)sqStr toDatabase:(sqlite3 *)database succesefulBlock:(void (^)())succese andFailureBlock:(void (^)(NSString *msg))failure
{
    DebugLog(@"%@", sqStr);
    char *msg = NULL;
    int result = sqlite3_exec(database, sqStr.UTF8String, NULL, NULL, &msg);
    if (result == SQLITE_OK) {
        if (succese) {
            succese();
        }
    }
    else
    {
        if (failure) {
            failure([NSString stringWithUTF8String:msg]);
        }
    }
}

/**
 *  准备需要sqlite3_stmt结果集的SQL语句
 *
 *  @param sqStr    SQL语句
 *  @param database 数据库连接
 *  @param succese  成功的回调
 *  @param failure  失败的回调
 */
+ (void)prepareSql:(NSString *)sqStr fromDatabase:(sqlite3 *)database succesefulBlock:(void (^)(sqlite3_stmt *stmt))succese andFailureBlock:(void (^)(NSString *msg))failure
{
    DebugLog(@"%@", sqStr);
    sqlite3_stmt *stmt = NULL;
    int result = sqlite3_prepare_v2(database, sqStr.UTF8String, -1, &stmt, NULL);
    if (result == SQLITE_OK) {
        if (succese) {
            succese(stmt);
        }
    }
    else
    {
        if (failure) {
            failure(@"SQL语句是非法的。");
        }
    }
}

2、接着就是抽象学生数据功能类了。根据需要我抽象出了如下五个方法

/**
 *  获取所有学生
 *
 *  @param callBack 回调
 */
+ (void)getAllStudents:(void (^)(NSArray *students, NSString *msg))callBack;

/**
 *  添加学生
 *
 *  @param studentModel 学生模型
 *  @param succese      添加成功回调
 *  @param failure      添加失败回调
 */
+ (void)addStudent:(StudentModel *)studentModel succesefulBlock:(void (^)(StudentModel *studentModel))succese andFailureBlock:(void (^)(NSString *msg))failure;

/**
 *  更新学生
 *
 *  @param studentModel 学生模型
 *  @param succese      更新成功回调
 *  @param failure      更新失败回调
 */
+ (void)updateStudent:(StudentModel *)studentModel  succesefulBlock:(void (^)(StudentModel *studentModel))succese andFailureBlock:(void (^)(NSString *msg))failure;

/**
 *  按条件搜索学生
 *
 *  @param condition 条件
 *  @param callBack  搜索回调
 */
+ (void)searchStudents:(NSString *)condition andCallBack:(void (^)(NSArray *students, NSString *msg))callBack;

/**
 *  删除学生
 *
 *  @param studentModel 学生模型
 *  @param succese      删除成功回调
 *  @param failure      删除失败回调
 */
+ (void)deleteStudent:(StudentModel *)studentModel  succesefulBlock:(void (^)(StudentModel *studentModel))succese andFailureBlock:(void (^)(NSString *msg))failure;

细节分析
- 由于该功能类都是使用类方法,所以不能以实例变量来存储数据,因此声明全局变量static sqlite3 *database来存储数据库的链接,并以static关键字修饰来避免其他源文件对其访问。
- 类方法initialize是在手动调用累中任何方法前调用一次,所以在这个方法中打开数据库并创建学生表是非常合适的。

/**
 *  在手动调用类里的任何方法前自动调用一次
 */
+ (void)initialize
{
    [SqliteDBAccess openDBPath:SqlitePathStr succesefulBlock:^(sqlite3 *db) {
        DebugLog(@"数据库打开成功!");
        [SqliteDBAccess executeSql:[NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS %@ (identifier integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, studentNumber text NOT NULL UNIQUE, photo blob, age integer NOT NULL, address text, describe text);", StudentTableName] toDatabase:db succesefulBlock:^{
            database = db;
            DebugLog(@"学生表创建成功!");
        } andFailureBlock:^(NSString *msg) {
            DebugLog(@"学生表创建失败,%@", msg);
        }];
    } andFailureBlock:^(NSString *msg) {
        DebugLog(@"数据库打开失败,%@", msg);
    }];
}
  • 在学生数据功能类的内部可以封装一个不公开的方法来从sqlite3_stmt结果集中获取学生模型对象
/**
 *  从sqlite3_stmt中获取学生数据
 *
 *  @param stmt sqlite3_stmt结果集
 *
 *  @return 学生数组
 */
+ (NSArray *)getStudentsFromStatement:(sqlite3_stmt *)stmt
{
    NSMutableArray *students = [NSMutableArray arrayWithCapacity:1];
    StudentModel *studentModel;
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        studentModel = [[StudentModel alloc] init];
        studentModel.identifier = sqlite3_column_int(stmt, 0);
        char *name = (char *)sqlite3_column_text(stmt, 1);
        char *studentNumber = (char *)sqlite3_column_text(stmt, 2);
        void *photo = (void *)sqlite3_column_blob(stmt, 3);
        studentModel.name = name ? [NSString stringWithUTF8String:name] : nil;
        studentModel.studentNumber = studentNumber ? [NSString stringWithUTF8String:studentNumber] : nil;
        studentModel.photo = photo ? [UIImage imageWithData:[NSData dataWithBytes:photo length:sqlite3_column_bytes(stmt, 3)]] : nil;
        studentModel.age = sqlite3_column_int(stmt, 4);
        char *address = (char *)sqlite3_column_text(stmt, 5);
        char *describe = (char *)sqlite3_column_text(stmt, 6);
        studentModel.address = address ? [NSString stringWithUTF8String:address] : nil;
        studentModel.describe = describe ? [NSString stringWithUTF8String:describe] : nil;

        [students addObject:studentModel];
    }

    return students;
}
  • 由于表中存在二进制数据,所以插入跟新二进制数据时使用sqlite3_exec函数执行SQL语句是有问题的,需要使用sqlite3_step函数来执行SQL语句。
/**
 *  更新学生
 *
 *  @param studentModel 学生模型
 *  @param succese      更新成功回调
 *  @param failure      更新失败回调
 */
+ (void)updateStudent:(StudentModel *)studentModel  succesefulBlock:(void (^)(StudentModel *studentModel))succese andFailureBlock:(void (^)(NSString *msg))failure
{
    [SqliteDBAccess prepareSql:[NSString stringWithFormat:@"UPDATE %@ SET name = ?, studentNumber = ?, photo = ?, age = ?, address = ?, describe = ? WHERE identifier = ?;", StudentTableName] fromDatabase:database succesefulBlock:^(sqlite3_stmt *stmt) {
        NSData *data = UIImagePNGRepresentation(studentModel.photo);
        sqlite3_bind_text(stmt, 1, studentModel.name.UTF8String, -1, NULL);
        sqlite3_bind_text(stmt, 2, studentModel.studentNumber.UTF8String, -1, NULL);
        sqlite3_bind_blob(stmt, 3, [data bytes], (int)[data length], NULL);
        sqlite3_bind_int(stmt, 4, studentModel.age);
        sqlite3_bind_text(stmt, 5, studentModel.address.UTF8String, -1, NULL);
        sqlite3_bind_text(stmt, 6, studentModel.describe.UTF8String, -1, NULL);
        sqlite3_bind_int(stmt, 7, studentModel.identifier);
        //执行完成
        if (sqlite3_step(stmt) == SQLITE_DONE) {
            if (succese) {
                succese(studentModel);
            }
        }
        else
        {
            if (failure) {
                failure([NSString stringWithFormat:@"更新学生失败,%@", [NSString stringWithUTF8String:sqlite3_errmsg(database)]]);
            }
        }

        //在遍历完结果集后,调用sqlite3_finalize以释放和预编译的语句相关的资源。
        sqlite3_finalize(stmt);
    } andFailureBlock:^(NSString *msg) {
        if (failure) {
            failure([NSString stringWithFormat:@"更新学生失败,%@", msg]);
        }
    }];
}

五、问题与补充

1.sqlite3事务:是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。例如,银行转账工作:从一个账号扣款并使另一个账号增款,这两个操作要么都执行,要么都不执行。所以,应该把它们看成一个事务。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。 针对上面的描述可以看出,事务的提出主要是为了解决并发情况下保持数据一致性的问题。事务的语句:
- 开始事物:BEGIN TRANSACTION
- 提交事物:COMMIT TRANSACTION
- 回滚事务:ROLLBACK TRANSACTION

项目中添加学生数据的时候,并没有设置identifer主键,但是主键是有用的,在插入成功后需要查出来,因此添加学生就涉及两步操作:插入数据,查询数据。而且这两个操作必须要同时成功才能算是学生添加成功。所以在这里,我就开启一个事务,先插入学生数据,再查询此学生数据,只要有一个步骤发生错误就回滚事务,全部成功则提交事务:

/**
 *  添加学生
 *
 *  @param studentModel 学生模型
 *  @param succese      添加成功回调
 *  @param failure      添加失败回调
 */
+ (void)addStudent:(StudentModel *)studentModel succesefulBlock:(void (^)(StudentModel *studentModel))succese andFailureBlock:(void (^)(NSString *msg))failure
{
    //开启事务
    [SqliteDBAccess executeSql:@"BEGIN TRANSACTION" toDatabase:database succesefulBlock:^{
        DebugLog(@"事务启动成功!");

        [SqliteDBAccess prepareSql:[NSString stringWithFormat:@"INSERT INTO %@ (name, studentNumber, photo, age, address, describe) VALUES(?, ?, ?, ?, ?, ?);", StudentTableName] fromDatabase:database succesefulBlock:^(sqlite3_stmt *stmt) {
            NSData *data = UIImagePNGRepresentation(studentModel.photo);
            sqlite3_bind_text(stmt, 1, studentModel.name.UTF8String, -1, NULL);
            sqlite3_bind_text(stmt, 2, studentModel.studentNumber.UTF8String, -1, NULL);
            sqlite3_bind_blob(stmt, 3, [data bytes], (int)[data length], NULL);
            sqlite3_bind_int(stmt, 4, studentModel.age);
            sqlite3_bind_text(stmt, 5, studentModel.address.UTF8String, -1, NULL);
            sqlite3_bind_text(stmt, 6, studentModel.describe.UTF8String, -1, NULL);
            if (sqlite3_step(stmt) == SQLITE_DONE) {
                [SqliteDBAccess prepareSql:[NSString stringWithFormat:@"SELECT * FROM %@ WHERE studentNumber = ?;", StudentTableName] fromDatabase:database succesefulBlock:^(sqlite3_stmt *stmt) {
                    sqlite3_bind_text(stmt, 1, studentModel.studentNumber.UTF8String, -1, NULL);
                    StudentModel *model = [[self getStudentsFromStatement:stmt] firstObject];
                    if (studentModel) {
                        studentModel.identifier = model.identifier;
                        if (succese) {
                            succese(studentModel);
                        }

                        //提交事务
                        [SqliteDBAccess executeSql:@"COMMIT TRANSACTION" toDatabase:database succesefulBlock:^{
                            DebugLog(@"提交事务成功!");
                        } andFailureBlock:^(NSString *msg) {
                            DebugLog(@"提交事务失败:%@", msg);
                        }];
                    }
                    else
                    {
                        //回滚事务
                        [SqliteDBAccess executeSql:@"ROLLBACK TRANSACTION" toDatabase:database succesefulBlock:^{
                            DebugLog(@"回滚成功!");
                        } andFailureBlock:^(NSString *msg) {
                            DebugLog(@"回滚失败:%@", msg);
                        }];
                    }

                    //在遍历完结果集后,调用sqlite3_finalize以释放和预编译的语句相关的资源。
                    sqlite3_finalize(stmt);
                } andFailureBlock:^(NSString *msg) {
                    if (failure) {
                        failure(msg);

                        //回滚事务
                        [SqliteDBAccess executeSql:@"ROLLBACK TRANSACTION" toDatabase:database succesefulBlock:^{
                            DebugLog(@"回滚成功!");
                        } andFailureBlock:^(NSString *msg) {
                            DebugLog(@"回滚失败:%@", msg);
                        }];
                    }
                }];
            }
            else
            {
                if (failure) {
                    failure([NSString stringWithFormat:@"添加学生失败,%@", [NSString stringWithUTF8String:sqlite3_errmsg(database)]]);
                }
            }

            //在遍历完结果集后,调用sqlite3_finalize以释放和预编译的语句相关的资源。
            sqlite3_finalize(stmt);
        } andFailureBlock:^(NSString *msg) {
            if (failure) {
                failure([NSString stringWithFormat:@"添加学生失败,%@", msg]);
            }

            //回滚事务
            [SqliteDBAccess executeSql:@"ROLLBACK TRANSACTION" toDatabase:database succesefulBlock:^{
                DebugLog(@"回滚成功!");
            } andFailureBlock:^(NSString *msg) {
                DebugLog(@"回滚失败:%@", msg);
            }];
        }];
    } andFailureBlock:^(NSString *msg) {
        DebugLog(@"事务启动失败:%@", msg);
    }];
}

2.sqlite3注入漏洞:
举例:登录功能
1.用户输入登录名和密码:userName = ‘zhangsan or 1 = 1 or ” = ”’ password = ‘123456’
select * from t_user where name = ‘zhangsan or 1 = 1 or ” = ”’ and password = ‘123456’;
如此条件永远成立
解决方案:参数化查询
用?替换掉需要查询的条件
select * from t_user where name = ? and passsword = ?;
检查查询语句合法后,再绑定参数
sqlite3_bind_text(stmt, 1, “张三”, NULL);

3.sqlite3模糊查询:确定给定的字符串是否与指定的模式匹配。模式可以包含常规字符和通配符字符。模式匹配过程中,常规字符必须与字符串中指定的字符完全匹配。然而,可 使用字符串的任意片段匹配通配符。与使用 = 和 != 字符串比较运算符相比,使用通配符可使 LIKE 运算符更加灵活。
- %:包含零个或更多字符的任意字符串。WHERE title LIKE ‘%computer%’ 将查找处于书名任意位置的包含单词 computer 的所有书名。
- _(下划线):任何单个字符。WHERE au_fname LIKE ‘_ean’ 将查找以 ean 结尾的所有 4 个字母的名字(Dean、Sean 等)。
- [ ]:指定范围 ([a-f]) 或集合 ([abcdef]) 中的任何单个字符。WHERE au_lname LIKE ‘[C-P]arsen’ 将查找以arsen 结尾且以介于 C 与 P 之间的任何单个字符开始的作者姓氏,例如,Carsen、Larsen、Karsen 等。
- [^]:不属于指定范围 ([a-f]) 或集合 ([abcdef]) 的任何单个字符。WHERE au_lname LIKE ‘de[^l]%’ 将查找以 de 开始且其后的字母不为 l 的所有作者的姓氏。

/**
 *  按条件搜索学生
 *
 *  @param condition 条件
 *  @param callBack  搜索回调
 */
+ (void)searchStudents:(NSString *)condition andCallBack:(void (^)(NSArray *students, NSString *msg))callBack
{
    int age = [condition intValue];
    NSString *selectStr;
    if (age != 0 || [condition isEqualToString:@"0"]) {
        selectStr = [NSString stringWithFormat:@"SELECT * FROM %@ WHERE name LIKE ? or studentNumber LIKE ? or age LIKE ?;", StudentTableName];
    }
    else
    {
        selectStr = [NSString stringWithFormat:@"SELECT * FROM %@ WHERE name LIKE ? or studentNumber LIKE ?;", StudentTableName];
    }
    [SqliteDBAccess prepareSql:selectStr fromDatabase:database succesefulBlock:^(sqlite3_stmt *stmt) {
        sqlite3_bind_text(stmt, 1, [NSString stringWithFormat:@"%%%@%%", condition].UTF8String, -1, NULL);
        sqlite3_bind_text(stmt, 2, [NSString stringWithFormat:@"%%%@%%", condition].UTF8String, -1, NULL);
        if (age != 0 || [condition isEqualToString:@"0"]) {
            sqlite3_bind_int(stmt, 3, age);
        }

        if (callBack) {
            callBack([self getStudentsFromStatement:stmt], @"搜索成功!");
        }

        //在遍历完结果集后,调用sqlite3_finalize以释放和预编译的语句相关的资源。
        sqlite3_finalize(stmt);
    } andFailureBlock:^(NSString *msg) {
        if (callBack) {
            callBack(nil, msg);
        }
    }];
}

3.UITableView索引:
- 对数据进行分组排序

/**
 *  按首字母分组排序
 *
 *  @param sourceArr 目标数组
 *
 *  @return 分好组的数组
 */
- (NSArray *)groupedByLetter:(NSArray *)sourceArr
{
    UILocalizedIndexedCollation *collation = [UILocalizedIndexedCollation currentCollation];

    //按照索引创建对应的数组
    NSMutableArray *sectionsArr = [NSMutableArray arrayWithCapacity:collation.sectionTitles.count];
    for (int index = 0; index < collation.sectionTitles.count; index++) {
        NSMutableArray *arr = [NSMutableArray arrayWithCapacity:1];
        [sectionsArr addObject:arr];
    }

    //设置StudentModel模型的组号,并装进对应数组中
    for (StudentModel *studentModel in sourceArr) {
        NSInteger section = [collation sectionForObject:studentModel collationStringSelector:@selector(name)];
        studentModel.section = section;
        [sectionsArr[section] addObject:studentModel];
    }

    //对每组数据进行按名称首字母排序
    NSMutableArray *resultArr = [NSMutableArray arrayWithCapacity:1];
    for (NSArray *arr in sectionsArr) {
        [resultArr addObject:[NSMutableArray arrayWithArray:[collation sortedArrayFromArray:arr collationStringSelector:@selector(name)]]];
    }

    return resultArr;
}
  • 在UITableView的代理方法中设置组的标题和索引数组

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    if (tableView == self.tableView && [self.studentsArr[section] count] > 0) {
        return [UILocalizedIndexedCollation currentCollation].sectionTitles[section];
    }

    return nil;
}
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
    if (tableView == self.tableView) {
        NSMutableArray *resultArr = [NSMutableArray arrayWithCapacity:1];
        for (NSArray *arr in self.studentsArr) {
            if (arr.count > 0) {
                NSInteger index = [self.studentsArr indexOfObject:arr];
                [resultArr addObject:[UILocalizedIndexedCollation currentCollation].sectionTitles[index]];
            }
        }
        [resultArr insertObject:UITableViewIndexSearch atIndex:0];

        return resultArr;
    }
    else
    {
        return nil;
    }
}
  • 在UITableView的代理方法中将索引与cell的组联系起来
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
    NSInteger result = -1;
    if (tableView == self.tableView) {
        if (index == 0) {
            [tableView scrollRectToVisible:tableView.tableHeaderView.frame animated:NO];
        }
        else
        {
            result = [[UILocalizedIndexedCollation currentCollation].sectionTitles indexOfObject:title];
        }
    }

    return result;
}

以上是关于iOS之Sqlite3封装的主要内容,如果未能解决你的问题,请参考以下文章

Python编程之sqlite3数据库

SQLite3的面向对象封装FMDB

iOS开发之数据存储

iOS开发之数据存储

(SQLite3封装) 一行代码实现增删改查 - 值得收藏转发分享

IOS-SQLite3的封装