iOS开发之数据存取

概览

在iOS开发中数据存储的方式可以归纳为两类:一类是存储为文件,另一类是存储到数据库。例如前面IOS开发系列—Objective-C之Foundation框架的文章中提到归档、plist文件存储,包括偏好设置其本质都是存储为文件,只是说归档或者plist文件存储可以选择保存到沙盒中,而偏好设置系统已经规定只能保存到沙盒的Library/Preferences目录。当然,文件存储并不作为本文的重点内容。本文重点还是说数据库存储,做过数据库开发的朋友应该知道,可以通过SQL直接访问数据库,也可以通过ORM进行对象关系映射访问数据库。这两种方式恰恰对应iOS中SQLite和Core Data的内容,在此将重点进行分析: 1.SQLite   2.Core Data   3.FMDB

SQLite

SQLite是目前主流的嵌入式关系型数据库,其最主要的特点就是轻量级、跨平台,当前很多嵌入式操作系统都将其作为数据库首选。虽然SQLite是一款轻型数据库,但是其功能也绝不亚于很多大型关系数据库。学习数据库就要学习其相关的定义、操作、查询语言,也就是大家日常说得SQL语句。和其他数据库相比,SQLite中的SQL语法并没有太大的差别,因此这里对于SQL语句的内容不会过多赘述,大家可以参考SQLite中其他SQL相关的内容,这里还是重点讲解iOS中如何使用SQLite构建应用程序。先看一下SQLite数据库的几个特点:

1.基于C语言开发的轻型数据库

2.在iOS中需要使用C语言语法进行数据库操作、访问(无法使用ObjC直接访问,因为libqlite3框架基于C语言编写)

3.SQLite中采用的是动态数据类型,即使创建时定义了一种类型,在实际操作时也可以存储其他类型,但是推荐建库时使用合适的类型(特别是应用需要考虑跨平台的情况时)

4.建立连接后通常不需要关闭连接(尽管可以手动关闭)

要使用SQLite很简单,如果在Mac OSX上使用可以考虑到SQLite官方网站下载命令行工具,也可以使用类似于SQLiteManager、Navicat for SQLite等工具。为了方便大家开发调试,建议在开发环境中安装上述工具。

在iOS中操作SQLite数据库可以分为以下几步(注意先在项目中导入libsqlite3框架):

1.打开数据库,利用sqlite3_open()打开数据库会指定一个数据库文件保存路径,如果文件存在则直接打开,否则创建并打开。打开数据库会得到一个sqlite3类型的对象,后面需要借助这个对象进行其他操作。

2.执行SQL语句,执行SQL语句又包括有返回值的语句和无返回值语句。

3.对于无返回值的语句(如增加、删除、修改等)直接通过sqlite3_exec()函数执行;

4.对于有返回值的语句则首先通过sqlite3_prepare_v2()进行sql语句评估(语法检测),然后通过sqlite3_step()依次取出查询结果的每一行数据,对于每行数据都可以通过对应的sqlite3_column_类型()方法获得对应列的数据,如此反复循环直到遍历完成。当然,最后需要释放句柄。

在整个操作过程中无需管理数据库连接,对于嵌入式SQLite操作是持久连接(尽管可以通过sqlite3_close()关闭),不需要开发人员自己释放连接。纵观整个操作过程,其实与其他平台的开发没有明显的区别,较为麻烦的就是数据读取,在iOS平台中使用C进行数据读取采用了游标的形式,每次只能读取一行数据,较为麻烦。因此实际开发中不妨对这些操作进行封装:

KCDbManager.h

KCDbManager.m

在上面的类中对于数据库操作进行了封装,封装之后数据操作更加方便,同时所有的语法都由C转换成了ObjC。

