1. 程式人生 > >最全iOS資料儲存方法介紹:FMDB,SQLite3 ,Core Data,Plist,Preference偏好設定,NSKeyedArchiver歸檔,Realm

最全iOS資料儲存方法介紹:FMDB,SQLite3 ,Core Data,Plist,Preference偏好設定,NSKeyedArchiver歸檔,Realm

專案準備運用的Core Data進行本地資料儲存,本來打算只寫一下Core Data的,不過既然說到了資料儲存,乾脆來個資料儲存基礎大總結!

本文將對以下幾個模組進行敘述。

  1. 沙盒
  2. Plist
  3. Preference偏好設定
  4. NSKeyedArchiver歸檔 / NSKeyedUnarchiver解檔
  5. SQLite3的使用
  6. FMDB
  7. Core Data

Realm的文件很詳細,這篇文章就不寫了: Realm的使用方法

下圖是Core Data堆疊的圖示,在這裡是為了做文章的封面圖片,後文會介紹Core Data的使用方法。

Core Data

一、沙盒

iOS本地化儲存的資料儲存在沙盒中, 並且每個應用的沙盒是相對獨立的。每個應用的沙盒檔案結構都是相同的,如下圖所示:

 

沙盒目錄

Documents:iTunes會備份該目錄。一般用來儲存需要持久化的資料。

Library/Caches:快取,iTunes不會備份該目錄。記憶體不足時會被清除,應用沒有執行時,可能會被清除,。一般儲存體積大、不需要備份的非重要資料。

Library/Preference:iTunes同會備份該目錄,可以用來儲存一些偏好設定。

tmp

: iTunes不會備份這個目錄,用來儲存臨時資料,應用退出時會清除該目錄下的資料。

如何拿到每個資料夾的路徑呢?我這裡就只說最好的一種方法。

// 這個方法返回的是一個數組,iOS中這個陣列其實只有一個元素,所以我們可以用lastObject或lastObject來拿到Documents目錄的路徑
NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
// 得到Document目錄下的test.plist檔案的路徑
NSString *filePath = [documentPath stringByAppendingPathComponent:@"test.plist"];

上面的方法有三個引數,這裡分別說一下。

NSDocumentDirectory: 第一個引數代表要查詢哪個檔案,是一個列舉,點進去看一下發現有很多選擇,為了直接找到沙盒中的Documents目錄,我們一般用NSDocumentDirectory。

NSUserDomainMask: 也是一個列舉,表示搜尋的範圍限制於當前應用的沙盒目錄。我們一般就選擇NSUserDomainMask。

YES (expandTilde): 第三個引數是一個BOOL值。iOS中主目錄的全寫形式是/User/userName,這個引數填YES就表示全寫,填NO就是寫成‘‘~’’,我們一般填YES。

根據上面的文字,你應用可以知道如何拿到Library/Caches目錄下的檔案路徑了吧?沒錯,就是這樣:

//獲取Library/Caches目錄路徑
NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 
//獲取Library/Caches目錄下的test.data檔案路徑
NSString *filePath = [path stringByAppendingPathComponent:@"test.data"];
//獲取temp路徑
NSString *tmp= NSTemporaryDirectory();
//獲取temp下test.data檔案的路徑
NSString *filePath = [tmp stringByAppendingPathComponent:@"test.data"];

所以,如果程式中有需要長時間持久化的資料,就選擇Documents,如果有體積大但是並不重要的資料,就可以選擇交給Library,而臨時沒用的資料當然是放到temp。至於Preference則可以用來儲存一些設定類資訊,後面會講到偏好設定的使用方法。

推薦一個好用的分類工具集合WHKit,可以直接使用分類方法拿到檔案路徑。

//一行程式碼搞定
NSString *filePath = [@"test.plist" appendDocumentPath];

//使用WHKit,上面這一行程式碼等同於下面這兩行。

NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [documentPath stringByAppendingPathComponent:@"test.plist"];

二、Plist儲存

Plist檔案的Type可以是字典NSDictionary或陣列NSArray,也就是說可以把字典或陣列直接寫入到檔案中。
NSString、NSData、NSNumber等型別,也可以使用writeToFile:atomically:方法直接將物件寫入檔案中,只是Type為空。

下面就舉個例子來看一下如何使用Plist來儲存資料。

//準備要儲存的資料
NSDictionary *dict = [NSDictionary dictionaryWithObject:@"first" forKey:@"1"];

//獲取路徑,你也可以利用上面說WHKit更優雅的拿到路徑
NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [documentPath stringByAppendingPathComponent:@"test.plist"];

//寫入資料
[dict writeToFile:filePath atomically:YES];

