FMDB的基本使用和解析(Swift3.0)

FMDB
FMDB是iOS平台的SQLite数据库框架,FMDB以OC的方式封装了SQLite的C语言API

FMDB的优点
使用起来更加面向对象,省去了很多麻烦、冗余的C语言代码提供了多线程安全的数据库操作方法,有效地防止数据混乱

FMDB中3个主要的类:FMDB 更多的详细信息,看github
FMDatabase - 代表一个SQLite 数据库,用于执行SQL语句.executeStatements:执行多条sql。executeQuery:执行查询语句。executeUpdate:执行除查询以外的语句,create, drop, insert, delete, update
FMResultSet -代表在FMDatabase中执行查寻的结果数据,通过字段名称获取字段值
FMDatabaseQueue - 如果想在多线程执行查寻和更新,我们将使用该类,处理线程安全。

数据库的创建(Database Creation)
 创建一个FMDatabase对象需要一个SQLite数据库文件路径,可以使用的方式之一:
 1
 一个系统文件路径,该文件并不一定是存在磁盘上,如果文件不存在,自己手动创建。
 2
一个空的字符串,一个空的数据库将被创建在一个零时的位置,当FMDatabase链接被关闭,数据库被删除。
 3NULL,一个内存的数据库被创建,当FMDatabase连接被关闭,数据库将被摧毁。

对于更多有关于零时和内存数据库,可以阅读sqlite文档,请看这里
 NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp.db"];
 FMDatabase *db = [FMDatabase databaseWithPath:path];

打开数据库(Opening)
在你能够跟数据库交互之前,数据库必须被打开,如果有无效的资源中,数据库打开失败
 
 if (![db open]) {
 // [db release];   // uncomment this line in manual referencing code; in ARC, this is not necessary/permitted
 db = nil;
 return;
 }
 

执行更新操作( Executing Updates)
任何类型的SQL语句,它不是一个SELECT语句都可以用于执行更新(update)。包括CREATE, UPDATE, INSERT, ALTER, COMMIT, BEGIN, DETACH, DELETE, DROP, END, EXPLAIN, VACUUM,  REPLACE 等语句。基本上,如果你的SQL语句不是以SELECT开始,它就是一个更新(update)语句。
 
执行更新将返回一个布尔值,如果是YES表示执行update成功,如果返回NO意味着update遇到了错误。可以在代码中使用lastErrorMessage和lastErrorCode方法获取失败的信息。
 
执行查寻(Executing Queries )
 一个SELECT声明是一个查寻语句,可以使用-executeQuery方法进行查寻操作。执行查寻之后,如果查寻成功将返回FMResultSet对象,如果失败为nil。可以在代码中使用lastErrorMessage和lastErrorCode方法获取失败的信息。

为了循环遍历查寻的结果,可以使用while()循环,对于FMDB,最容易的方式如下:

 FMResultSet *s = [db executeQuery:@"SELECT * FROM myTable"];
 while ([s next]) {
 //retrieve values for each record
 }
 
必须值访问查寻结果之前执行 -[FMResultSet next],及时只使用一个:
 FMResultSet *s = [db executeQuery:@"SELECT COUNT(*) FROM myTable"];
 if ([s next]) {
 int totalCount = [s intForColumnIndex:0];
 }
 FMResultSet有许多合适的方法来获取数据:
 intForColumn:
 longForColumn:
 longLongIntForColumn:
 boolForColumn:
 doubleForColumn:
 stringForColumn:
 dateForColumn:
 dataForColumn:
 dataNoCopyForColumn:
 UTF8StringForColumnName:
 objectForColumnName:
每一个方法都是该{type}ForColumnIndex:类型的变体,基于当前列获取对应类型的数据。这里并不需要手动close掉FMResultSet对象,因为当执行发生的时候,要么 result set被释放掉了,要么数据库被关闭了。
 
关闭(Closing)