下面仍然以微博查看为例进行SQLite演示。当然实际开发中微博数据是从网络读取的,但是考虑到缓存问题,通常会选择将微博数据保存到本地,下面的Demo演示了将数据存放到本地数据库以及数据读取的过程。当然,实际开发中并不会在视图控制器中直接调用数据库操作方法,在这里通常会引入两个概念Model和Service。Model自不必多说,就是MVC中的模型。而Service指的是操作数据库的服务层,它封装了对于Model的基本操作方法,实现具体的业务逻辑。为了解耦,在控制器中是不会直接接触数据库的,控制器中只和模型(模型是领域的抽象)、服务对象有关系,借助服务层对模型进行各类操作,模型的操作反应到数据库中就是对表中数据的操作。具体关系如下:

要完成上述功能,首先定义一个应用程序全局对象进行数据库、表的创建。为了避免每次都创建数据库和表出错,这里利用了偏好设置进行保存当前创建状态(其实这也是数据存储的一部分),如果创建过了数据库则不再创建,否则创建数据库和表。

KCDatabaseCreator.m

其次,定义数据模型,这里定义用户User和微博Status两个数据模型类。注意模型应该尽量保持其单纯性,仅仅是简单的POCO,不要引入视图、控制器等相关内容。

KCUser.h

KCUser.m

KCStatus.h

KCStatus.m

然后,编写服务类,进行数据的增、删、改、查操作,由于服务类方法同样不需要过多的配置,因此定义为单例,保证程序中只有一个实例即可。服务类中调用前面封装的数据库方法将对数据库的操作转换为对模型的操作。

KCUserService.h

KCUserService.m

KCStatusService.h

KCStatusService.m

最后,在视图控制器中调用相应的服务层进行各类数据操作,在下面的代码中分别演示了增、删、改、查四类操作。

KCMainViewController.m

项目目录结构:

运行效果

 

Core Data

基本概念

当前,各类应用开发中只要牵扯到数据库操作通常都会用到一个概念“对象关系映射(ORM)”。例如在Java平台使用Hibernate,在.NET平台使用Entity Framework、Linq、NHibernate等。在iOS中也不例外,iOS中ORM框架首选Core Data,这是官方推荐的,不需要借助第三方框架。无论是哪种平台、哪种技术,ORM框架的作用都是相同的,那就是将关系数据库中的表(准确的说是实体)转换为程序中的对象,其本质还是对数据库的操作(例如Core Data中如果存储类型配置为SQLite则本质还是操作的SQLite数据库)。细心的朋友应该已经注意到,在上面的SQLite中其实我们在KCMainViewController中进行的数据库操作已经转换为了对象操作,服务层中的方法中已经将对数据库的操作封装起来,转换为了对Model的操作,这种方式已经是面向对象的。上述通过将对象映射到实体的过程完全是手动完成的,相对来说操作比较复杂,就拿对KCStatus对象的操作来说:首先要手动创建数据库(Status表),其次手动创建模型KCStatus,接着创建服务层KCStatusService。Core Data正是为了解决这个问题而产生的,它将数据库的创建、表的创建、对象和表的转换等操作封装起来,简化了我们的操作(注意Core Data只是将对象关系的映射简化了,并不是把服务层替代了,这一点大家需要明白)。

使用Core Data进行数据库存取并不需要手动创建数据库,这个过程完全由Core Data框架完成,开发人员面对的是模型,主要的工作就是把模型创建起来,具体数据库如何创建则不用管。在iOS项目中添加“Data Model”文件。然后在其中创建实体和关系:

 

模型创建的过程中需要注意:

1.实体对象不需要创建ID主键,Attributes中应该是有意义属性(创建过程中应该考虑对象的属性而不是数据库中表有几个字段,尽管多数属性会对应表的字段)。

2.所有的属性应该指定具体类型(尽管在SQLite中可以不指定),因为实体对象会对应生成ObjC模型类。

3.实体对象中其他实体对象类型的属性应该通过Relationships建立,并且注意实体之间的对应关系(例如一个用户有多条微博,而一条微博则只属于一个用户,用户和微博形成一对多的关系)。