上面的程式碼執行之後,在應用沙盒的Documents中就建立了一個plist檔案,並且已經寫入資料儲存。

 

寫入plist

資料儲存了,那麼如何讀取呢?

//拿到plist檔案路徑,你也可以利用上面說WHKit更優雅的拿到路徑
NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [documentPath stringByAppendingPathComponent:@"test.plist"];

//解析資料,log出的結果為first,讀取成功
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSString *result = dict[@"1"];
NSLog(@"%@",result);

上面這段程式碼就讀出了plist種的資料。

三、Preference偏好設定

偏好設定的使用非常方便快捷,我們一般使用它來進行一些設定的記錄,比如使用者名稱,開關是否開啟等設定。
Preference是通過NSUserDefaults來使用的,是通過鍵值對的方式記錄設定。下面舉個例子。

利用NSUserDefaults判斷APP是不是首次啟動。

// 啟動的時候判斷key有沒有value,如果有,說明已經啟動過了,如果沒有,說明是第一次啟動
if (![[NSUserDefaults standardUserDefaults] valueForKey:@"first"]) {
        
    //如果是第一次啟動,就運用偏好設定給key設定一個value
    [[NSUserDefaults standardUserDefaults] setValue:@"start" forKey:@"first"];
    NSLog(@"是第一次啟動");
        
} else {
    NSLog(@"不是第一次啟動");
}

通過鍵值對的方式非常easy的儲存了資料。

注意:NSUserDefaults可以儲存的資料型別包括:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要儲存其他型別,則需要轉換為前面的型別,才能用NSUserDefaults儲存。

下面的例子是用NSUserDefaults儲存圖片,需要先把圖片轉換成NSData型別。

UIImage *image=[UIImage imageNamed:@"photo"];
//UIImage物件轉換成NSData
NSData *imageData = UIImageJPEGRepresentation(image, 100);

//偏好設定可以儲存NSData,但是不能儲存UIimage
[[NSUserDefaults standardUserDefaults] setObject:imageData forKey:@"image"];
    
// 讀出data
NSData *getImageData = [[NSUserDefaults standardUserDefaults] dataForKey:@"image"];
//NSData轉換為UIImage
UIImage *Image = [UIImage imageWithData:imageData];

NSUserDefaults是不是很好用!不過有沒有覺得每次都寫[NSUserDefaults standardUserDefaults]有點煩,那麼又到了推薦環節,好用的分類工具WHKit,這個工具中有許多好用的巨集定義,其中一個就是KUSERDEFAULT,等同於[NSUserDefaults standardUserDefaults]。
WHKit中還有更多好用的巨集定義等著你!

四、NSKeyedArchiver歸檔 / NSKeyedUnarchiver解檔

歸檔和解檔會在寫入、讀出資料之前進行序列化、反序列化,資料的安全性相對高一些。
NSKeyedArchiver可以有三個使用情景。

1.對單個簡單物件進行歸檔/解檔

與plist差不多,對於簡單的資料進行歸檔,直接寫入檔案路徑。

//獲取歸檔檔案路徑
NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [documentPath stringByAppendingPathComponent:@"test"];

//對字串@”test”進行歸檔,寫入到filePath中
[NSKeyedArchiver archiveRootObject:@"test" toFile:filePath];

//根據儲存資料的路徑filePath解檔資料
NSString *result = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
//log結構為@”test”,就是上面歸檔的資料
NSLog(@"%@",result);

當然,也可以儲存NSArray,NSDictionary等物件。

2.對多個物件進行歸檔/解檔

這種情況可以一次儲存多種不同型別的資料,最終使用的是與plist相同的writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile來寫入資料。

    //獲取歸檔路徑
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    NSString *filePath = [documentPath stringByAppendingPathComponent:@"test"];
    
    //用來承載資料的NSMutableData
    NSMutableData *data = [[NSMutableData alloc] init];
    //歸檔物件
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    
    //將要被儲存的三個資料
    NSString *name = @"jack";
    int age = 17;
    double height = 1.78;
    
    //運用encodeObject:方法歸檔資料
    [archiver encodeObject:name forKey:@"name"];
    [archiver encodeInt:age forKey:@"age"];
    [archiver encodeDouble:height forKey:@"height"];
    //結束歸檔
    [archiver finishEncoding];
    //寫入資料(儲存資料)
    [data writeToFile:filePath atomically:YES];
    
    
    //NSMutableData用來承載解檔出來的資料
    NSMutableData *resultData = [[NSMutableData alloc] initWithContentsOfFile:filePath];
    //解檔物件
    NSKeyedUnarchiver *unArchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:resultData];
    
    //分別解檔出三個資料
    NSString *resultName = [unArchiver decodeObjectForKey:@"name"];
    int resultAge = [unArchiver decodeIntForKey:@"age"];
    double resultHeight = [unArchiver decodeDoubleForKey:@"height"];
    //結束解檔
    [unArchiver finishDecoding];
    //成功打印出結果,說明成功歸檔解檔
    NSLog(@"name = %@, age = %d, height = %.2f",resultName,resultAge,resultHeight);

