iOS 数据持久化

1.简介

数据持久存储是一种非易失性存储,在重启动计算机或设备后也不会丢失数据。持久化技术主要用于MVC模型中的model层。其中目前再IOS平台上主要使用如下的四种技术:

  • 属性列表
  • 对象归档
  • SQLite3
  • Core Data
2.沙盒(SandBox)

IOS中的沙盒机制(SandBox)是一种安全体系,它规定了应用程序只能在为该应用创建的文件夹内读取文件,不可以访问其他地方的内容。所有的非代码文件都保存在这个地方,比如图片、声音、属性列表和文本文件等。

沙盒结构

每个应用程序沙盒主要包含以下三个目录:

【注】:也包含其他目录,但不设计数据持久化。比如:AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。

****1) Documents:****
    应用程序可以将数据存储在Documents目录中。在此目录中的文件可以被共享,其中本文中的4种数据持久化技术都涉及该目录。
    ****2) Library:****
    这个目录下有两个子目录: ****Caches 和 Preferences ****
    ****① Preferences**** 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好。
    ****② Caches**** 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。
    ****3)Tmp:****
    Tmp目录供应用存储临时文件。在不需要这些文件时,应用要负责删除tmp中等待文件,以免占用文件系统的空间。

获取目录

获取沙盒主路径:NSHomeDirectory()
获取Document路径:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]
获取Library路径:[NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject]
ps:第一个参数指明要查找的内容,第二参数指明查找的范围,其中返回值说明返回的是数组,但是由于在沙盒中只有一个documents,或是只有一个library,那么返回的数组只有一个元素,我们只需取得第一个元素即可。
获取Temp路径:NSTemporaryDirectory( )
获取应用包路径:[[NSBundle mainBundle] pathForAuxiliaryExecutable:@”“]
3.NSFileManager 文件管理类

1)NSFileManager可以完成沙盒路径下的文件管理工作,包括目录创建、文件创建、删除、移动、复制等。
2)NSFileManager使用单例方法访问:
  NSFileManager *fileManager = [NSFileManager defaultManager];
3)判断指定路径下是否存在文件:

- (BOOL)fileExistsAtPath:(NSString *)path;
- (BOOL)fileExistsAtPath:(NSString *)path isDirectory:(BOOL *)isDirectory;

4)文件处理方法:

// 1)创建文件
- (BOOL)createFileAtPath:(NSString *)path contents:(NSData *)data attributes:(NSDictionary *)attr;
// 2)创建目录
- (BOOL)createDirectoryAtPath:(NSString *)path attributes:(NSDictionary *)attributes;
// 3)复制文件
- (BOOL)copyItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
// 4)移动文件
- (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
// 5)删除文件
- (BOOL)removeItemAtPath:(NSString *)path error:(NSError *)error;
4、NSUserDefaults(偏好设置)

NSUserDefaults单例以key-value的形式存储了一系列偏好设置,key是名称,value是相应的数据。存取数据时可以使用方法objectForKey:和setObject:forKey:来把对象存储到相应的plist文件中,或者读取,既然是plist文件,那么对象的类型则必须是plist文件可以存储的类型:

  • NSNumber(NSInteger、float、double)
  • NSString
  • NSDate
  • NSArray
  • NSDictionary
  • BOOL

1)NSUserDefaults 存储数据:

- (void)setObject:(id)value forKey:(NSString *)defaultName;

- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName;

- (void)setFloat:(float)value forKey:(NSString *)defaultName;

- (void)setDouble:(double)value forKey:(NSString *)defaultName;

- (void)setBool:(BOOL)value forKey:(NSString *)defaultName;

- (void)setURL:(NSURL *)url forKey:(NSString *)defaultName;

2)NSUserDefaults 读取数据:

- (id)objectForKey:(NSString *)defaultName;

- (NSInteger)integerForKey:(NSString *)defaultName;

- (float)floatForKey:(NSString *)defaultName;

- (double)doubleForKey:(NSString *)defaultName;

- (BOOL)boolForKey:(NSString *)defaultName;

- (NSURL *)URLForKey:(NSString *)defaultName;

3)NSUserDefaults同步数据到文件系统:
- (BOOL)synchronize;

代码示例:

// 如果想要将数据永久保存到NSUserDefaults中,代码实现为:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

[defaults setObject:@”li“ forKey:@"name"];
[defaults setInteger:10 forKey:@"age"];
[defaults synchronize];

ps:其中,方法synchronise是为了强制存储,其实并非必要,因为这个方法会在系统中默认调用,但是你确认需要马上就存储,这样做是可行的。

// 将数据取出,只需要取出key 对应的值就好了,代码如下:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

NSString *name = [defaults objectForKey:@"name"]
NSInteger age = [defaults integerForKey:@"age"];
5、属性列表

属性列表文件是一种xml文件,拓展名为plist(Property List),Foundation框架中的数组和字典都可以与属性列表文件互相转换。简单的说就是调用数组或字典的方法(read或write)进行xml文件的读或写操作。
 虽然可以将数组和字典转换为XML文件,但只有某些对象才能被放置到集合(即数组和字典)中,来实现转换。这些可被放置到集合的类有如下:

  • Array、NSArray、NSMutableArray;
  • Dictionary、NSDictionary、NSMutableDictionary;
  • NSData、NSMutableData;
  • String、NSString、NSMutableString;
  • NSNumber;
  • NSDate
集合 方法(Object-C) 描述
NSArray +arrayWithContentsOfFile(读) 静态创建工厂方法,用于从属性列表文件中读取数据,创建 NSArray对象。Swift没有对应的构造器。
NSArray initWithContentsOfFile(读) 构造器,用于从属性列表文件中读取数据,创建NSArray对象。Swift表示为convenience init?(contentsOfFile aPath:String)。
NSArray -writeToFile:atomically(写) 该方法把NSArray对象写入属性列表文件中。Swift是writeToFile。
NSDictionary +dictionaryWithContentsOfFile(读) 静态工厂方法,从属性列表文件中读取数据,创建NSDictionary对象。Swift没有对应的构造器。
NSDictionary -initWithContentsOfFile(读) 构造器,从属性列表文件中读取数据,创建NSDictionary对象。Swift表示成convenience init?(contentsOfFile aPath:String)。
NSDictionary -writeToFile:atomically(写) 将NSDictionary对象写入到属性列表文件中,Swift是writeToFile。

【注】:由于Swift代码中的writeToFile(toFile:,atomically:)方法实际属于ObjectC的NSArray或NSDictionary类。所以要使用这个方法时,需要将Swift的Array(Dictionary)强制转换为NSArray(NSDictionary)。如:
let nsArray = array as! NSArray

代码示例:

// 用swift简写一下代码的执行逻辑,至于怎么优化格式封装方式这里不做说明.
class ViewController: UIViewController {
    open func create() {
        // 创建swift的数组
        let array:[String] = ["1","2","3"];
        // 将swift的数组转换为ObjectC的数组,并将数组写入属性列表文件
        let writeArray:NSArray = array as NSArray;
        // 写入到属性列表文件中
        writeArray.write(toFile: self.applicationDocumentsDirectoryFile(), atomically: true);
        
        // 从属性列表文件中读取数组
        let readArray = NSArray(contentsOfFile: self.applicationDocumentsDirectoryFile()) as! [String];
        // 输出验证
        for str in readArray {
            print(str);
        }
    }