当完成了数据库的查寻(executing queries)和更新(executing updates),应该使用close方法关闭与数据库(FMDatabase)的连接.
 [db close];

事务(Transactions)
FMDatabase
能够使用合理的方式开始(begin)和提交(commint)一个事务或者执行开始(begin)和结束(end)事务。
 
多语句批量操作(Multiple Statements and Batch Stuff)
可以使用FMDatabaseexecuteStatements:withResultBlock: 执行字符串中的多条语句:

 NSString *sql = @"create table bulktest1 (id integer primary key autoincrement, x text);"
 "create table bulktest2 (id integer primary key autoincrement, y text);"
 "create table bulktest3 (id integer primary key autoincrement, z text);"
 "insert into bulktest1 (x) values ('XXX');"
 "insert into bulktest2 (y) values ('YYY');"
 "insert into bulktest3 (z) values ('ZZZ');";
 
 success = [db executeStatements:sql];
 
 sql = @"select count(*) as count from bulktest1;"
 "select count(*) as count from bulktest2;"
 "select count(*) as count from bulktest3;";
 
 success = [self.db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) {
 NSInteger count = [dictionary[@"count"] integerValue];
 XCTAssertEqual(count, 1, @"expected one record for dictionary %@", dictionary);
 return 0;
 }];

数据处理(Data Sanitization)
当为FMDB提供SQL语句时,在进行插入(insertion)之前不应该尝试处理任意的值,相反,应该使用标准SQLite的绑定语法: 

 INSERT INTO myTable VALUES (?, ?, ?, ?)
 ?字符被认为是SQLite中插入值的默认占位符,执行方法中的所有参数变量都可以使用。如在 Objective-C使用?:
 NSString *name = @"Liam O'Flaherty (\"the famous Irish author\")";
 NSDate *date = [NSDate date];
 NSString *comment = nil;
 
 BOOL success = [db executeUpdate:@"INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", @(identifier), name, date, comment ?: [NSNull null]];
 if (!success) {
 NSLog(@"error = %@", [db lastErrorMessage]);
 }
注意:对于基础数据类型,向NSInteger变量identifier,应该使用NSNumber对象,使用@语法成功包裹,如上面代码所示。或者使用[NSNumber numberWithInt:identifier]方法。
同样的,SQL中的NULL值应该被插入为[NSNull null]。例如:例子中的comment可能为nil,可以使用comment ?: [NSNull null]语法表示,如果comment不是nil,这将插入应该字符串,如果是nil,将插入[NSNull null]
 
swift中,使用executeUpdate(values:)方法,不仅仅需要精确的Swift语法,而且需要处理throws 异常情况,如下:
do {
    let identifier = 42
    let name = "Liam O'Flaherty (\"the famous Irish author\")"
    let date = NSDate()
    let comment: String? = nil

    try db.executeUpdate("INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", values: [identifier, name, date, comment ?? NSNull()])
} catch {
    print("error = \(error)")
}
注意:在Swift中,并没有包裹基本数字类型的操作,但是可以使用comment ?? NSNull()语法进行操作,如果为空,为nil,否则使用字符串。
也可以使用命名参数语法:
INSERT INTO authors (identifier, name, date, comment) VALUES (:identifier, :name, :date, :comment)
参数必须是以冒号(:)开始,SQLite它也支持其它符号,但是内部字典的key值是使用前缀冒号,字典key值并不包括冒号。
NSDictionary *arguments = @{@"identifier": @(identifier), @"name": name, @"date": date, @"comment": comment ?: [NSNull null]};
BOOL success = [db executeUpdate:@"INSERT INTO authors (identifier, name, date, comment) VALUES (:identifier, :name, :date, :comment)" withParameterDictionary:arguments];
if (!success) {
    NSLog(@"error = %@", [db lastErrorMessage]);
}