3.歸檔儲存自定義物件

我認為這是歸檔最常使用的情景。
定義一個Person類,如果想對person進行歸檔解檔,首先要讓Person遵守<NSCoding>協議。

/********Person.h*********/

#import <Foundation/Foundation.h>

//遵守NSCoding協議
@interface Person : NSObject<NSCoding>

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

//自定義的歸檔儲存資料的方法
+(void)savePerson:(Person *)person;

//自定義的讀取沙盒中解檔出的資料
+(Person *)getPerson;

@end

NSCoding協議有2個方法:

  • (void)encodeWithCoder:(NSCoder *)aCoder
    歸檔時呼叫這個方法,在方法中使用encodeObject:forKey:歸檔變數。
  • (instancetype)initWithCoder:(NSCoder *)aDecoder
    解檔時呼叫這個方法,在方法中石油decodeObject:forKey讀出變數。
/******Person.m*******/

#import "Person.h"

@implementation Person

//歸檔,Key建議使用巨集代替,這裡就不使用了
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeInteger:self.age forKey:@"age"];
}

//解檔
-(instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self=[super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.age = [aDecoder decodeIntegerForKey:@"age"];
    }
    return self;
}

//類方法,運用NSKeyedArchiver歸檔資料
+(void)savePerson:(Person *)person {
    NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    NSString *path=[docPath stringByAppendingPathComponent:@"Person.plist"];
    [NSKeyedArchiver archiveRootObject:person toFile:path];
}

//類方法,使用NSKeyedUnarchiver解檔資料
+(Person *)getPerson {
    NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    NSString *path=[docPath stringByAppendingPathComponent:@"Person.plist"];
    Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    return person;
}

@end

下面就可以在需要的地方歸檔或解檔Person物件。

/*******ViewController.m*******/

    //建立Person物件
    Person *person = [Person new];
    person.name = @"jack";
    person.age = 17;
    //歸檔儲存資料
    [Person savePerson:person];
    //解檔拿到資料
    Person *resultPerson = [Person getPerson];
    //打印出結果,證明歸檔解檔成功
    NSLog(@"name = %@, age = %ld",resultPerson.name,resultPerson.age);

五、SQLite3的使用

1、首先需要新增庫檔案libsqlite3.0.tbd
2、匯入標頭檔案#import <sqlite3.h>
3、開啟資料庫
4、建立表
5、對資料表進行增刪改查操作
6、關閉資料庫

上程式碼之前,有些問題你需要了解。

  • SQLite 不區分大小寫,但也有需要注意的地方,例如GLOB 和 glob 具有不同作用。

  • SQLite3有5種基本資料型別 text、integer、float、boolean、blob

  • SQLite3是無型別的,在建立的時候你可以不宣告欄位的型別,不過還是建議加上資料型別

 create table t_student(name, age);
 create table t_student(name text, age integer);

下面的程式碼就是SQLite3的基本使用方法,帶有詳細註釋。
程式碼中用到了一個Student類,這個類有兩個屬性name和age。

/****************sqliteTest.m****************/

#import "sqliteTest.h"
//1.首先匯入標頭檔案
#import <sqlite3.h>

//2.資料庫
static sqlite3 *db;

@implementation sqliteTest

/** 3.開啟資料庫 */
+ (void)openSqlite {
    
    //資料庫已經開啟
    if (db != nil) {
        NSLog(@"資料庫已經開啟");
        return;
    }
    
    //建立資料檔案路徑
    NSString *string = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    NSString *path = [string stringByAppendingPathComponent:@"Student.sqlite"];
    NSLog(@"%@",path);
    
    //開啟資料庫
    int result = sqlite3_open(path.UTF8String, &db);
    
    if (result == SQLITE_OK) {
        NSLog(@"資料庫開啟成功");
    } else {
        NSLog(@"資料庫開啟失敗");
    }
}