    // 获得数据库文件路径
    func applicationDocumentsDirectoryFile() -> String {
        let documentDirectory: NSArray = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) as NSArray
        let path = (documentDirectory[0] as AnyObject).appendingPathComponent("NotesList.plist") as String
        print("path : \(path)")
        return path
    }
    
    // 【注】:这里用的时候直接self.,每次都要get一下,在真正项目中尽量不要这样调用,可以设置一个私有的沙箱目录中属性列表文件路径。
    // fileprivate var plistFilePath: String!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        // 调用函数
        self.create()
    }
}
6、对象归档

归档与属性列表方式不同,属性列表只有指定的一些对象才能进行持久化,而归档是任何实现了NSCopying协议的对象都可以被持久化,其中归档涉及两个类:NSKeyedArchiver和NSKeyedUnarchiver。归档和反归档都是采用健值对的形式编码。

实现协议:

方法 描述
-(void)encodeWithCoder:(NSCoder *)encoder 对象进行序列化的方法,把对象信息封装在NSCoder对象中。
-(instancetype)initWithCoder:(NSCoder *)decoder 对象的反序列化方法,通过NSCoder对象获取相应数据。

其中encoder和decoder是提供给用户进行编码和解码的流对象,两个都是采用健值对的形式进行操作,并根据不同的数据类型提供不同的写入和读取的方法,如encodeInt、encodeFloat、decodeIntForKey和decodeFloatForKey等方法。
如下是Note类实现的两个协议的程序:

@interface Note : NSObject<NSCoding>

@property(nonatomic, strong) NSDate *date;
@property(nonatomic, strong) NSString *content;

@end

------m------
@implementation Note

@synthesize date = _date;
@synthesize content = _content;

// - 归档(也叫序列化)
// - 当将一个自定义对象保存到文件的时候就会调用该方法;
// - 在该方法中,说明如何存储自定义对象的属性,也就是说在该方法中说清楚存储自定义对象的哪些属性;
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:_date forKey:@"date"];
    [aCoder encodeObject:_content forKey:@"content"];
}

// - 反归档(也叫反序列化)
// - 当文件中读取一个对象的时候就会调用该方法;
// - 在该方法中说明如何读取保存在文件中的对象,也就是说在该方法中说清楚怎么读取文件中的对象
- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self=[super init]) {
    self.date = [aDecoder decodeObjectForKey:@"date"];
    self.content = [aDecoder decodeObjectForKey:@"content"];
    }
    return self;
}
@end

【注】:要将一个自定义的类进行归档,那么类里面的每个属性都必须是可以被归档的,如果是不能归档的类型,我们可以把他转化为NSValue进行归档,然后在读出来的时候在转化为相应的类。通过****plist保存的数据是直接显示的****,不安全。通过归档方法保存的数据在文件中打开是乱码的,****加密的****,更安全。

归档与反归档
 ****所谓归档:****将复杂对象转化为NSData类型数据(复杂-->归档-->NSData--->WriteToFile)
注意:归档是将对象转化为数据字节,以文件的形式存储在磁盘上

归档化过程是使用NSKeyedArchiver对象归档数据,其操作步骤如下:
1) 创建NSMutableData对象:只需使用构造函数init()创建为空的对象;
2) 创建NSKeyedArchiver对象:用其构造函数initForWritingWithMutableData()创建对象;
3) 归档对象:调用NSKeyedArchiver对象的encodeObject()方法写入被归档的对象;
4) 完成操作:调用NSKeyedArchiver对象的finishEncoding()方法完成写入操作;
5) 写入文件:调用NSMutableData对象的writeToFile()写入到指定的目录下;

****所谓反归档:****将NSData类型数据转化为复杂对象(读取文件-->NSData-->反归档--->复杂对象)

对象反归档的过程与对象归档过程类似,不同的是在创建NSMutableData对象时,需要指定目录路径,且不需要写入文件中。其操作步骤如下:
1) 创建NSMutableData对象:指定文件路径调用构造函数initWithContentsOfFile()创建对象;
2) 创建NSKeyedUnarchiver 对象:用其构造函数initForReadingWithData()创建对象;
3) 反归档对象:调用NSKeyedUnarchiver 对象的decodeObjectForKey()方法写入被归档的对象;
4) 完成操作:调用NSKeyedUnarchiver 对象的finishEncoding()方法完成写入操作;

代码示例:

#define FILE_NAME @"NotesList.archive"
#define ARCHIVE_KEY @"NotesList"

// 获取Documents的路径,并创建NotesList.archive的路径 
- (NSString *)applicationDocumentsDirectoryFile {
    NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *path = [documentDirectory stringByAppendingPathComponent:FILE_NAME];
    NSLog(@"路径:%@", path);//Documents
    return path;
}

// 创建被保存的数据对象
Note *note = [[Note alloc] init];
note.date = [[NSDate alloc] init];
note.content = @"li'xiang";

NSString *path = [self applicationDocumentsDirectoryFile];
// 设置数据区,并将其连接到一个NSKeyedArchiver对象
NSMutableData *theData = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
                                  initForWritingWithMutableData:theData];
// 进行数据归档 
[archiver encodeObject:array forKey:ARCHIVE_KEY];
// 发出归档完成消息
[archiver finishEncoding];
// 将存档的数据区写入文件中。
[theData writeToFile:path atomically:YES];

// 进行数据反归档 
NSString *path = [self applicationDocumentsDirectoryFile];
NSData *theData = [NSData dataWithContentsOfFile:path];
NSKeyedUnarchiver *archiver = [[NSKeyedUnarchiver alloc]
                                        initForReadingWithData:theData];
Note *listData = [archiver decodeObjectForKey:ARCHIVE_KEY];
// 结束反归档
[archiver finishDecoding];

编码和反编码C语言类型
 NSKeyedArchiver 和NSKeyedUnarchiver类不能对structures, arrays, 和bit fields类型进行编码或反编码。
1)指针类型
 由于不能归档指针类型,所以若需要只能归档指针所指向的对象。但对于C语言的字符串类型(char*)却是支持的,它比较特殊,可以使用 encodeBytes:length:forKey:方法进行归档。
2)基本数据类型的数组
 一种基本的方法是一个元素一个元素归档,如"theArray[0]", "theArray[1]"这样一个个的进行归档,非常简单。
3)对象类型的数组
 对于C语言的数组且元素类型是对象,那么最简单的方式是将该数组用NSArray进行封装。这样就可以进行归档了;当进行反归档时,也是获得NSArray对象,然后一个个地拆封为C语言的数组元素。
4)数据结构类型
 可以将结构体的封装为一个对象,对象的每个成员是结构体的每个成员。若需要归档则先将结构体封装为OC对象,然后再归档;而反归档则是将其解析为OC对象,然后转换为C语言结构体。

7、SQLite3

SQLite是目前主流的第三方的数据库嵌入式关系型数据库,其最主要的特点就是轻量级、跨平台,当前很多嵌入式都将其作为数据库首选。SQLite提供多种方式的接口,有命令行的接口、客户端及多种语言的API接口。在iOS中需要使用C语言语法进行数据库操作、访问(无法使用ObjC直接访问,因为libqlite3框架基于C语言编写)。