使用 FMDatabaseQueue(Using FMDatabaseQueue and Thread Safety)
在多线程中使用单一的FMDatabase实例是一种不好的idea,虽然为每一个线程创建一个FMDatabase实例是可以的。但是不适用,所有这里使用FMDatabaseQueue,该类的实例对象能够在多线程中进行,FMDatabaseQueue对象将同步多线程的访问,下面看看如何使用:

1:创建列队
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
2:使用如下
[queue inDatabase:^(FMDatabase *db) {
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @2];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @3];

    FMResultSet *rs = [db executeQuery:@"select * from foo"];
    while ([rs next]) {
        …
    }
}];
而且在事务中包裹内容非常简单,如下:
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @2];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @3];

    if (whoopsSomethingWrongHappened) {
        *rollback = YES;
        return;
    }
    // etc…
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @4];
}];
Swift中实现如下:
queue.inTransaction { db, rollback in
    do {
        try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [1])
        try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [2])
        try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [3])

        if whoopsSomethingWrongHappened {
            rollback.memory = true
            return
        }

        try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [4])
    } catch {
        rollback.memory = true
        print(error)
    }
}
FMDatabaseQueue将在串行列队运行block,所以如果在多线程同时调用FMDatabaseQueue的方法,他们将按顺序执行,不会造成混乱。

了解了上面的基本知识点,现在来看一个Demo,原文这里
该例子是使用Swift3.0编写程序,在Swift代码中导入FMDB进行数据的本地保存,DEMO单独创建了一个数据库管理类,并提供了基本的方法实现创建数据库,打开数据库,插入,删除,更新数据等基本操作。主要效果是一个tableView展示相关电影的列表页面,点击cell进入详情页面。相关页面效果如下:


由于DEMO本身比较简单,所以界面效果相关部分就不介绍了,主要是一起看一下数据库管理类的的代码实现:

首先看一下数据库管理类,看代码之前,了解一点基本思想:

1:与数据库进行工作,基本上都是:与数据库建立连接;加载或者修改存储的数据;最终关闭数据库等操作。
2:创建一个单独的类来处理以下事情:
1)使用FMDB API来与数据库进行交流,我们并没有必要多次写代码来确认是否实际的数据库文件存在,或者数据库是否被打开。
2)
实现数据库操作相关的方法,我们根据需求创建具体的自定义方法,当其他类需要使用数据的时候,我们将调用这些方法。

正如上面解释,我们即将创建一些列高等级的数据库API基于FMDB,但是所有使用的API都与我们APP的目的相关。为了给我们建立的类提供更大的便利性,我们将使用单例,保证整个工程只创建一个数据库对象进行相应的操作。由于管理类代码比较多,所以会分几个部分展示:

下面是创建了数据库管理类,并且用于相关的属性,在初始化的时候创建数据库文件路径:

class DBManager: NSObject {
    
    let field_MovieID = "movieID"
    let field_MovieTitle = "title"
    let field_MovieCategory = "category"
    let field_MovieYear = "year"
    let field_MovieURL = "movieURL"
    let field_MovieCoverURL = "coverURL"
    let field_MovieWatched = "watched"
    let field_MovieLikes = "likes"
    
    //创建单例对象
    static let shared: DBManager = DBManager()
    
    //数据库文件名,这并不是一定要作为属性,但是方便重用。
    let databaseFileName = "database.sqlite"
    //数据库文件的路径
    var pathToDatabase: String!
    //FMDatabase对象用于访问和操作实际的数据库
    var database: FMDatabase!
    
    override init() {
        super.init()
        //创建数据库文件路径
        let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
        pathToDatabase = documentDirectory.appending("/\(databaseFileName)")
    }
    //这里添加后续代码

}
这里提供了两个便利的方法来创建和打开数据库