以上模型创建后,接下来就是根据上面的模型文件(.xcdatamodeld文件)生成具体的实体类。在Xcode中添加“NSManagedObject Subclass”文件,按照步骤选择创建的模型及实体,Xcode就会根据所创建模型生成具体的实体类。

User.h

User.m

Status.h

Status.m

很显然,通过模型生成类的过程相当简单,通常这些类也不需要手动维护,如果模型发生的变化只要重新生成即可。有几点需要注意:

1.所有的实体类型都继承于NSManagedObject,每个NSManagedObject对象对应着数据库中一条记录。

2.集合属性(例如User中的status)生成了访问此属性的分类方法。

3.使用@dynamic代表具体属性实现,具体实现细节不需要开发人员关心。

当然,了解了这些还不足以完成数据的操作。究竟Core Data具体的设计如何,要完成数据的存取我们还需要了解一下Core Data几个核心的类。

1.Persistent Object Store:可以理解为存储持久对象的数据库(例如SQLite,注意Core Data也支持其他类型的数据存储,例如xml、二进制数据等)。

2.Managed Object Model:对象模型,对应Xcode中创建的模型文件。

3.Persistent Store Coordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。

4.Managed Object Context:对象管理上下文,负责实体对象和数据库之间的交互。

Core Data使用

Core Data使用起来相对直接使用SQLite3的API而言更加的面向对象,操作过程通常分为以下几个步骤:

1.创建管理上下文

创建管理上下可以细分为:加载模型文件->指定数据存储路径->创建对应数据类型的存储->创建管理对象上下方并指定存储。

经过这几个步骤之后可以得到管理对象上下文NSManagedObjectContext,以后所有的数据操作都由此对象负责。同时如果是第一次创建上下文,Core Data会自动创建存储文件(例如这里使用SQLite3存储),并且根据模型对象创建对应的表结构。下图为第一次运行生成的数据库及相关映射文件:

为了方便后面使用,NSManagedObjectContext对象可以作为单例或静态属性来保存,下面是创建的管理对象上下文的主要代码:

2.查询数据

对于有条件的查询,在Core Data中是通过谓词来实现的。首先创建一个请求,然后设置请求条件,最后调用上下文执行请求的方法。

如果有多个条件,只要使用谓词组合即可,那么对于关联对象条件怎么查询呢?这里分为两种情况进行介绍:

a.查找一个对象只有唯一一个关联对象的情况,例如查找用户名为“Binger”的微博(一个微博只能属于一个用户),通过keypath查询

此时如果跟踪Core Data生成的SQL语句会发现其实就是把Status表和User表进行了关联查询(JOIN连接)。

b.查找一个对象有多个关联对象的情况,例如查找发送微博内容中包含“Watch”并且用户昵称为“小娜”的用户(一个用户有多条微博),此时可以充分利用谓词进行过滤。

注意:如果单纯查找微博中包含“Watch”的用户,直接查出对应的微博,然后通过每个微博的user属性即可获得用户,此时就不用使用额外的谓词过滤条件。

3.插入数据

插入数据需要调用实体描述对象NSEntityDescription返回一个实体对象,然后设置对象属性,最后保存当前上下文即可。这里需要注意,增、删、改操作完最后必须调用管理对象上下文的保存方法,否则操作不会执行。

4.删除数据

删除数据可以直接调用管理对象上下文的deleteObject方法,删除完保存上下文即可。注意,删除数据前必须先查询到对应对象。

5.修改数据

修改数据首先也是取出对应的实体对象,然后通过修改对象的属性,最后保存上下文。

调试

虽然Core Data(如果使用SQLite数据库)操作最终转换为SQL操作,但是调试起来却不想操作SQL那么方便。特别是对于初学者而言经常出现查询报错的问题,如果能看到最终生成的SQL语句自然对于调试很有帮助。事实上在Xcode中是支持Core Data调试的,具体操作:Product-Scheme-Edit Scheme-Run-Arguments中依次添加两个参数(注意参数顺序不能错):-com.apple.CoreData.SQLDebug、1。然后在运行程序过程中如果操作了数据库就会将SQL语句打印在输出面板。