配置
 在项目中的Build Phases选项中的Link Binary With Libraries下,点击其"+"号,从而添加所需要的libsqlite3.tbd或者是libsqlite3.0.tbd库。
在需要操作sqlite数据的文件中导入如下头文件:
oc直接添加#import "sqlite3.h"
 swift添加桥接文件,新建一个.h文件,在里边添加#import "SQLite3.h"然后在进入Buil Setting选项中,搜索Bridging Header,双击添加相对路径:$(SRCROOT)/文件在工程下路径,如在主目录这里删除/SQLite_Bridge.h

【注】:实际上libsqlite3.0.tbd本身是个替身,它指向libsqlite3.tbd。也就是说在项目里如果你添加libsqlite3.tbd和添加libsqlite3.0.tbd其实是添加了同一个文件,如果引用的是libsqlite3.0.tbd,SQLite库更新项目中不必修改了。


libsqlite.png
方法 描述
sqlite3 *db 数据库句柄,跟文件句柄FILE很类似
sqlite3_stmt *stmt 这个相当于ODBC的Command对象,用于保存编译好的SQL语句
sqlite3_open() 打开数据库,没有数据库时创建
sqlite3_exec() 执行非查询的sql语句
sqlite3_step() 在调用sqlite3_prepare后,使用这个函数在记录集中移动
sqlite3_close() 关闭数据库文件
sqlite3_column_text() 取text类型的数据
sqlite3_column_blob() 取blob类型的数据
sqlite3_column_int() 取int类型的数据

代码示例:

OC实在没啥可写的,这里就用swift简单写一下逻辑,详细的就不多提了。

class ViewController: UIViewController {
    
    // 定义数据库变量
    var db:OpaquePointer? = nil //sqlite3 *db
    
    // 查询数据方法
    public func findAll() {
        let path = self.applicationDocumentsDirectoryFile()
        let cpath = path.cString(using: String.Encoding.utf8)
        
        var statement:OpaquePointer? = nil
        
        if sqlite3_open(cpath!, &db) != SQLITE_OK {
            // 数据库打开失败。
            NSLog("数据库打开失败。")
        } else {
            let sql = "SELECT cdate,content FROM Note"
            let cSql = sql.cString(using: String.Encoding.utf8)
            //预处理过程
            if sqlite3_prepare_v2(db, cSql!, -1, &statement, nil) == SQLITE_OK {
                
                // 执行
                while sqlite3_step(statement) == SQLITE_ROW {
                    if let strContent = getColumnValue(index:0, stmt:statement!) {
                        NSLog("数据打印:%@", strContent)
                    }
                }
            }
        }
        
        defer {
            print("关闭数据库")
            sqlite3_close(db)
        }
        defer {
            print("释放语句对象")
            sqlite3_finalize(statement)
        }
    }
    
    // 插入数据与查询数据
    public func create() {
        let path = self.applicationDocumentsDirectoryFile()
        let cpath = path.cString(using: String.Encoding.utf8)
        
        var statement: OpaquePointer? = nil
        
        if sqlite3_open(cpath!, &db) != SQLITE_OK {
            //数据库打开失败。
            NSLog("数据库打开失败。")
        } else {
            let sql = "INSERT OR REPLACE INTO note (cdate, content) VALUES (?,?)"
            let cSql = sql.cString(using: String.Encoding.utf8)
            // 预处理过程
            if sqlite3_prepare_v2(db, cSql!, -1, &statement, nil) == SQLITE_OK {
                
                let cContent = "lixiang".cString(using: String.Encoding.utf8)
                // 绑定参数开始
                sqlite3_bind_text(statement, 1, cContent!, -1, nil)
                // 执行插入
                if sqlite3_step(statement) != SQLITE_DONE {
                    // 插入数据失败。
                    NSLog("插入数据失败。")
                }
            }
        }
        
        defer {
            print("释放语句对象")
            sqlite3_finalize(statement)
        }
        
        defer {
            print("关闭数据库")
            sqlite3_close(db)
        }
    }
    
    // 获得字段数据
    private func getColumnValue(index: CInt, stmt: OpaquePointer)->String? {
        
        if let ptr = UnsafeRawPointer.init(sqlite3_column_text(stmt, index)) {
            let uptr = ptr.bindMemory(to:CChar.self, capacity:0)
            let txt = String(validatingUTF8:uptr)
            return txt
        }
        return nil
    }
    
    // 创建数据库
    func createEditableCopyOfDatabaseIfNeeded() {
        
        let path = self.applicationDocumentsDirectoryFile()
        let cpath = path.cString(using: String.Encoding.utf8)
        
        // 第一个参数:数据库文件路径  第二个参数:数据库对象
        if sqlite3_open(cpath!, &db) != SQLITE_OK {
            NSLog("数据库打开失败。")
        } else {
            // 建表的SQL语句
            let sql = "CREATE TABLE IF NOT EXISTS Note (cdate TEXT PRIMARY KEY, content TEXT)"
            let cSql = sql.cString(using: String.Encoding.utf8)
            
            if (sqlite3_exec(db,cSql!, nil, nil, nil) != SQLITE_OK) {
                // 建表失败。
                NSLog("建表失败。")
            }
            
        }
        defer {
            print("关闭数据库")
            sqlite3_close(db)
        }
    }
    // 获得数据库文件路径
    private func applicationDocumentsDirectoryFile() ->String {
        let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) as NSArray
        let path = (documentDirectory[0] as AnyObject).appendingPathComponent("NotesList.sqlite3") as String
        print("path : \(path)")
        return path
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        // 调用函数
        self.createEditableCopyOfDatabaseIfNeeded()
        self.create()
        self.findAll()
    }
}

ps:别忘了引入的时候创建桥接文件哦!
8、Core Data

Core Data是iOS的一个持久化框架,它提供了对象-关系映射(ORM)的功能,即能够将程序中的对象(swift或Object-C中类的实例)转化成数据,保存在SQLite数据库文件中,也能够将保存在数据库中的数据还原成程序中的对象。在此数据操作期间,我们不需要编写任何SQL语句。

对象-关系模型.png

左边是关系模型,即数据库,数据库里面有张person表,person表里面有id、name、age三个字段,而且有2条记录;右边是对象模型,可以看到,有2个OC对象;利用Core Data框架,我们就可以轻松地将数据库里面的2条记录转换成2个OC对象,也可以轻松地将2个OC对象保存到数据库中,变成2条表记录,而且不用写一条SQL语句。

Core Data搭建结构

描述
NSManagedObjectModel 称为被管理对象模型类,是系统中的"实体",与数据库中的表对象对应,可以了解为图4中对象的结合,该模型是通过项目中的.xcdatamodeld文件进行声明的。
NSPersisntentStoreCoordinator 称为持久化存储协调器类,在持久化对象存储之上提供了一个接口,可以把它考虑成为数据库的连接。即相当是SQLite数据库中的SQLite3类型。
NSManageedObjectContext 称为被管理对象上下文类,在上下文中可以查找、删除和插入对象,然后通过栈同步到持久化对象存储,即相当是SQLite数据库中的语句(sqlite3_Stmt类型)。其中程序员主要使用该实例对象间接地与数据库进行交互。
Core Data结构.png

创建模型文件的过程:
创建模板:
1)创建项目时,勾选"Use core Data"的复选框方式创建。

创建工程时创建.png

2)如果创建工程时没有创建,可以同过command+N,的方式创建。