//自定义创建数据库方法,返回布尔值,如果为true,那么数据库创建成功,否则失败
    func createDatabase() -> Bool{
        var created = false
        //如果数据库文件不存在那么就创建,存在就不创建
        if !FileManager.default.fileExists(atPath: pathToDatabase) {
            database = FMDatabase(path: pathToDatabase)
            if database != nil{
                //数据库是否被打开
                if database.open() {
                    //为数据库创建表,表中的相关属性都是依据MovieInfo结构体模型
                   let createMoviesTableQuery = "create table movies (\(field_MovieID) integer primary key autoincrement not null, \(field_MovieTitle) text not null, \(field_MovieCategory) text not null, \(field_MovieYear) integer not null, \(field_MovieURL) text, \(field_MovieCoverURL) text not null, \(field_MovieWatched) bool not null default 0, \(field_MovieLikes) integer not null)"
                    
                    do{
                       //执行查询,将为数据库创建新的表,这里需要使用try-catch来捕获异常
                       try database.executeUpdate(createMoviesTableQuery, values: nil)
                       //表创建成功,设置created为true
                       created = true
                    }catch{
                       print("Could not create table.")
                       print(error.localizedDescription)
                    }
                    
                    //关闭数据库
                    database.close()
                }else{
                  print("Could not open the database.")
                }
            }
        }
        return created
    }
    
    //打开数据库
    func openDatabase() -> Bool{
        //确认database对象是否被初始化,如果为nil,那么判断路径是否存在并创建
        if database == nil{
            if FileManager.default.fileExists(atPath: pathToDatabase){
               database = FMDatabase(path: pathToDatabase)
            }
        }
        //如果database对象存在,打开数据库,返回真,表示打开成功,否则数据库文件不存在或者发生了其它错误
        if database != nil{
            if database.open(){
               return true
            }
        }
      return false
    }
在创建表的时候,字段根据自己所建模型进行对应设置,当前demo使用结构体,如下:

//数据模型
struct MovieInfo {
    var movieID: Int!
    var title: String!
    var category: String!
    var year: Int!
    var movieURL: String!
    var coverURL: String!
    var watched: Bool!
    var likes: Int!
}

实现插入数据

//插入电影数据
    func insertMovieData(){
        if openDatabase(){
            if let pathToMoviesFile = Bundle.main.path(forResource: "movies", ofType: "tsv"){
                do{
                    //因为使用contentsOfFile初始化String可能出现异常,所以使用do-catch捕获异常
                    let moviesFileContents = try String(contentsOfFile: pathToMoviesFile)
                    //基于"\r\n"将字符串变成数组
                    let moviesData = moviesFileContents.components(separatedBy: "\r\n")
                    
                    var query = ""
                    for movie in moviesData{
                        let movieParts = movie.components(separatedBy: "\t")
                        if movieParts.count == 5{
                            let movieTitle = movieParts[0]
                            let movieCategory = movieParts[1]
                            let movieYear = movieParts[2]
                            let movieURL = movieParts[3]
                            let movieCoverURL = movieParts[4]
                            
                            //创建查寻语句,注意,每一个查寻语句最后使用分号(;)结束,因为我们想同时执行多条查寻语句,SQLite将基于分号来区别对应的查寻语句,而且对于values的每一个值,如果是字符串类型引用需要使用单引号括起来。最后两个值使用了默认值0
                            query += "insert into movies (\(field_MovieID), \(field_MovieTitle), \(field_MovieCategory), \(field_MovieYear), \(field_MovieURL), \(field_MovieCoverURL), \(field_MovieWatched), \(field_MovieLikes)) values (null, '\(movieTitle)', '\(movieCategory)', \(movieYear), '\(movieURL)', '\(movieCoverURL)', 0, 0);"
                        }
                    }
                    
                    //对于FMDB,同时执行多条查寻语句是非常容易的
                    if !database.executeStatements(query){
                        //打印插入操作所遭遇的问题
                        print("Failed to insert initial data into the database.")
                        print(database.lastError(), database.lastErrorMessage())
                    }
                }catch{
                    print(error.localizedDescription)
                }
            }
            //记得最后关闭数据库
            database.close()
        }
    }