/** 4.建立表 */
+ (void)createTable {
    
    //建立表的SQLite語句,其中id是主鍵,not null 表示在表中建立紀錄時這些欄位不能為NULL
    NSString *sqlite = [NSString stringWithFormat:@"create table if not exists t_student (id integer primary key autoincrement, name text not null, age integer)"];
    
    //用來記錄錯誤資訊
    char *error = NULL;
    
    //執行SQLite語句
    int result = sqlite3_exec(db, sqlite.UTF8String, nil, nil, &error);
    
    if (result == SQLITE_OK) {
        NSLog(@"建立表成功");
    }else {
        NSLog(@"建立表失敗");
    }
}

/** 5.新增資料 */
+ (void)addStudent:(Student *)stu {
    
    //增添資料的SQLite語句
    NSString *sqlite = [NSString stringWithFormat:@"insert into t_student (name,age) values ('%@','%ld')",stu.name,stu.age];
    char *error = NULL;
    
    int result = sqlite3_exec(db, [sqlite UTF8String], nil, nil, &error);
    
    if (result == SQLITE_OK) {
        NSLog(@"新增資料成功");
    } else {
        NSLog(@"新增資料失敗");
    }
}

/** 6.刪除資料 */
+ (void)deleteStuWithName:(NSString *)name {
    
    //刪除特定資料的SQLite語句
    NSString *sqlite = [NSString stringWithFormat:@"delete from t_student where name = '%@'",name];
    char *error = NULL;
    
    int result = sqlite3_exec(db, sqlite.UTF8String, nil, nil, &error);
    
    if (result == SQLITE_OK) {
        NSLog(@"刪除資料成功");
    } else {
        NSLog(@"刪除資料失敗");
    }
}

/** 7.更改資料 */
+ (void)upDateWithStudent:(Student *)stu WhereName:(NSString *)name {
    
    //更新特定欄位的SQLite語句
    NSString *sqlite = [NSString stringWithFormat:@"update t_student set name = '%@', age = '%ld' where name = '%@'",stu.name,stu.age,name];
    char *error = NULL;
    
    int result = sqlite3_exec(db, sqlite.UTF8String, nil, nil, &error);
    
    if (result == SQLITE_OK) {
        NSLog(@"修改資料成功");
    } else {
        NSLog(@"修改資料失敗");
    }
}

/** 8.根據條件查詢 */
+ (NSMutableArray *)selectWithAge:(NSInteger)age {
    
    //可變陣列,用來儲存查詢到的資料
    NSMutableArray *array = [NSMutableArray array];
    
    //查詢所有資料的SQLite語句
    NSString *sqlite = [NSString stringWithFormat:@"select * from t_student where age = '%ld'",age];
    
    //定義一個stmt存放結果集
    sqlite3_stmt *stmt = NULL;
    
    //執行
    int result = sqlite3_prepare(db, sqlite.UTF8String, -1, &stmt, NULL);
    
    if (result == SQLITE_OK) {
        NSLog(@"查詢成功");
        
        //遍歷查詢到的所有資料,並新增到上面的陣列中
        while (sqlite3_step(stmt) == SQLITE_ROW) {
            Student *stu = [[Student alloc] init];
            //獲得第1列的姓名,第0列是id
            stu.name = [NSString stringWithUTF8String:(const char *)sqlite3_column_text(stmt, 1)];
            //獲得第2列的年齡
            stu.age = sqlite3_column_int(stmt, 2);
            [array addObject:stu];
        }
    } else {
        NSLog(@"查詢失敗");
    }
    
    //銷燬stmt,防止記憶體洩漏
    sqlite3_finalize(stmt);
    return array;
}

/** 9.查詢所有資料 */
+ (NSMutableArray *)selectStudent {
    
    //可變陣列,用來儲存查詢到的資料
    NSMutableArray *array = [NSMutableArray array];
    
    //查詢所有資料的SQLite語句
    NSString *sqlite = [NSString stringWithFormat:@"select * from t_student"];
    
    //定義一個stmt存放結果集
    sqlite3_stmt *stmt = NULL;
    
    //執行
    int result = sqlite3_prepare(db, sqlite.UTF8String, -1, &stmt, NULL);
    
    if (result == SQLITE_OK) {
        NSLog(@"查詢成功");
        
        //遍歷查詢到的所有資料,並新增到上面的陣列中
        while (sqlite3_step(stmt) == SQLITE_ROW) {
            Student *stu = [[Student alloc] init];
            //獲得第1列的姓名,第0列是id
            stu.name = [NSString stringWithUTF8String:(const char *)sqlite3_column_text(stmt, 1)];
            //獲得第2列的年齡
            stu.age = sqlite3_column_int(stmt, 2);
            [array addObject:stu];
        }
    } else {
        NSLog(@"查詢失敗");
    }
    
    //銷燬stmt,防止記憶體洩漏
    sqlite3_finalize(stmt);
    return array;
}