注意:如果模型发生了变化,此时可以重新生成实体类文件,但是所生成的数据库并不会自动更新,这时需要考虑重新生成数据库并迁移原有的数据。

FMDB

基本使用

相比于SQLite3来说Core Data存在着诸多优势,它面向对象,开发人员不必过多的关心更多数据库操作知识,同时它基于ObjC操作,书写更加优雅等。但是它本身也存在着一定的限制,例如如果考虑到跨平台,则只能选择SQLite,因为无论是iOS还是Android都可以使用同一个数据库,降低了开发成本和维护成本。其次是当前多数ORM框架都存在的性能问题,因为ORM最终转化为SQL操作,其中牵扯到模型数据转化,其性能自然比不上直接使用SQL操作数据库。那么有没有更好的选择呢?答案就是对SQLite进行封装。

其实通过前面对于SQLite的分析,大家应该已经看到KCDbManager就是对于SQLite封装的结果,开发人员面对的只有SQL和ObjC方法,不用过多libsqlite3的C语言API。但它毕竟只是一个简单的封装,还有更多的细节没有考虑,例如如何处理并发安全性,如何更好的处理事务等。因此,这里推荐使用第三方框架FMDB,整个框架非常轻量级但又不失灵活性,也是很多企业开发的首选。

1.FMDB既然是对于libsqlite3框架的封装,自然使用起来也是类似的,使用前也要打开一个数据库,这个数据库文件存在则直接打开否则会创建并打开。这里FMDB引入了一个MFDatabase对象来表示数据库,打开数据库和后面的数据库操作全部依赖此对象。下面是打开数据库获得MFDatabase对象的代码:

注意:dataWithPath中的路径参数一般会选择保存到沙箱中的Documents目录中;如果这个参数设置为nil则数据库会在内存中创建;如果设置为@””则会在沙箱中的临时目录创建,应用程序关闭则文件删除。

2.对于数据库的操作跟前面KCDbManager的封装是类似的,在FMDB中FMDatabase类提供了两个方法executeUpdate:和executeQuery:分别用于执行无返回结果的查询和有返回结果的查询。当然这两个方法有很多的重载这里就不详细解释了。唯一需要指出的是,如果调用有格式化参数的sql语句时,格式化符号使用“?”而不是“%@”、等。下面是两种情况的代码片段:

a.无返回结果

b.有返回结果

对于有返回结果的查询而言,查询完返回一个游标FMResultSet,通过遍历游标进行查询。而且FMDB中提供了大量intForColumn、stringForColumn等方法进行取值。

并发和事务

我们知道直接使用libsqlite3进行数据库操作其实是线程不安全的,如果遇到多个线程同时操作一个表的时候可能会发生意想不到的结果。为了解决这个问题建议在多线程中使用FMDatabaseQueue对象,相比FMDatabase而言,它是线程安全的。

创建FMDatabaseQueue的方法是类似的,调用databaseQueueWithPath:方法即可。注意这里不需要调用打开操作。

然后所有的增删改查操作调用FMDatabaseQueue的inDatabase:方法在block中执行操作sql语句即可。

之所以将事务放到FMDB中去说并不是因为只有FMDB才支持事务,而是因为FMDB将其封装成了几个方法来调用,不用自己写对应的sql而已。其实在在使用libsqlite3操作数据库时也是原生支持事务的(因为这里的事务是基于数据库的,FMDB还是使用的SQLite数据库),只要在执行sql语句前加上“begin transaction;”执行完之后执行“commit transaction;”或者“rollback transaction;”进行提交或回滚即可。另外在Core Data中大家也可以发现,所有的增、删、改操作之后必须调用上下文的保存方法,其实本身就提供了事务的支持,只要不调用保存方法,之前所有的操作是不会提交的。在FMDB中FMDatabase有beginTransaction、commit、rollback三个方法进行开启事务、提交事务和回滚事务。

收集自:http://www.cocoachina.com/ios/20141111/10190.html

留下评论