后期创建.png

添加实体:
 以一对一双向关联的两个实体为例,即Person中有Card属性,Card中有Person属性。
 人实体中有:姓名(年龄),年龄(age),卡(身份证)三个属性
 卡实体中有:无(号码),人(人)两个属性

添加实体.png

为两个实体添加实体属性:


实体1.png
实体2.png

建立两个实体的关系:


表示人中有个卡类型的卡属性,目的就是建立人跟卡之间的一对一关联的关系.png
表示卡中有个人类型的人属性,目的就是建立卡跟人之间的一对一关联的关系.png

代码示例:
1.搭建上下文环境:

 // 从应用程序包中加载模型文件
    NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
    // 传入模型对象,初始化NSPersistentStoreCoordinator
    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    // 构建SQLite数据库文件的路径
    NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)lastObject];
    NSURL *url = [NSURL fileURLWithPath:[docs stringByAppendingPathComponent:@"person.data" ]];
    // 添加持久化存储库,这里使用SQLite作为存储库
    NSError *error = nil;
    NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error];
    if(store == nil) {  // 直接抛异常
        [NSException raise:@"添加数据库错误" 格式:@"%@",[error localizedDescription]];
    }
    // 初始化上下文,设置persistentStoreCoordinator属性
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
    context.persistentStoreCoordinator = psc;  

2.添加数据到数据库:

    // 传入上下文,创建一个人实体对象
    NSManagedObject *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"  inManagedObjectContext:context];
    // 设置人的简单属性
    [person setValue:@"XN" forKey:@"name"];
    [person setValue:[NSNumber numberWithInt:24] forKey:@"age"];
    // 传入上下文,创建一张卡实体对象
    NSManagedObject *card = [NSEntityDescription insertNewObjectForEntityForName:@"Card"  inManagedObjectContext:context];
    [card setValue:@"1234567890" forKey:@"no"];
    // 设置人与卡之间的关联关系
    [person setValue:card forKey:@"card"];
    // 利用上下文对象,将数据同步到持久化存储库
    NSError *error = nil;
    BOOL success = [上下文保存:&error];
    if(!success) {
        [NSException raise:@"访问数据库错误"格式:@"%@", [error localizedDescription]];
    }
    // 如果是想做更新操作:只要在更改了实体对象的属性后调用[context save:&error],就能将更改的数据同步到数据库

3.从数据库中查询数据:

    // 初始化一个查询请求
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    //设置要查询的实体
    request.entity = [NSEntityDescription entityForName:@"Person"  inManagedObjectContext:context];
    // 设置排序(按照年龄降序)
    NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"age"  ascending:NO];
    request.sortDescriptors = [NSArray arrayWithObject:sort];
    // 设置条件过滤(搜索名称中包含字符串“Itcast-1”的记录,注意:设置条件过滤时间,数据库SQL语句中的%要用*来代替,所以%Itcast-1%应该写成* 1 *)
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name like%@", @"* Itcast-1 *"];
    request.predicate = 谓词;
    // 执行请求
    NSError *error = nil;
    NSArray *objs = [context executeFetchRequest:request error:&error];
    
    // 遍历数据
    for(NSManagedObject *obj in objs) {
        NSLog(@"name =%@", [obj valueForKey:@"name"])
    }

4.删除数据库中的数据:

    // 传入需要删除的实体对象
    [context deleteObject:managedObject];
    // 将结果同步到数据库
    NSError * error = nil;
    [上下文保存:&error];
    if(error) {
        [NSException raise:@"删除错误" 格式:@"%@", [error localizedDescription]];
    }

打开CoreData的SQL语句输出开关:
1.打开产品,点击EditScheme ...
2.点击参数,在参数中启动中添加2项
1> -com.apple.CoreData.SQLDebug
2> 1

SQL输出开关1.png
SQL输出开关2.png

创建NSManagedObject的子类
 默认情况下,利用核心数据取出的实体都是NSManagedObject类型的,能够利用键 - 值对来存取数据。但是一般情况下,实时在存取数据的基础上,有时还需要添加一些业务方法来完成一些其他任务,那么就必须创建NSManagedObject子的类,实体类。

实体分类.png

代码示例:

添加数据:
Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:context];  
person.name = @"XN";  
person.age = [NSNumber numberWithInt:24];  
  
Card *card = [NSEntityDescription insertNewObjectForEntityForName:@”Card" inManagedObjectContext:context];  
card.no = @”1234567890";  
person.card = card;  
// 最后调用[context save&error];保存数据  
9、FMDB

iOS中原生的SQLite API在进行数据存储的时候,需要使用C语言中的函数,操作比较麻烦。于是,就出现了一系列将SQLite API进行封装的库,FMDB
 FMDB是一款简洁、易用的封装库。它是对libsqlite3框架的封装,用起来的步骤与SQLite使用类似,并且它对于多线程的并发操作进行了处理,所以是线程安全的。

核心类
FMDatabase
 FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句
 FMDatabase这个类是线程不安全的,如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱等问题
 为了保证线程安全,FMDB提供方便快捷的FMDatabaseQueue类
FMResultSet
 使用FMDatabase执行查询后的结果集
FMDatabaseQueue
 用于在多线程中执行多个查询或更新,它是线程安全的,解决多个线程同时访问同个表而导致的崩溃问题,串行队列,不支持(block块内)串行嵌套任务执行

FMDB使用步骤
 下载FMDB文件,并将FMDB文件夹添加到项目中(也可使用CocoaPods导入)
 导入libsqlite3.0框架,导入头文件FMDatabase.h
 代码实现,与SQLite使用步骤相似,创建数据库路径,获得数据库路径,打开数据库,然后对数据库进行增、删、改、查操作,最后关闭数据库。

数据库创建
 创建FMDatabase对象时参数为SQLite数据库文件路径,该路径有三种情况。

  • 具体文件路径:如果不存在会自动创建
  • 空字符串@"":会在临时目录创建一个空的数据库,当FMDatabase连接关闭时,数据库文件也被删除
  • nil:会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁

代码示例:
1、使用FMDataBase类建立数据库:

  // 打开数据库-创建表
- (void)createEditableCopyOfDatabaseIfNeeded {
    // 获得数据库文件的路径
    NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)  lastObject];
    NSString *fileName = [doc stringByAppendingPathComponent:@"student.sqlite"];
    // 获得数据库
    FMDatabase *db = [FMDatabase databaseWithPath:fileName];
    // 使用如下语句,如果打开失败,可能是权限不足或者资源不足。通常打开完操作操作后,需要调用 close 方法来关闭数据库。在和数据库交互之前,数据库必须是打开的。如果资源或权限不足无法打开或创建数据库,都会导致打开失败。
    if ([db open]) {// 数据库打开成功
        // 创建表
        BOOL result = [db executeUpdate:@"CREATE TABLE IF NOT EXISTS t_student (id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL, age integer NOT NULL);"];
        if (result) {
            NSLog(@"创建表成功");
        } else {
            NSLog(@"创建表失败");
        }
    }
    
    self.db = db;
}