/** 10.刪除表中的所有資料 */
+ (void)deleteAllData {
    NSString *sqlite = [NSString stringWithFormat:@"delete from t_student"];
    char *error = NULL;
    int result = sqlite3_exec(db, sqlite.UTF8String, nil, nil, &error);
    if (result == SQLITE_OK) {
        NSLog(@"清除資料庫成功");
    } else {
        NSLog(@"清除資料庫失敗");
    }
}

/** 11.刪除表 */
+ (void)dropTable {
    NSString *sqlite = [NSString stringWithFormat:@"drop table if exists t_student"];
    char *error = NULL;
    int result = sqlite3_exec(db, sqlite.UTF8String, nil, nil, &error);
    if (result == SQLITE_OK) {
        NSLog(@"刪除表成功");
    } else {
        NSLog(@"刪除表失敗");
    }
}

/** 12.關閉資料庫 */
+ (void)closeSqlite {
    int result = sqlite3_close(db);
    if (result == SQLITE_OK) {
        NSLog(@"資料庫關閉成功");
    } else {
        NSLog(@"資料庫關閉失敗");
    }
}

附上SQLite的基本語句

  • 建立表:
    create table if not exists 表名 (欄位名1, 欄位名2...);

create table if not exists t_student (id integer primary key autoincrement, name text not null, age integer)

  • 增加資料:
    insert into 表名 (欄位名1, 欄位名2, ...) values(欄位1的值, 欄位2的值, ...);

insert into t_student (name,age) values (@"Jack",@17);

  • 根據條件刪除資料:
    delete from 表名 where 條件;

delete from t_student where name = @"Jack";

  • 刪除表中所有的資料:
    delete from 表名

delete from t_student

  • 根據條件更改某個資料:
    update 表名 set 欄位1 = '值1', 欄位2 = '值2' where 欄位1 = '欄位1的當前值'

update t_student set name = 'lily', age = '16' where name = 'Jack'

  • 根據條件查詢:
    select * from 表名 where 欄位1 = '欄位1的值'

select * from t_student where age = '16'

  • 查詢所有資料:
    select * from 表名

select * from t_student

  • 刪除表:
    drop table 表名

drop table t_student

  • 排序查詢:
    select * from 表名 order by 欄位

select * from t_student order by age asc (升序,預設)
select * from t_student order by age desc (降序)

  • 限制:
    select * from 表名 limit 值1, 值2

select * from t_student limit 5, 10 (跳過5個,一共取10個數據)

SQLite3還有事務方面的使用,這裡就不做說明了,下面的FMDB中會有事務的使用。

六、FMDB

FMDB封裝了SQLite的C語言API,更加面向物件。
首先需要明確的是FMDB中的三個類。

FMDatabase:可以理解成一個數據庫。

FMResultSet:查詢的結果集合。

FMDatabaseQueue:運用多執行緒,可執行多個查詢、更新。執行緒安全。

FMDB基本語法

查詢:executeQuery: SQLite語句命令。

[db executeQuery:@"select id, name, age from t_person"]

其餘的操作都是“更新”:executeUpdate: SQLite語句命令。

// CREATE, UPDATE, INSERT, DELETE, DROP,都使用executeUpdte
[db executeUpdate:@"create table if not exists t_person (id integer primary key autoincrement, name text, age integer)"]

FMDB的基本使用

在專案中匯入FMDB框架和sqlite3.0.tbd,匯入標頭檔案。

1. 開啟資料庫,並建立表

初始化FMDatabase:FMDatabase *db = [FMDatabase databaseWithPath:filePath];
其中的filePath是提前準備好要存放資料的路徑。

開啟資料庫:[db open]

建立資料表:[db executeUpdate:@"create table if not exists t_person (id integer primary key autoincrement, name text, age integer)"];

#import "ViewController.h"
#import <FMDB.h>

@interface ViewController ()

@end

@implementation ViewController{
    FMDatabase *db;
}

- (void)openCreateDB {

    //存放資料的路徑
    NSString *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
    NSString *filePath = [path stringByAppendingPathComponent:@"person.sqlite"];
    
    //初始化FMDatabase
    db = [FMDatabase databaseWithPath:filePath];
    
    //開啟資料庫並建立person表,person中有主鍵id,姓名name,年齡age
    if ([db open]) {
        BOOL success = [db executeUpdate:@"create table if not exists t_person (id integer primary key autoincrement, name text, age integer)"];
        if (success) {
            NSLog(@"創表成功");
        }else {
            NSLog(@"建立表失敗");
        }
    } else {
        NSLog(@"開啟失敗");
    }
}

2. 插入資料

