[iOS]基于ORM思想的数据库处理
本文主要是介绍iOS上数据库存储方案,利用ORM对FMDB做一层封装,让代码更加简洁方便。
一、数据库开发的问题 有了FMDB对sqlite 的封装,iOS端的数据库操作相当方便,我们再也不用面对那些磨人的线程问题和蛋疼的sqlite。但是在实际开发中仍无法避免的遇到一些蛋疼问题。
(void)createPaintTable{ [self.db executeUpdate:@”CREATE TABLE paint_table (paintID varchar(32), answercount int, create_timestamp double, last_modified_time double, imgwidth int, imgheight int, imgurl varchar(256), last_reply_user_id varchar(32), text varchar(512), money int, questype int, replytype int, user_id varchar(32), viewcount int, commentcount int, supportcount int, author_id varchar(32), replyAuthor_id varchar(32), accepted_user_id varchar(32))”];
[self checkError]; }
这是简单的创建一张表,我想你看到这样的代码都要发疯。然而更让人发疯的是某天因为产品的需求我们需要增加一个字段,那我们不得不在在各个地方改啊改。而一旦因为疏忽数据库升级没有处理好,就可能造成很严重的线上事故。(很遗憾我就遇到了这种事故)
二、如何解决这种问题
2.1 Key-Value式的存储 - YTKKeyValueStore iOS客户端的数据表往往还要对应一个Model,这导致在使用时还要加层Model转化,而移动端对于数据库的查询要求并不高,针对这些特点Key-Value式的存储应运而生。比较代表性的框架有 猿题库的 YTKKeyValueStore(主要思路是将数据模型进行序列化做为Value存储)。但是这类框架的缺陷也相当明显,只能通过主Key查询。而且如果模型发生变化,数据升级处理也只能放在 模型转换时处理。Key-Value式的存储实际上已经不能称之为数据库存储了,它是根据客户端的特殊需求而产生的一种特殊产物,数据库对它而言只是一种容器。
2.2 ORM框架-GYDataCenter ORM(Object Relational Mapping)框架采用元数据来描述对象一关系映射细节,元数据一般采用XML格式,并且存放在专门的对象一映射文件中。只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。当前ORM框架主要有四种:Hibernate(Nhibernate),iBATIS,mybatis,EclipseLink。
以上是百度百科对ORM框架的解释,大家注意我标注的两个关键词,运行时以及对象,iOS客户端的数据库存储本质就是要将一个对象存储到数据库,而OC也正是一种运行时语言,我们要做的就是建立一个映射文件,在运行时参照映射文件产生对应sql语句将对象保存到数据库。
MLeaksFinder这个框架我想不少同学都不陌生,它采用了一种很巧妙的方式,让我们在DEBUG模式下可以轻松的发现内存泄漏问题。今天我逛作者 Zepo 的gitHub时发现他又写了另外一个框架GYDataCenter我简单的看了一下它正是采用ORM思想写的一款iOS客户端的数据库存储方案,与我的想法不谋而合。
我并没有看GYDataCenter的源码,只是根据使用方式提出一些问题。GYDataCenter的 映射关系 是通过GYModelObject提供相应方法来提供,而使用必需继承GYModelObject实现对应方法。也就是说作者将 映射关系 与 对象模型 合在了GYModelObject类,造成了耦合。这种耦合会造成一些问题,在项目中 模型使用的地方太多,模型转换,view展示,数据处理都要涉及到模型将 映射关系 加到模型中很容易导致模型的臃肿。而继承关系的使用也使得入侵性太强。所以说 映射关系 应该独立于对象 模型存在。
三、JYDatabase(铺垫了那么多这才是我要讲的……) JYDatabase 这是本人基于ORM思想写的一款数据库应用框架,和GYDataCenter一样也是基于FMDB实现的,下面主要讲一下框架的实现和使用。
3.1 要解决的问题 1.自动处理数据库升级,让使用者不用考虑数据库升级带来烦劳。 2.封装简单常用的查询语句,让使用者只用关注特殊的SQL查询,不用被蛋疼的sql语句折磨。
3.2 数据表的建立(映射关系的建立) 数据表的建立需要继承 JYContentTable(该类实现了工作中用到的大部分SQL查询),只要重写以下几个方法就可以快速创建一张数据表。
// 必须实现 contentClass 是该表所对应的模型类,tableName 是表的名字
- (void)configTableName{ self.contentClass = [JYPersonInfo class]; self.tableName = @"JYPersonTable"; }// 必须实现 contentId 是该表的主键(也是唯一索引)比如用户的userId 必须是 contentClass 的属性
- (NSString *)contentId{ return @"personnumber"; }// 数据表的其他字段,必须是 contentClass 的属性,如不实现则默认取 contentClass 以“DB”结尾 的属性
- (NSArray<NSString *> *)getContentField{ return @[@"mutableString1",@"integer1",@"uInteger1",@"int1",@"bool1",@"double1"]; }// 表创建时对应字段的默认长度,如不写,取默认。
- (NSDictionary*)fieldLenght{ return @[@"mutableString1":@"512"]; }// 查询是否使用NSCache缓存,默认YES。
- (BOOL)enableCache{ return NO; }注意:1.数据表映射的属性支持 NSString NSMutableString NSInteger NSUInteger int BOOL double float NSData 等数据类型,具体如下:
NSDictionary * jy_correspondingDic(){ return @{@”Tb”:@”BOOL”, @”TB”:@”BOOL”, @”Tc”:@”BOOL”, @”TC”:@”BOOL”, @”Td”:@”DOUBLE”, @”TD”:@”DOUBLE”, @”Tf”:@”FLOAT”, @”TF”:@”INTEGER”, @”Ti”:@”INTEGER”, @”TI”:@”INTEGER”, @”Tq”:@”INTEGER”, @”TQ”:@”INTEGER”, @”T@\”NSMutableString\”“:@”VARCHAR”, @”T@\”NSString\”“:@”VARCHAR”, @”T@\”NSData\”“:@”BLOB”, @”T@\”UIImage\”“:@”BLOB”, @”T@\”NSNumber\”“:@”BLOB”, @”T@\”NSDictionary\”“:@”BLOB”, @”T@\”NSMutableDictionary\”“:@”BLOB”, @”T@\”NSMutableArray\”“:@”BLOB”, @”T@\”NSArray\”“:@”BLOB”,}; }
2.NSCache的默认缓存条数是20条,可自行设置修改self.cache.countLimit = 20; 使用enableCache 将优先从缓存中取数据 如自行实现的查询请在适当情况下使用以下三个方法来加入缓存。方法内部有 enableCache 的实现。
- (id)getCacheContentID:(NSString *)aID; - (void)saveCacheContent:(id)aContent; - (void)removeCacheContentID:(NSString *)aID;3.3数据库的创建和升级管理 3.3.1数据库的创建和升级管理类需要继承JYDataBase
关键方法:
// 该方法会根据当前版本判断 是创建数据库表还是 数据表升级
- (void)buildWithPath:(NSString *)aPath mode:(ArtDatabaseMode)aMode;// 返回当前数据库版本,只要数据表有修改 返回版本号请 +1 默认返回 1
(NSInteger)getCurrentDBVersion{ return 4; }// 所有数据表的创建请在该方法实现 调用固定方法 - (void)createTable:(FMDatabase *)aDB; - (void)createAllTable:(FMDatabase *)aDB{ [self.personTable createTable:aDB]; }
// 所有数据表的升级请在该方法实现 调用固定方法 -
(void)insertDefaultData:(FMDatabase *)aDb; - (void)updateDB:(FMDatabase *)aDB{ [self.personTable updateDB:aDB]; }3.3.2数据库升级的实现- (void)updateDB:(FMDatabase *)aDB
对于每一张表,所谓的修改无非是 添加了一个 新的字段 或减少了几个字段(这种情况很少)。
字段的对比 PRAGMA table_info([tableName]) 可以获取一张表的所有字段 再与当前需要的字段做对比即可得到要增加的字段和减少的字段
字段的添加 sqlite有提供对应的SQL语句实现 ALTER TABLE tableName ADD 字段 type(lenght)
减少字段 sqlite并未提供对应的SQL语句实现但可通过以下方法实现 a.根据原表新建一个表 sql = [NSString stringWithFormat:@”create table %@ as select %@%@ from %@”, tempTableName,[self contentId],tableField,self.tableName]; b.删除原表 sql = [NSString stringWithFormat:@”drop table if exists %@”, self.tableName]; c.将表改名 sql = [NSString stringWithFormat:@”alter table %@ rename to %@”,tempTableName ,self.tableName];
d.为新表添加唯一索引
sql = [NSString stringWithFormat:@"create unique index '%@_key' on %@(%@)", self.tableName,self.tableName,[self contentId]];3.4条件查询的实现 关于复杂查询我提供了一个简单的方法
- (NSArray *)getContentByConditions:(void (^)(JYQueryConditions *make))block;可以看下它的使用
NSArray*infos = [[JYDBService shared] getPersonInfoByConditions:^(JYQueryConditions *make) { make.field(@"personnumber").greaterThanOrEqualTo(@"12345620"); make.field(@"bool1").equalTo(@"1"); make.field(@"personnumber").lessTo(@"12345630"); make.asc(@"bool1").desc(@"int1"); }];其实它不过产生了如下一条查询语句:
SELECT * FROM JYPersonTable WHERE personnumber >= 12345620 AND bool1 = 1 AND personnumber < 12345630 order by bool1 asc , int1 desc make.field(@"personnumber").greaterThanOrEqualTo(@"12345620"); 就代表了 personnumber >= 12345620实现实际相当简单,先用 JYQueryConditions 记录下所描描述的参数,最后再拼接出完整的sql语句。 至于通过点语法的链式调用则参考了 Masonry 的声明方式
- (JYQueryConditions * (^)(NSString *compare))equalTo; - (JYQueryConditions * (^)(NSString *field))field{ return ^id(NSString *field) { NSMutableDictionary *dicM = [[NSMutableDictionary alloc] init]; dicM[kField] = field; [self.conditions addObject:dicM]; return self; }; }四、结尾 关于JYDatabase的使用和介绍 大家可以去gitHub查看更详细的说明
https://github.com/weijingyunIOS/JYDatabase,关于上面介绍的框架大家可以根据实际需要做下对比后选择使用。 GYDataCenter 是相当不错的数据库处理方案,如果它早点出来或者我早点看到也许就不会花费大量精力去写JYDatabase了,不过写JYDatabase也是很愉快的因为它的思路走向是根据我自身遇到的痛点来写的,比如sql查询,就是因为sql语句可能就是因为一个关键位置的空格问题导致无法执行,所以我专门写了个JYQueryConditions来弱化sql。