// 插入数据
- (void)create {
    
    for (int i = 0; i < 10; i++) {
        NSString *name = [NSString stringWithFormat:@"miao-%d", arc4random_uniform(100)];
        // 不确定的参数用?来占位(后面参数必须是oc对象,;代表语句结束)
        [self.db executeUpdate:@"INSERT INTO t_student (name, age) VALUES (?, ?);", name, @(arc4random_uniform(40))];
        // 参数是数组的使用方式
        // [self.db executeUpdate:@"INSERT INTO t_student (name, age) VALUES (?, ?);" withArgumentsInArray:@[name, @(arc4random_uniform(40))]];
        // 不确定的参数用%@、%d等来占位(参数为原始数据类型,执行语句不区分大小写)
        // [self.db executeUpdateWithFormat:@"INSERT INTO t_student (name, age) VALUES (%@, %d);", name, arc4random_uniform(40)];
    }
}

// 删除数据
- (void)remove {
    // 删除表数据
    // [self.db executeUpdate:@"DELETE FROM t_student;"];
    // 删除整个表
    [self.db executeUpdate:@"DROP TABLE IF EXISTS t_student;"];    
    
    // 按照主键删除
    // 不确定的参数用?来占位 (后面参数必须是oc对象,需要将int包装成OC对象)
    // [self.db executeUpdate:@"delete from t_student where id = ?;",@1];
    // 不确定的参数用%@,%d等来占位
    // [self.db executeUpdateWithFormat:@"delete from t_student where name = %@;",@"李响"];
}

// 修改
- (void)modify {
    // [self.db executeUpdate:@"update t_student set name = ? where name = ?",newName,oldName];
}

// 查询
- (void)findAll
{
    // 查询整个表
    FMResultSet *resultSet = [self.db executeQuery:@"SELECT * FROM t_student"];
    
    // 根据条件查询
    // FMResultSet *resultSet = [self.db executeQuery:@"select * from t_student where id=?",@1];
    
    // 遍历结果
    while ([resultSet next]) {
        int ID = [resultSet intForColumn:@"id"];
        NSString *name = [resultSet stringForColumn:@"name"];
        int age = [resultSet intForColumn:@"age"];
        NSLog(@"%d %@ %d", ID, name, age);
    }
}

// 数据库销毁命令SQLdrop ...
- (void)destroy {
    // 如果表格存在 则销毁
    [self.db executeUpdate:@"drop table if exists t_student;"];
}

2、使用FMDatabaseQueue类实现多线程操作:
 首先说一下事物与非事物,事物是一个并发控制的基本单元,所谓的事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
 事物与非事物,简单的举例来说就是,事物就是把所有的东西打包在一起,一次性处理它。而非事务就是一条一条的来执行并且处理。

代码示例:

// 打开数据库-创建表
- (void)establish {
    // 获得数据库文件的路径
    NSString *doc = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES)  lastObject];
    NSString *dbPath = [doc stringByAppendingPathComponent:@"test.sqlite"];
    NSLog(@"路径:%@", dbPath);
    // 创建一个队列
    self.dataBaseQueue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
    [self.dataBaseQueue inDatabase:^(FMDatabase *db) {
        if ([db open]) {// 数据库打开成功
            // 判断指定表是否存在
            if (![self checkTableExist:@"testTable" withFMData:db]) {
                // 创建表
                BOOL result =  [db executeUpdate:@"create table if not exists testTable (id integer PRIMARY KEY AUTOINCREMENT, name text)"];
                if (result) {
                    NSLog(@"创建表成功");
                } else {
                    NSLog(@"创建表失败");
                }
            }
            
        }
        else
        {
            // 关闭数据库
            [db close];
        }
    }];
}

// 判断指定表是否存在
- (BOOL)checkTableExist:(NSString *)tableName withFMData:(FMDatabase *)dbDatabase {
    BOOL result;
    
    tableName = [tableName lowercaseString];
    
    FMResultSet *rs = [dbDatabase executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName];
    
    result = [rs next];
    
    [rs close];
    
    return result;
    
}

插入数据:

- (void)testResult {
    // 非事务插入数据
    NSDate *one = [NSDate date];
    [self.dataBaseQueue inDatabase:^(FMDatabase *db) {
        for (int i = 0; i < 500; i++) {
            BOOL iserror = [db executeUpdate:@"insert into testTable (name) values(?)",[NSString stringWithFormat:@"name-%d",i]];
            if (!iserror) {
                NSLog(@"插入失败");
            }
        }
    }];
    
    NSDate *two = [NSDate date];
    NSTimeInterval first = [two timeIntervalSinceDate:one];
    NSLog(@"first = %lf",first);
    
    // 开启事务插入
    NSDate *three = [NSDate date];
    [self.dataBaseQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
        BOOL result = YES;
        for (int i = 500; i < 1000; i++) {
            result =  [db executeUpdate:@"insert into testTable (name) values(?)",[NSString stringWithFormat:@"name-%d",i]];
            if (!result) {
                NSLog(@"插入失败");
                *rollback = YES;
                break;
            }
        }
    }];
    
    NSDate *four = [NSDate date];
    NSTimeInterval second = [four timeIntervalSinceDate:three];
    NSLog(@"second = %lf",second);
    
    // 手动事物插入
    NSDate *five = [NSDate date];
    [self.dataBaseQueue inDatabase:^(FMDatabase *db) {
        
        /**
         *  操作放入事物中(加入事物操作)
         */
        [db beginTransaction];
        
        for (int i = 1000; i < 1500; i++) {
            BOOL iserror = [db executeUpdate:@"insert into testTable (name) values(?)",[NSString stringWithFormat:@"name-%d",i]];
            if (!iserror) {
                NSLog(@"插入失败");
            }
        }
        
        /**
         *  提交事物
         */
        [db commit];
    }];
    
    NSDate *Six = [NSDate date];
    NSTimeInterval third = [Six timeIntervalSinceDate:five];
    NSLog(@"third = %lf",third);
}

输出打印:

2017-05-31 00:42:47.773 FMDB应用[50737:15432041] 创建表成功
2017-05-31 00:42:48.079 FMDB应用[50737:15432041] first = 0.306702
2017-05-31 00:42:48.084 FMDB应用[50737:15432041] second = 0.004107
2017-05-31 00:42:48.089 FMDB应用[50737:15432041] third = 0.004684

删除数据:

// 删除数据
- (void)deleteData {
    //[self.dataBaseQueue inDatabase:^(FMDatabase *db) {
    //    [db executeUpdate:@"DELETE FROM testTable WHERE id=?",@1];
    //}];

    NSDate *five = [NSDate date];
    [self.dataBaseQueue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:@"delete from testTable where id < 500"];
    }];
    
    NSDate *six = [NSDate date];
    NSTimeInterval third = [six timeIntervalSinceDate:five];
    NSLog(@"third = %lf",third);
    
    
    NSDate *seven = [NSDate date];
    [self.dataBaseQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
        [db executeUpdate:@"delete from testTable where  id >= 500"];
    }];
    
    NSDate *eight = [NSDate date];
    NSTimeInterval fourth = [eight timeIntervalSinceDate:seven];
    NSLog(@"fourth = %lf",fourth);
}

输出打印:

2017-05-31 00:51:56.617 FMDB应用[50890:15440944] third = 0.001500
2017-05-31 00:51:56.619 FMDB应用[50890:15440944] fourth = 0.001703

更新数据:

// 更新
- (void)update {
    //[self.dataBaseQueue inDatabase:^(FMDatabase *db) {
    //    [db executeUpdate:@"UPDATE testTable SET name='lx' WHERE id=?",@8];
    //}];
    
    
    // 如果要保证多个操作同时成功或者同时失败,用事务,即把多个操作放在同一个事务中。
    [self.dataBaseQueue inDatabase:^(FMDatabase *db) {
        // 开启事务
        [db beginTransaction];
        BOOL flag = [db executeUpdate:@"UPDATE testTable SET name='xiugai' WHERE id=?",@2];
        [db executeUpdate:@"UPDATE testTable SET name='zheli' WHERE id=?",@3];
        
        if (flag) {
            NSLog(@"数据更新成功");
            
        } else {
            NSLog(@"数据更新失败");
            // 事务回滚
            // 发现情况不对时,主动回滚用下面语句。否则是根据commit结果,如成功就成功,如不成功才回滚
            [db rollback];
            [db executeUpdate:@"UPDATE testTable SET name='Eric' WHERE id=?",@4];
        }
        // 提交事务
        [db commit];
        
    }];
    
    __block BOOL flag;
    // FMDB中,也可以直接利用队列进行事务操作,队列中的打开、关闭、回滚事务等都已经被封装好了。
    [self.dataBaseQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
        flag = [db executeUpdate:@"UPDATE testTable SET name='xiaoqiang' WHERE id=?",@2];
        [db executeUpdate:@"UPDATE testTable SET name='xiaoming' WHERE id=?",@3];
        
        if (flag) {
            NSLog(@"数据更新成功");
            *rollback = !flag;
        } else {
            NSLog(@"数据更新失败");
            // 发现情况不对时,主动回滚用下面语句。
            *rollback = YES;
            flag = NO;
            
            [db executeUpdate:@"UPDATE testTable SET name='Eric' WHERE id=?",@4];
        }
    }];
}
// 其他线程操作
- (void)multithreading {
    // 异步并发全局队列
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self.dataBaseQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
            BOOL result = YES;
            for (int i = 500; i < 1000; i++) {
                result =  [db executeUpdate:@"insert into testTable (name) values(?)",[NSString stringWithFormat:@"name-%d",i]];
                if (!result) {
                    NSLog(@"break");
                    *rollback = YES;
                    break;
                }
            }
            
        }];
    });
}
10、Magical Record

Magical Record借用了Ruby on Rails中的Active Record模式,使得你可以非常容易的添加、查找、删除数据,降低了Core Data的使用门槛。

创建模型文件:
 创建一个名为Model的模型文件。 (File > New File… > Core Data > Data Model)
 点击左下角的Add Entity,更改Entity的名字为Person。
 为Entity添加三个Attribute:age(Integer16)、firstname(string)、lastname(string)。
 点击Editor > Create NSManagedObject Subclass… 创建实体分类。

使用Magical Record:
引入头文件

#import "MagicalRecord.h" 或 #import <MagicalRecord/MagicalRecord.h>

如果在创建工程之初勾选了使用Core Data的选项,系统会自动在AppDelegate中生成大量的Core Data代码。但是完可以使用一行代码代替,如下:

初始化
标准初始化

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [MagicalRecord setupCoreDataStackWithStoreNamed:@"MyDatabase.sqlite"];
    // 或者
    /*
    + (void)setupCoreDataStack;
    + (void)setupAutoMigratingCoreDataStack;
    + (void)setupCoreDataStackWithInMemoryStore;
    + (void)setupCoreDataStackWithStoreNamed:(NSString *)storeName;
    + (void)setupCoreDataStackWithAutoMigratingSqliteStoreNamed:(NSString *)storeName;
    + (void)setupCoreDataStackWithStoreAtURL:(NSURL *)storeURL;
    + (void)setupCoreDataStackWithAutoMigratingSqliteStoreAtURL:(NSURL *)storeURL;
    */
    return YES;
}

每次调用都会实例化一块 CoreData 栈,并且对该实例提供 getter 和 setter 方法。 MagicalRecord 将这些实例作为默认使用的栈。

当在DeBug模式下使用默认SQLite 数据存储时,如果没有创建新数据模型就更改了数据模型,MagicalRecord 将会自动删除旧的存储并创建新的存储。这将为你节省大量时间——每当更改数据模型,不必再卸载、重装应用。但请确保发布的应用的不是DeBug版本:否则在未告知用户的情况下删除应用数据,这可不是好事!

退出前,你应该调用类方法+cleanUp:

- (void)applicationWillTerminate:(UIApplication *)application {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    [MagicalRecord cleanUp];
}

这用于使用MagicalRecord后的整理工作:解除我们自定义的错误处理器并把MagicalRecord创建的所有的Core Data 栈设为 nil。

开启iCloud 持久化存储
 为了更好地使用苹果的iCloud Core Data 同步机制,使用下面初始化方法中的 一种 来替换来替换前面列出的标准初始化方法:

     + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID
     localStoreNamed:(NSString *)localStore;
     
     + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID
     contentNameKey:(NSString *)contentNameKey
     localStoreNamed:(NSString *)localStoreName
     cloudStorePathComponent:(NSString *)pathSubcomponent;
     
     + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID
     contentNameKey:(NSString *)contentNameKey
     localStoreNamed:(NSString *)localStoreName
     cloudStorePathComponent:(NSString *)pathSubcomponent
     completion:(void (^)(void))completion;
     
     + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID
     localStoreAtURL:(NSURL *)storeURL;
     
     + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID
     contentNameKey:(NSString *)contentNameKey
     localStoreAtURL:(NSURL *)storeURL
     cloudStorePathComponent:(NSString *)pathSubcomponent;
     
     + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID
     contentNameKey:(NSString *)contentNameKey
     localStoreAtURL:(NSURL *)storeURL
     cloudStorePathComponent:(NSString *)pathSubcomponent
     completion:(void (^)(void))completion;

如果你需要同时管理多个iCloud 存储内容,推荐使用可以设置 contentNameKey 的方法。不能设置contentNameKey的方法则会根据应用的 bundle identifier(CFBundleIdentifier)自动生成 NSPersistentStoreUbiquitousContentNameKey作为标示。

操作被管理的对象上下文
 对象上下文环境是你操作Core Data内数据的基础,只有正确获取到了上下文环境,才有可能进行相关的读写操作。换句话说,程序的任意位置,只要能正确获取上下文,都能进行Core Data的操作.这也是使用Core Data共享数据的基础之一。相较于传统的方式,各个页面之间只需要与一个透明的上下文环境进行交互,即可进行页面间数据的共享。

创建新的对象上下文:

+ [NSManagedObjectContext MR_context]: 设置默认的上下文为它的父级上下文.并发类型为 NSPrivateQueueConcurrencyType .
+ [NSManagedObjectContext MR_newMainQueueContext]: 并发类型为 ** NSMainQueueConcurrencyType**.
+ [NSManagedObjectContext MR_newPrivateQueueContext]: 并发类型为 NSPrivateQueueConcurrencyType .
+ [NSManagedObjectContext MR_contextWithParent:…]: 允许自定义父级上下文.并发类型为 NSPrivateQueueConcurrencyType .
+ [NSManagedObjectContext MR_contextWithStoreCoordinator:…]:允许自定义持久化存储协调器.并发类型为 NSPrivateQueueConcurrencyType .

例:

NSManagedObjectContext *localContext = [NSManagedObjectContext MR_context];
// 在当前上下文环境中创建一个新的 Person 对象.
Person *person  = [Person MR_createEntityInContext:localContext];
person.firstname = @"默";
person.lastname = @"六";
person.age = 24;
    
// 保存修改到当前上下文中.
[localContext MR_saveToPersistentStoreAndWait];

默认上下文:
 当使用Core Data时,你经常使用的两个类主要对象是:NSManagedObject和NSManagedObjectContext。
 MagicalRecord 提供了一个简单类方法来获取一个默认的NSManagedObjectContext对象,这个对象在整个应用全局可用。这个上下文对象,在主线程操作,对于简单的单线程应用来说非常强大。
获取方法:

NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext];

这个上下文对象,在MagicalRecord的任何需要使用上下文对象方法中都可以使用,但是并不需要给这些方法显示提供一个指定对象管理上下文对象参数。
 如果你想创建一个新的对象管理上下文对象,以用于非主线程,可使用下面的方法:

NSManagedObjectContext *myNewContext = [NSManagedObjectContext MR_context];

这将会创建一个新的对象管理上下文,和默认的上下文对象有相同的对象模型和持久化存储;但是在另一个线程中使用时,是线程安全的,它自动设置默认上下文对象为父级上下文。
 如果你想要将你的myNewContext实例作为所有获取请求默认的上下文对象,使用下面的类方法:

[NSManagedObjectContext MR_setDefaultContext:myNewContext];

[注意]: 建议默认的上下文对象在主线程使用并发类型为NSMainQueueConcurrencyType的对象管理上线文对象创建和设置。

在后台线程中执行任务
 MagicalRecord 提供了创建及使用后台线程上下文的方法,按顺序的在后台保存。后台保存操作为代码块的形式。

  • 用于更改实体的block将永远不会在主线程执行。
  • 在你的block内部提供一个单一的 NSManagedObjectContext 上下文对象。

例如,如果我们有一个Person实体对象,并且我们需要设置它的firstName和lastName字段,下面的代码展示了如何使用MagicalRecord来设置一个后台保存的上下文对象:

    // 获取上下文环境
    NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext];
    // 在当前上下文环境中创建一个新的 Person 对象.
    Person *person  = [Person MR_createEntityInContext:defaultContext];
    person.firstname = @"firstname";
    person.lastname  = @"lastname";
    person.age       = 100; 
    // 保存修改到当前上下文中. 
    [defaultContext MR_saveToPersistentStoreAndWait];
    
    [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
        Person *localPerson = [person MR_inContext:localContext];
        localPerson.firstname = @"Yan";
        localPerson.lastname = @"Feng";
    }];

在这个方法中,指定的block给你提供了一个合适的上下文对象来执行你的操作,你不需要担心这个上下文对象的初始化来告诉默认上线文它准备好了,并且应当更新,因为变更是在另一个线程执行。
 为了在保存block完成时执行某个操作,你可以使用 completion block:

    // 获取上下文环境
    NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext];
    // 在当前上下文环境中创建一个新的 Person 对象。
    Person *person  = [Person MR_createEntityInContext:defaultContext];
    person.firstname = @"firstname";
    person.lastname  = @"lastname";
    person.age       = 100; 
    // 保存修改到当前上下文中。
    [defaultContext MR_saveToPersistentStoreAndWait];
    
    [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
        Person *localPerson = [person MR_inContext:localContext];
        localPerson.firstname = @"Yan";
        localPerson.lastname = @"Feng";
    }  completion:^(BOOL success, NSError *error) {
        NSArray * persons = [Person MR_findAll];
        [persons enumerateObjectsUsingBlock:^(Person * obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"firstname: %@, lastname: %@\n", obj.firstname, obj.lastname);
        }];
    }];
    // 这个完成的block,在主线程(队列)中调用,所以可以在此block里安全触发UI更新。

创建实体对象
 为了创建并插入一个新的实体实例到默认上下文对象中,你可以使用:

Person *myPerson = [Person MR_createEntity];

创建实体实例,并插入到指定的上下文中:

Person *myPerson = [Person MR_createEntityInContext:otherContext];

删除实体对象

删除默认上下文中的实体对象:
[myPerson MR_deleteEntity];
删除指定上下文中的实体对象:
[myPerson MR_deleteEntityInContext:otherContext];
删除默认上下文中的所有实体:
[Person MR_truncateAll];
删除指定上下文中的所有实体:
[Person MR_truncateAllInContext:otherContext];

获取实体对象
基础查找

MagicalRecord中的大多数方法返回NSArray结果.

举个例子,如果你有一个名为 Person 的实体,和实体 Department 关联,你可以从持久化存储中获取所有的 Person 实体:
NSArray *people = [Person MR_findAll];

可以指定以某个属性排序:
NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName" ascending:YES];

可以使用多个属性进行排序:
NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName,FirstName" ascending:YES];

当使用多个属性进行排序时,可以单独指定升序或降序.
// 按照属性LastName降序、FirstName升序排序
NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName:NO,FirstName" ascending:YES]; 
// 或者 NSArray *peopleSorted = [Person MR_findAllSortedBy:@"LastName,FirstName:YES" ascending:NO];

如果你有办法通过某种方式从数据库中获取唯一的一个对象(比如,给对象一个特定的唯一标记),你可以使用下面方法获取某个实体对象:
Person *person = [Person MR_findFirstByAttribute:@"FirstName" withValue:@"名字"];

高级查找
使用 谓词 查询特定实体:

NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", @[dept1, dept2]];
NSArray *people = [Person MR_findAllWithPredicate:peopleFilter];

使用 谓词 查询特定实体:
使用谓词获取对应查询条件的 NSFetchRequest 对象

NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments];
NSFetchRequest *people = [Person MR_requestAllWithPredicate:peopleFilter];
每执行一次,就创建一个这些查询条件对应的NSFetchRequest和NSSortDescriptor.

自定义查询请求

NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments];

NSFetchRequest *peopleRequest = [Person MR_requestAllWithPredicate:peopleFilter];
[peopleRequest setReturnsDistinctResults:NO];
[peopleRequest setReturnPropertiesNamed:@[@"FirstName", @"LastName"]];

NSArray *people = [Person MR_executeFetchRequest:peopleRequest];

查询实体数目

你还可以获取指定类型的实体个数:
NSNumber *count = [Person MR_numberOfEntities];

使用谓词或其它过滤器查询实体数目:
NSNumber *count = [Person MR_numberOfEntitiesWithPredicate:...];

以下方法返回类型则是NSUInteger:
+ (NSUInteger) MR_countOfEntities;
+ (NSUInteger) MR_countOfEntitiesWithContext:(NSManagedObjectContext *)context;
+ (NSUInteger) MR_countOfEntitiesWithPredicate:(NSPredicate *)searchFilter;
+ (NSUInteger) MR_countOfEntitiesWithPredicate:(NSPredicate *)searchFilter
                                     inContext:(NSManagedObjectContext *)context;

合计操作

NSNumber *totalCalories = [CTFoodDiaryEntry MR_aggregateOperation:@"sum:" onAttribute:@"calories" withPredicate:predicate];

NSNumber *mostCalories  = [CTFoodDiaryEntry MR_aggregateOperation:@"max:" onAttribute:@"calories" withPredicate:predicate];