運用executeUpdate方法執行插入資料命令: [db executeUpdate:@"insert into t_person(name,age) values(?,?)",@"jack",@17]

-(void)insertData {
    BOOL success = [db executeUpdate:@"insert into t_person(name,age) values(?,?)",@"jack",@17];
    if (success) {
        NSLog(@"新增資料成功");
    } else {
        NSLog(@"新增資料失敗");
    }
}

3. 刪除資料

刪除姓名為lily的資料:[db executeUpdate:@"delete from t_person where name = 'lily'"]

-(void)deleteData {
    BOOL success = [db executeUpdate:@"delete from t_person where name = 'lily'"];
    if (success) {
        NSLog(@"刪除資料成功");
    } else {
        NSLog(@"刪除資料失敗");
    }
}

4. 修改資料

把年齡為17歲的資料,姓名改為lily:[db executeUpdate:@"update t_person set name = 'lily' where age = 17"]

-(void)updateData {
    BOOL success = [db executeUpdate:@"update t_person set name = 'lily' where age = 17"];
    if (success) {
        NSLog(@"更新資料成功");
    } else {
        NSLog(@"更新資料失敗");
    }
}

5. 查詢資料

執行查詢語句,用FMResultSet接收查詢結果:FMResultSet *set = [db executeQuery:@"select id, name, age from t_person"]

遍歷查詢結果:[set next]

拿到每條數的姓名:NSString *name = [set stringForColumnIndex:1];

也可以這樣拿到每條資料的姓名:NSString *name = [result stringForColumn:@"name"];

    FMResultSet *set = [db executeQuery:@"select id, name, age from t_person"];
    while ([set next]) {
        int ID = [set intForColumnIndex:0];
        NSString *name = [set stringForColumnIndex:1];
        int age = [set intForColumnIndex:2];
        NSLog(@"%d,%@,%d",ID,name,age);
    }
}

6. 刪除表

刪除指定表:[db executeUpdate:@"drop table if exists t_person"]

-(void)dropTable {
    BOOL success = [db executeUpdate:@"drop table if exists t_person"];
    if (success) {
        NSLog(@"刪除表成功");
    } else {
        NSLog(@"刪除表失敗");
    }
}

FMDatabaseQueue基本使用

FMDatabase是執行緒不安全的,當FMDB資料儲存想要使用多執行緒的時候,FMDatabaseQueue就派上用場了。

初始化FMDatabaseQueue的方法與FMDatabase類似

//資料檔案路徑
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"student.sqlite"];
    
//初始化FMDatabaseQueue
FMDatabaseQueue *dbQueue = [FMDatabaseQueue databaseQueueWithPath:filePath];

在FMDatabaseQueue中執行命令的時候也是非常方便,直接在一個block中進行操作

-(void)FMDdatabaseQueueFunction {
    
    NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    //資料檔案路徑
    NSString *filePath = [path stringByAppendingPathComponent:@"student.sqlite"];
    //初始化FMDatabaseQueue
    FMDatabaseQueue *dbQueue = [FMDatabaseQueue databaseQueueWithPath:filePath];
    
    //在block中執行SQLite語句命令
    [dbQueue inDatabase:^(FMDatabase * _Nonnull db) {
        //建立表
        [db executeUpdate:@"create table if not exists t_student (id integer primary key autoincrement, name text, age integer)"];
        //新增資料
        [db executeUpdate:@"insert into t_student(name,age) values(?,?)",@"jack",@17];
        [db executeUpdate:@"insert into t_student(name,age) values(?,?)",@"lily",@16];
        //查詢資料
        FMResultSet *set = [db executeQuery:@"select id, name, age from t_student"];
        //遍歷查詢到的資料
        while ([set next]) {
            int ID = [set intForColumn:@"id"];
            NSString *name = [set stringForColumn:@"name"];
            int age = [set intForColumn:@"age"];
            NSLog(@"%d,%@,%d",ID,name,age);
        }
        
    }];
}

FMDB中的事務

什麼是事務?

事務(Transaction)是不可分割的一個整體操作,要麼都執行,要麼都不執行。
舉個例子,幼兒園有20位小朋友由老師組織出去春遊,返校的時候,所有人依次登上校車,這時候如果有一位小朋友沒有上車,車也是不能出發的。所以哪怕19人都上了車,也等於0人上車。20人是一個整體。
當然這個例子可能不是很精準。

FMDB中有事務的回滾操作,也就是說,當一個整體事務在執行的時候出了一點小問題,則執行回滾,之後這套事務中的所有操作將整體無效。