实现加载所有数据

 //Loading Data  记得每一次操作都需要打开和关闭数据库
    func loadMovies() -> [MovieInfo]!{
        var movies: [MovieInfo]!
        
        if openDatabase(){
            //创建SQL查寻语句,加载数据,这里是基于field_MovieYear值的升序排列
            let query = "select * from movies order by \(field_MovieYear) asc"
            do{
                print(database)
                //执行SQL语句,该方法需要两个参数,第一个是查寻的语句,第二个是数组,数组中可以包含想查寻的值,并且返回FMResultSet对象,该对象包含了获取的值
                let results = try database.executeQuery(query, values: nil)
                //遍历查寻结果,创建MovieInfo实例对象,并添加到数组中
                while results.next() {
                     let movie = MovieInfo(movieID: Int(results.int(forColumn: field_MovieID)),
                                           title: results.string(forColumn: field_MovieTitle),
                                           category: results.string(forColumn: field_MovieCategory),
                                           year: Int(results.int(forColumn: field_MovieYear)),
                                           movieURL: results.string(forColumn: field_MovieURL),
                                           coverURL: results.string(forColumn: field_MovieCoverURL),
                                           watched:  results.bool(forColumn: field_MovieWatched),
                                           likes:  Int(results.int(forColumn: field_MovieLikes))
                                          )
                    if movies == nil{
                       movies = [MovieInfo]()
                    }
                    
                     movies.append(movie)
                }
            }catch{
                print(error.localizedDescription)
            }
              database.close()
        }
        return movies
    }
实现更新数据

  //通过电影的ID来查寻对应的电影数据,并通过闭包返回电影数据
    func loadMovie(withID ID:Int, completionHandler: (_ movieInfo: MovieInfo?) -> Void){
        var movieInfo: MovieInfo!
        
        if openDatabase(){
            //建立查寻语句
            let query = "select * from movies where \(field_MovieID)=?"
            
            do{
                //执行查寻
                let results = try database.executeQuery(query, values: [ID])
                //创建对象的数据模型对象
                if results.next() {
                    movieInfo = MovieInfo(movieID: Int(results.int(forColumn: field_MovieID)),
                                          title: results.string(forColumn: field_MovieTitle),
                                          category: results.string(forColumn: field_MovieCategory),
                                          year: Int(results.int(forColumn: field_MovieYear)),
                                          movieURL: results.string(forColumn: field_MovieURL),
                                          coverURL: results.string(forColumn: field_MovieCoverURL),
                                          watched: results.bool(forColumn: field_MovieWatched),
                                          likes: Int(results.int(forColumn: field_MovieLikes))
                    )
                    
                }
                else {
                    print(database.lastError())
                }
                
            }catch{
               print(error.localizedDescription)
            }
            //关闭数据库
           database.close()
        }
        //回调查寻的数据
        completionHandler(movieInfo)
    }
    
    //使用具体的电影数据更新数据库
    func updateMovie(withID ID: Int, watched: Bool, likes: Int){
        if openDatabase() {
            //创建更新语句 以电影的ID为准,更新数据
            let query = "update movies set \(field_MovieWatched)=?, \(field_MovieLikes)=? where \(field_MovieID)=?"
            
            do {
                //执行SQL语句
                try database.executeUpdate(query, values: [watched, likes, ID])
            }
            catch {
                print(error.localizedDescription)
            }
            database.close()
        }
    }
实现删除数据

//Delete Records
    func deleteMovie(withID ID: Int) -> Bool {
        var deleted = false
        
        if openDatabase() {
            //更具选中电影的ID,创建查寻语句
            let query = "delete from movies where \(field_MovieID)=?"
            
            do {
                //执行删除
                try database.executeUpdate(query, values: [ID])
                deleted = true
            }
            catch {
                print(error.localizedDescription)
            }
            //关闭数据库
            database.close()
        }
        
        return deleted
    }
参考:
对于想了解更多SQLite内容,可以看这里。
SQLite介绍和基本使用

你可能感兴趣的:(iOS)