NSArray *caloriesByMonth = [CTFoodDiaryEntry MR_aggregateOperation:@"sum:" onAttribute:@"calories" withPredicate:predicate
                                                           groupBy:@"month"];

在指定上下文中查找实体

所有的 find,fetch,request 方法都可以通过 inContext: 参数指定查询的上下文:

NSArray *peopleFromAnotherContext = [Person MR_findAllInContext:someOtherContext];

Person *personFromContext = [Person MR_findFirstByAttribute:@"lastName" withValue:@"名字" inContext:someOtherContext];

NSUInteger count = [Person MR_numberOfEntitiesWithContext:someOtherContext];

保存实体
 大多数情况下,当数据发生变化的时候就执行保存操作。有一些应用只是在应用关闭的时候才保存,但这样增加了丢失数据的风险。当应用崩溃的时候就会造成数据丢失,用户会丢失他们的数据。应当避免这种情况的发生。
 如果保存操作花费的时间太长,你可以采取以下措施:

1.在后台线程中执行保存操作:
 MagicalRecord 为更改、保存实体提供了简洁的API。

[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
    // Do your work to be saved here, against the `localContext` instance
    // 在此块中的所有操作均在后台执行
} completion:^(BOOL success, NSError *error) {
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];

将任务拆分成小任务保存:
 像一次性导入大量数据的任务,你需要将数据拆分成小块来操作。一次处理的数据大小并没有统一标准,你需要使用类似 Apples’ Instruments 的工具来测试调整,获取最佳大小。

处理长时储存
ios平台
 当iOS平台app退出时,app会获得短暂时间向硬盘中保存整理数据。如果你的应用存储时间较长,最好在应用完全终止前,请求延长后台执行时间。

UIApplication *application = [UIApplication sharedApplication];
 
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];
 
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
    // 此处执行保存操作
 
} completion:^(BOOL success, NSError *error) {
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];

请确保认真阅读beginBackgroundTaskWithExpirationHandler ,因为不适当或不必要的延长应用生命周期,申请上架的时候可能会被 App Store 拒绝。

OS X 平台
 OS X Mavericks (10.9) 及更高版本中,App Nap 可以让你 app 看起来像关闭了,但仍在后台运行。如果有长时间的保存操作,最好的方式是禁用自动终止和突然终止( automatic and sudden termination):

NSProcessInfo *processInfo = [NSProcessInfo processInfo];
 
[processInfo disableSuddenTermination];
[processInfo disableAutomaticTermination:@"Application is currently saving to persistent store"];
 
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
    // Do your work to be saved here
 
} completion:^(BOOL success, NSError *error) {
    [processInfo enableSuddenTermination];
    [processInfo enableAutomaticTermination:@"Application has finished saving to the persistent store"];
}];

导入对象
导入之前你应当充分了解导入数据对象的结构,同时构建与之对应的实体。完成这些之后,再进行数据导入。导入的方式有以下几种:

依据数据对象自动创建新实例:

NSDictionary *contactInfo = // 此处解析 JSON 或其他数据源
 
Person *importedPerson = [Person MR_importFromObject:contactInfo];

两步方法:

NSDictionary *contactInfo = // 此处解析 JSON 或其他数据源
Person *person = [Person MR_createEntity]; // 此处不一定是新建实体,也可使用已有实体
[person MR_importValuesForKeysWithObject:contactInfo];

两步方法中的实体可以是新建的,也可以是已存在的,两步方法可以帮你更新已存在的实体。

+MR_importFromObject:根据已配置的查询值(lookup value)查找相应对象(即上文中的relatedByAttribute 和 attributeNameID)。它遵循Cocoa导入key-value 的范例,保证安全导入数据。

+MR_importFromObject: 是对实例方法-MR_importValuesForKeysWithObject:和创建对象方法的封装,返回值为赋值的新对象。

以上两个方法均为同步执行方法。导入时间长短不一,很可能影响交互体验,若将所有数据导入工作都放在后台执行,就不会影响软件交互了。像前面提到的,MagicalRecord 提供了很多好用的后台线程 API :

[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *)localContext {
  Person *importedPerson = [Person MR_importFromObject:personRecord inContext:localContext]; //导入操作
}];

导入数组
使用 array 对象来保存单一类型数据很常见,可以使用 +MR_importFromArray: 方法来处理:

NSArray *arrayOfPeopleData = /// 解析JSON的结果
NSArray *people = [Person MR_importFromArray:arrayOfPeopleData];

此方法与+MR_importFromObject:方法类似,也是同步执行方法,所以你想要后台执行,就得使用前面提到的方法,在后台块里执行。

如果你导入的数据与Core Data model 完全匹配,那么上面的方法已经能帮你完成导入工作了。但大多数情况下并不理想,两者之间稍有差别, MagicalRecord 的一些方法处理导入数据和Core Data model之间的不同。

实践:

错误数据处理

API 方法经常会返回格式错误或内容错误的数据。最好的处理方法是通过实体类的分类方法处理,有三个方法可供选择:

Method Purpose
- (BOOL) shouldImport; 在导入数据前调用此方法。如果返回为 NO,则取消向实体导入数据。
- (void) willImport:(id)data; 紧邻数据导入之前调用。
- (void) didImport:(id)data; 紧邻数据导入之后调用。

一般来说,如果你尝试导入所有数据时,发现一些损坏数据,你可能想修复它们。

常见的情景是在导入JSON数据时,常常将数字字符串被错误的解析为数字。如果你想确保它们以字符串的类型导入,可以这么做:

@interface MyGreatEntity
 
@property(readwrite, nonatomic, copy) NSString *identifier;
 
@end
 
@implementation MyGreatEntity
 
@dynamic identifier;
 
- (void)didImport:(id)data
{
  if (NO == [data isKindOfClass:[NSDictionary class]]) {
    return;
  }
 
  NSDictionary *dataDictionary = (NSDictionary *)data;
 
  id identifierValue = dataDictionary[@"my_identifier"];
 
  if ([identifierValue isKindOfClass:[NSNumber class]]) {
    NSNumber *numberValue = (NSNumber *)identifierValue;
 
    self.identifier = [numberValue stringValue];
  }
}
@end

更新数据时删除本地记录

在后续的导入操作中,有时不仅需要更新记录,同时要删除本地存在、远程数据库中不存在的数据。此时需要先根据 relatedByAttribute来判断哪些记录不在远程数据库中,然后将其删除。

NSArray *arrayOfPeopleData = /// JSON 解析结果
NSArray *people = [Person MR_importFromArray:arrayOfPeopleData];
NSArray *idList = [arrayOfPeopleData valueForKey:@"id"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT(id IN %@)", idList];
[Person MR_deleteAllMatchingPredicate:predicate];

如果你想在更新操作中确保相关记录已被删除,你可以使用与上面代码类似的逻辑,并在Person 的 willImport:方法中实现:

@implementation Person
- (void)willImport:(id)data {
    NSArray *idList = [data[@"posts"] valueForKey:@"id"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT(id IN %@) AND person.id == %@", idList, self.id];
    [Post MR_deleteAllMatchingPredicate:predicate];
}

[注]:如不想使用MR_前缀,只需要在*-Prefix.pch文件中添加一句#define MR_SHORTHAND即可,注意这句要在#import “MagicalRecord.h”之前。

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

推荐阅读更多精彩内容