下面程式碼中,利用事務迴圈向資料庫中新增2000條資料,假如在新增的過程中出現了一些問題,由於執行了*rollback = YES的回滾操作,資料庫中一個數據都不會出現。
如果第2000條資料的添加出了問題,哪怕之前已經添加了1999條資料,由於執行了回滾,資料庫中依然一個數據都沒有。

//資料庫路徑
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"student.sqlite"];

//初始化FMDatabaseQueue
FMDatabaseQueue *dbQueue = [FMDatabaseQueue databaseQueueWithPath:filePath];

//FMDatabaseQueue的事務inTransaction
[dbQueue inTransaction:^(FMDatabase * _Nonnull db, BOOL * _Nonnull rollback) {
        //建立表
        [db executeUpdate:@"create table if not exists t_student (id integer primary key autoincrement, name text, age integer)"];
        //迴圈新增2000條資料
        for (int i = 0; i < 2000; i++) {
            BOOL success = [db executeUpdate:@"insert into t_student(name,age) values(?,?)",@"jack",@(i)];
            //如果新增資料出現問題,則回滾
            if (!success) {
                //資料回滾
                *rollback = YES;
                return;
            }
        }
    }];

七、Core Data

Core Data有著圖形化的操作介面,並且是操作模型資料的,更加面向物件。當你瞭解並熟悉它的時候,相信你會喜歡上這個資料庫儲存框架。

利用Core Data快速實現資料儲存

1. 圖形化建立模型

建立專案的時候,勾選下圖中的Use Core Data選項,工程中會自動建立一個數據模型檔案。當然,你也可以在開發中自己手動建立。

自動建立模型檔案

下圖就是自動創建出來的檔案

創建出來的檔案

如果沒有勾選,也可以在這裡手動建立。

手動建立

點選Add Entity之後,相當一張資料表。表的名稱自己在上方定義,注意首字母要大寫。
在介面中還可以為資料實體新增屬性和關聯屬性。

建立一個數據表

Core Data屬性支援的資料型別如下

資料型別

編譯之後,Xcode會自動生成Person的實體程式碼檔案,並且檔案不會顯示在工程中,如果下圖中右側Codegen選擇Manual/None,則Xcode就不會自動生成程式碼,我們可以自己手動生成。

6.png

手動生成實體類程式碼,選中CoreDataTest.xcdatamodeld檔案,然後在Mac選單欄中選擇Editor,如下圖所示。一路Next就可以了。
如果沒有選擇Manual/None,依然進行手動建立的話,則會與系統自動建立的檔案發生衝突,這點需要注意。
你也可以不要選擇Manual/None,直接使用系統建立好的NSManagedObject,同樣會有4個檔案,只是在工程中是看不到的,使用的時候直接匯入#import "Person+CoreDataClass.h"標頭檔案就可以了。

手動建立NSManagedObject

手動創建出來的是這樣4個檔案

10.png

還要注意程式語言的選擇,Swift或OC

程式語言

2.Core Data堆疊的介紹與使用

下面要做的就是對Core Data進行初始化,實現本地資料的儲存。
需要用到的類有三個:

NSManagedObjectModel 資料模型的結構資訊
NSPersistentStoreCoordinator 資料持久層和物件模型協調器
NSManagedObjectContext 物件的上下文managedObject 模型

如下圖所示,一個context內可以有多個模型物件,不過在大多數的操作中只存在一個context,並且所有的物件存在於那個context中。
物件和他們的context是相關聯的,每個被管理的物件都知道自己屬於哪個context,每個context都知道自己管理著哪些物件。

Core Data從系統讀或寫的時候,有一個持久化儲存協調器(persistent store coordinator),並且這個協調器在檔案系統中與SQLite資料庫互動,也連線著存放模型的上下文Context。

Core Data

下圖是比較常用的方式:

Core Data堆疊

下面我們來一步步實現Core Data堆疊的建立。

  • 首先在AppDelegate中定義一個NSManagedObjectModel屬性。然後利用懶載入來建立NSManagedObjectModel物件。並且要注意建立時候的字尾用momd,程式碼如下:
//建立屬性
@property (nonatomic, readwrite, strong) NSManagedObjectModel *managedObjectModel;

//懶載入
- (NSManagedObjectModel *)managedObjectModel {
    if (!_managedObjectModel) {
        //注意副檔名為 momd
        NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreDataTest" withExtension:@"momd"];
        _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    }
    return _managedObjectModel;
}
  • 建立協調器NSPersistentStoreCoordinator,同樣的先在AppDelegate中來一個屬性,然後懶載入。
//屬性
@property (nonatomic, readwrite, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;

//懶載入
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    
    if (!_persistentStoreCoordinator) {
        
        //傳入之前建立好了的Model
        _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
        
        //指定sqlite資料庫檔案
        NSURL *sqliteURL = [[self documentDirectoryURL] URLByAppendingPathComponent:@"CoreDataTest.sqlite"];
        
        //這個options是為了進行資料遷移用的,有興趣的可以去研究一下。
        NSDictionary *[email protected]{NSMigratePersistentStoresAutomaticallyOption:@(YES),NSInferMappingModelAutomaticallyOption:@(YES)};
        NSError *error;
        
        [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                  configuration:nil
                                                            URL:sqliteURL
                                                        options:options
                                                          error:&error];
        if (error) {
            NSLog(@"建立協調器失敗: %@", error.localizedDescription);
        }
    }
    return _persistentStoreCoordinator;
}


//獲取document目錄
- (nullable NSURL *)documentDirectoryURL {
    return [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
}
  • 建立NSManagedObjectContext,同樣的是屬性+懶載入。
//屬性
@property (nonatomic, readwrite, strong) NSManagedObjectContext *context;

//懶載入
- (NSManagedObjectContext *)context {
    if (!_context) {
        
        _context = [[NSManagedObjectContext alloc ] initWithConcurrencyType:NSMainQueueConcurrencyType];
        
        // 指定協調器
        _context.persistentStoreCoordinator = self.persistentStoreCoordinator;
    }
    return _context;
}

3.運用Core Data對資料進行增刪改查

  • 新增資料
    使用NSEntityDesctiption類的一個方法建立NSManagedObject物件。引數一是實體類的名字,引數二是之前建立的Context。
    為物件賦值,然後儲存。
@implementation ViewController {
    NSManagedObjectContext *context;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //拿到managedObjectContext
    context = [AppDelegate new].context;
    
    //運用NSEntityDescription建立NSManagedObject物件
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context];
    //為物件賦值
    person.name = @"Jack";
    person.age = 17;
    NSError *error;
    //儲存到資料庫
    [context save:&error];
}
  • 查詢資料
    Core Data從資料庫中查詢資料,會用到三個類:

NSFetchRequest:一條查詢請求,相當於 SQL 中的select語句
NSPredicate:謂詞,指定一些查詢條件,相當於 SQL 中的where
NSSortDescriptor:指定排序規則,相當於 SQL 中的 order by

NSFetchRequest中有兩個屬性:

predicate:是NSPredicate物件
sortDescriptors:它是一個NSSortDescriptor陣列,陣列中前面的優先順序比後面高。可以有多個排列規則。

    //更多NSFetchRequest屬性
    fetchLimit:結果集最大數,相當於 SQL 中的limit
    fetchOffset:查詢的偏移量,預設為0
    fetchBatchSize:分批處理查詢的大小,查詢分批返回結果集
    entityName/entity:資料表名,相當於 SQL中的from
    propertiesToGroupBy:分組規則,相當於 SQL 中的group by
    propertiesToFetch:定義要查詢的欄位,預設查詢全部欄位

設定好NSFetchRequest之後,呼叫NSManagedObjectContext的executeFetchRequest方法,就會返回結果集了。

    //Xcode自動建立的NSManagedObject會生成fetchRequest方法,可以直接得到NSFetchRequest
    NSFetchRequest *fetchRequest = [Person fetchRequest];
    //也可以這樣獲得:[NSFetchRequest fetchRequestWithEntityName:@"Student"];
    
    //謂詞
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"age == %@", @(16)];
    
    //排序
    NSArray<NSSortDescriptor *> *sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]];
    
    fetchRequest.sortDescriptors = sortDescriptors;

    //運用executeFetchRequest方法得到結果集
    NSArray<Person *> *personResult = [context executeFetchRequest:fetchRequest error:nil];

Xcode自己有一個NSFetchRequest的code snippet,“fetch”,出現的結果如下圖。

snippet

NSFetchRequest的code snippet: “fetch”

  • 更新資料
    更新資料比較簡單,查詢出來需要修改的資料之後,直接修改值,然後用context save就可以了。
for (Person *person in personResult) {
        //直接修改
        person.age = 26;
    }

//別忘了save一下
[context save:&error];
  • 刪除資料
    查詢出來需要刪除的資料之後,呼叫 NSManagedObjectContext 的deleteObject方法就可以了。
for (Person *person in personResult) {
        //刪除資料
        [context deleteObject:person];
    }
//別忘了save
[context save:&error];

至此Core Data的基礎內容就講完了。


後記:

iOS中有多種資料持久化的方法,要根據具體情景選用最合適的技術。

推薦簡單又好用的分類集合:WHKit
github地址:https://github.com/remember17

 



作者:remember17
連結:https://www.jianshu.com/p/e88880be794f
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。