SQLite数据库编程

SQLite是一个开源的关系型数据库系统,它轻巧简单,性能出色,因此在嵌入式系统和手机等智能设备中使用非常广泛。

SQLite提供的是一个C语言级的操作API接口,使用起来不太方便。开源的FMDB 在它基础上做了进一步封装,提供了Cocoa的接口。另外FMDB还提供了多线程安全的访问接口,大大降低了在应用中使用FMDB的门槛和难度。

FMDB提供的是通用的数据库操作接口,我们基于实际的应用需要,将增删查询更新常用操作CURD接口做了更上层的封装。

SQLite数据库是明文存储,在考虑安全的场景下简单介绍了使用SQLCipher加密去保证数据的安全型。

数据库的元数据meta描述了数据库中的表定义和配置信息,我们介绍了如何获取SQLite表的元数据信息,通过元数据信息我们可以方便的构造自动化程序来做一些基本的代码生成。

FMDB介绍

FMDB库提供了2个核心对象FMDatabase,FMDatabaseQueue来管理操作数据库,FMResultSet是一个查询结果集。在单线程环境下可以直接使用FMDatabase去操作数据库,FMDatabaseQueue在多线程环境使用避免多线程数据访问修改冲突。

FMResultSet 结果集是一个游标式访问方式,通过执行它的next接口,直到返回空为止。
它提供了2类方便的数据获取接口:

  1. 根据列的顺序索引访问
  2. 根据列名称访问

另外它还提供了一个数据转换接口resultDictionary,能将当前记录转换为字典对象,里面以key/value形式存储。key为字段名,value为对应的值。

FMDBClass

下面是单线程访问数据库例子代码

//指定数据库的路径
NSString *dbPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:kDatabaseName];
//如果路径不存在 则返回
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL success = [fileManager fileExistsAtPath:dbPath];
if(!success){
    return;
}

FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
//判断是否可以正常打开数据库
if(![db open]){
    NSLog(@"error opening the database!");
    return ;
}
//数据查询
FMResultSet *rs = [db executeQuery:@"select * from Person"];
NSMutableArray *datas = [NSMutableArray array];
while ([rs next]) {
    NSLog(@"data %@",[rs resultDictionary]);
    [datas addObject:[rs resultDictionary]];
    //根据列名访问
    NSString *name = [rs stringForColumn:@"name"];
    int age = [rs intForColumn:@"age"];
    
    /*  根据列顺序访问
     NSString *name = [rs stringForColumnIndex:0];
     int age = [rs intForColumnIndex:1];
     */
}

//增加数据
BOOL isOK = [db executeUpdate:@"insert into Person values ('jhon',10)" ];

if(!isOK){
    NSLog(@"insert data failed!");
}
//删除数据
isOK = [db executeUpdate:@"delete from Person where age > 40 " ];

if(!isOK){
    NSLog(@"delete data failed!");
}

[db close];

多线程环境使用FMDatabaseQueue在Block中执行各种SQL操作,将上面代码直接放到块内部执行就可以,下面是一段示例代码。

FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
NSMutableArray *datas = [NSMutableArray array];
[queue inDatabase:^(FMDatabase *db) {
    //数据查询
    FMResultSet *rs = [db executeQuery:@"select * from Person"];
    while ([rs next]) {
        NSLog(@"data %@",[rs resultDictionary]);
        [datas addObject:[rs resultDictionary]];
        //根据列名访问
        NSString *name = [rs stringForColumn:@"name"];
        int age = [rs intForColumn:@"age"];
        
    }
    
}];

FMDatabaseQueue中创建了一个GCD的串行队列,同一个FMDatabaseQueue 共享同一个FMDatabase对象,每一个SQL操作都是同步执行,因此在多线程环境下保障了数据的安全有序的访问。

FMDBQueue

数据库操作接口封装

为了方便数据库管理,我们通过MDatabase数据库管理对象对FMDatabaseQueue做了接口封装。

DAO是一个数据访问对象,它依赖MDatabase暴漏的FMDatabaseQueue对象,提供了表的增删改查接口。借助于KVC提供的kv键值接口,对单个对象的增删修改接口同时支持MModel模型对象和NSDictonary类型。

MModel类提供了对象级数据库操作的接口,同时它也是表数据模型类的基类。

SQLiteMDatabaseClass

数据库管理对象MDatabase

MDatabase是一个单例对象,提供数据库打开关闭接口,同时它对外提供一个只读FMDatabaseQueue 类型的属性。

openDBWithName:打开数据库的方法是最关键方法:

  1. 它首先根据数据库文件名在应用的Document目录查找数据库文件是否存在。如果不存在说明是第一次访问数据库,则从应用的程序资源路径copy数据库文件到Document目录。

  2. 以数据库全路径为参数创建一个FMDatabaseQueue对列queue,如果queue不为空说明创建成功。

  3. queue创建成功后,并不会立即创建FMDatabase对象打开数据库文件,FMDatabase是懒加载创建的,只有需要执行数据库操作时才会创建一个FMDatabase对象缓存起来。

@class FMDatabaseQueue;
@interface MDatabase : NSObject
@property(nonatomic,readonly) FMDatabaseQueue *queue;
+(instancetype)sharedInstance;
-(BOOL)openDBWithName:(NSString*)dbName;
-(void)close;
@end

@interface MDatabase ()
@property(nonatomic,readwrite)FMDatabaseQueue *queue;
@property(nonatomic,strong)NSString *dbName;
@end

@implementation MDatabase
- (void)dealloc {
    [self close];
}

+ (instancetype)sharedInstance {
    static MDatabase *instace = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        instace = [[self alloc] init];
    });
    return instace;
}

- (BOOL)openDBWithName:(NSString*)dbName {
    NSString *dbPath = [[self docPath]stringByAppendingPathComponent:dbName];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error;
    BOOL success = [fileManager fileExistsAtPath:dbPath];
    if (!success) {
        NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:self.dbName];
        success = [fileManager copyItemAtPath:defaultDBPath toPath:dbPath error:&error];
        if (!success) {
            NSAssert1(0, @"Failed to create writable database file with message '%@'.", [error localizedDescription]);
            return NO;
        }
        else {
            NSLog(@"\n create database success");
        }
    }
    self.queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
    if(!self.queue){
        NSLog(@"\n create queue failed!");
        return NO;
    }
    return YES;
}

- (void)close {
    if(self.queue){
        [self.queue close];
    }
}

- (NSString*)docPath {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    return documentsDirectory;
}
@end

数据访问对象DAO/MDAO

DAO接口属性,包括表名词、字段列表、主键集,不包括主键的列。同时还有一个weak引用的FMDatabaseQueue对列queue,使用它来最终完成各种表的操作。

@property(nonatomic,copy)    NSString  *tableName;/*table name*/
@property(nonatomic,strong)  NSArray   *fldList;/*all coulumn list*/
@property(nonatomic,strong)  NSArray   *keyList;/*primary key coulumn list*/
@property(nonatomic,strong)  NSArray   *fldExcludeKeyList;/*coulumn list */
@property(nonatomic,weak)    FMDatabaseQueue *queue;

DAO对象对表操作提供了4组接口,下面只做一个简单说明,详细的实现可以参考本书提供的配套示例代码。

DAO对象实现的查询接口返回的行数据是json字典的格式;MDAO继承自DAO,返回的行数据是MModel模型对象。

1.记录数和分页相关接口

总记录数
-(NSInteger)numbersOfRecord;

按pageSize做分页大小的分页数
-(NSInteger)pageNumberWithSize:(NSInteger)pageSize;

基于SQL查询的记录数
-(NSInteger)numbersOfRecordWithSQL:(NSString*)sql;

SQL查询后的分页数
-(NSInteger)pageNumberWithSQL:(NSString*)sql pageSize:(NSInteger)pageSize;

2.增删修改接口

接口比较简单,核心是插入和修改时的SQL拼接的实现。

/*insert a record*/
-(BOOL)insert:(id)model;

/*update a record*/
-(BOOL)update:(id)model;

/*delete a record*/
-(BOOL)delete:(id)model;

/*delete all record*/
-(BOOL)removeAll;

下面是插入的具体实现代码,整个执行流程是:

1)首先判断表是否存在主键没有主键则退出执行。

2)如果表存在主键,但是当前数据中主键值为0,则查询表取最大的主键值加1做为当前要插入记录的主键值。

如果当前数据中主键值不为0 则执行一次查询,判断要插入的记录已经存在,如果存在则调用DAO的update:更新接口。

3)通过model对象动态构造SQL语句

4)通过queue执行SQL完成插入操作。

- (BOOL)insert:(id)model {
    if(!model){
        return NO;
    }
    NSInteger keyCount = [self.keyList count];
    if(keyCount>0){
        NSString *key = self.keyList[0];
        NSInteger keyID = [[model valueForKey:key]integerValue];
        if(keyID==0){
            keyID = [[self findMaxKey]integerValue]+1;
            [model setValue:@(keyID) forKey:key];
        }
    }
    if([self findByKey:model]){
        DLog(@"info:add %@ exsist!\n",self.tableName);
        return [self update:model];
    }
    NSMutableString *vals = [[NSMutableString alloc]initWithCapacity:10];
    NSInteger fieldCount = [self.fldList count];
    for(NSInteger i =0 ;i < fieldCount;i++){
        if(i!=fieldCount-1){
            [vals appendString:@"?,"];
        }
        else{
            [vals appendString:@"?"];
        }
    }
    NSString *fieldString = [self.fldList componentsJoinedByString:@","];
    NSString *sql = [NSString stringWithFormat:@"INSERT INTO %@ (%@) VALUES ( %@ )",self.tableName,fieldString,vals];
    DLog(@"insert sql=%@",sql)
    NSMutableArray *args=[[NSMutableArray alloc]initWithCapacity:10];
    for(NSInteger i =0 ;i < fieldCount;i++){
        NSString *mapkey = [self.fldList objectAtIndex:i];
        id value = [model valueForKey:mapkey];
        if(!value){
            value = [NSNull null];
        }
        [args addObject:value];
    }
    
    __block BOOL  isOK  = NO;
    [self.queue inDatabase:^(FMDatabase *db) {
        isOK = [db executeUpdate:sql withArgumentsInArray:args];
        DLog("insert lastErrorMessage =%@",[db lastErrorMessage]);
    }];
    return isOK;
}

3.SQL执行接口

执行SQL语句
-(BOOL)sqlUpdate:(NSString*)sql;

-(BOOL)sqlUpdate:(NSString*)sql withArgumentsInArray:(NSArray*)args;

-(BOOL)sqlUpdate:(NSString*)sql withParameterDictionary:(NSDictionary*)dics;

4.SQL查询接口

根据SQL查询数据
-(NSArray*)sqlQuery:(NSString*)sql;

根据SQL和分页参数查询数据
-(NSArray*)sqlQuery:(NSString*)sql pageIndex:(NSInteger)pageIndex pageSize:(NSInteger)pageSize;

-(NSArray*)sqlQuery:(NSString*)sql  withArgumentsInArray:(NSArray*)args;

-(NSArray*)sqlQuery:(NSString*)sql  withParameterDictionary:(NSDictionary*)dics;

查询所有数据
-(NSArray*)findAll;

根据主键组合的kv字典对象查询
-(id)findByKey:(NSDictionary*)kv;

返回最大的主键值
-(id)findMaxKey;

以kv字典构造查询条件查询数据
-(NSArray*)findByAttributes:(NSDictionary*)Attributes;

表数据分页查询
-(NSArray*)findByPage:(NSInteger)pageIndex pageSize:(NSInteger)pageSize;

数据模型对象MModel

MModel有点类似Core Data中的模型对象 或者Active Record,创建了它以后可以透明的完成增删修改的动作,不需要跟底层的数据库接口有任何交互。

MModel接口定义

@interface MModel : NSObject
-(id)initWithDictionary:(NSDictionary *)dictionary;
-(BOOL)save;
-(BOOL)update;
-(BOOL)delete;
@end

MModel的实现

#import "MModel.h"
#import "MDAO.h"

@interface MModel ()
@property(nonatomic,strong)MDAO *dao;
@end

@implementation MModel
- (id)initWithDictionary:(NSDictionary *)dictionary {
    self = [super init];
    if(!self){
        return nil;
    }
    return self;
}

- (MDAO*)dao {
    if(!_dao){
        NSString *selfName = NSStringFromClass([self class]);
        NSString *className = [NSString stringWithFormat:@"%@MDAO",selfName];
        Class class = NSClassFromString(className);
        return [[class alloc]init];
    }
    return _dao;
}

- (BOOL)save {
    return [self.dao insert:self];
}

- (BOOL)update {
    return [self.dao update:self];
}

- (BOOL)delete {
    return [self.dao delete:self];
}

具体使用

我们以SQLite数据库中的一个表Person为例说一个简单说明。

Person表的创建的SQL语句如下:

CREATE TABLE Person ("id" INTEGER NOT NULL,"name" VARCHAR ,"address" VARCHAR ,"age" INTEGER ,PRIMARY KEY ( "id" ))

SQLiteTablePerson

Person表对应的数据模型类定义实现如下

#import "MModel.h"
@interface Person : MModel
@property (nonatomic, assign) NSInteger id;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *address;
@property (nonatomic, assign) NSInteger age;
@end

#import "Person.h"
@implementation Person
@end

PersonMDAO的定义和实现

#import <Foundation/Foundation.h>
#import "MDAO.h"
@interface PersonDAO : MDAO
@end

在具体的MDAO中要定义好表名,主键,初主键外其它列名。

#import "PersonMDAO.h"
@implementation PersonMDAO
- (id)init{
    self = [super init];
    if (self) {
        self.tableName = @"Person";
        self.fldList   = [[NSArray alloc]initWithObjects:@"id",@"name",@"address",@"age",nil];
        self.keyList   = [[NSArray alloc]initWithObjects:@"id",nil];
        NSMutableSet *allFieldSet = [[NSMutableSet alloc]initWithArray:self.fldList];
        NSSet *keyFieldSet = [[NSSet alloc]initWithArray:self.keyList];
        [allFieldSet minusSet:keyFieldSet];
        self.fldExcludeKeyList = [allFieldSet allObjects];
    }
    return self;
}
@end

下面是AppDelegate中的具体通过DAO,Model类来进行数据库操作的代码

#import "AppDelegate.h"
#import "MDatabase.h"
#import "PersonDAO.h"
#import "Person.h"
#define  kDatabaseName   @"DatabaseDemo.sqlite"

@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    if(![[MDatabase sharedInstance]openDBWithName:kDatabaseName]){
        NSLog(@"Open Db %@ Failed!",kDatabaseName);
        return;
    }
    
    //查询所有记录
    PersonDAO *personDAO = [[PersonDAO alloc]init];
    NSArray *persons = [personDAO findAll];
    for(Person *p in persons){
        NSLog(@"name %@",p.name);
        NSLog(@"address %@",p.address);
    }
    
    //修改并保存
    for(Person *p in persons){
        p.address = @"Japan";
        [p save];
    }
    
    persons = [personDAO findAll];
    for(Person *p in persons){
        NSLog(@"name %@",p.name);
        NSLog(@"new address %@",p.address);
    }
    
    //条件查询 name=to
    persons = [personDAO findByAttributes:@{@"name":@"to"}];
    for(Person *p in persons){
        NSLog(@"name %@",p.name);
    }
    
    //根据主键查询
    Person *person = [personDAO findByKey:@{@"id":@(1)}];
    //删除数据
    [person delete];
}

数据库加密

下载支持加密的FMDB版本

新建一个工程为SQLCipherDemo,进入工程目录,创建一个podfile,内容为:
pod 'FMDB/SQLCipher'
打开命令行终端,进入工程根目录,执行 pod install,等待下载完成。
关闭SQLCipherDemo工程,打开新的pods生成的SQLCipherDemo.xcworkspace工程。

SQLCipherDemoPrj

自己编译不依赖pods的独立文件

新建一个FMDBEncrypt工程,在工程导航区创建FMDB组。从SQLCipherDemo.xcworkspace工程中 Pods\FMDB\common,Pods\SQLCipher\common下分别拖入FMDB 相关文件和sqlite3.h,sqlite3.c文件到新工程的FMDB组。

增加libsqlite3.dylib,Security.framework 到工程中。

libsqlite3

修改工程Build Setting中Other Linker Flags 和 Other C Flags.

双击Other Linker Flags 输入区,输入下面参数

$(inherited) -ObjC -framework Security

双击Other Libririan Flags 输入区,输入下面参数

$(OTHER_LDFLAGS)

双击Other C Flags 输入区,输入下面参数

$(inherited) -DSQLITE_HAS_CODEC -DSQLITE_HAS_CODEC -DSQLITE_TEMP_STORE=2 -DSQLITE_THREADSAFE -DSQLCIPHER_CRYPTO_CC

运行工程,编译成功!

加密数据库

对于已经存在的明文未加密的数据库,我们可以使用SQLCipher提供的函数创建一个新的空的数据库,然后把原来的数据库attach到新数据库,完成数据迁移到加密后的新数据库。

#define kDBKEY                  @"secureKey"
#define kDBName               @"Database.sqlite"
#define kDBEncryptName   @"DatabaseEn.sqlite"

NSString *originalDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:kDBName];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *newPath = [documentsDirectory stringByAppendingPathComponent:kDBEncryptName];

const char* SQL = [[NSString stringWithFormat:@"ATTACH DATABASE '%@' AS encrypted KEY '%@';",newPath,kDBKEY] UTF8String];

sqlite3 *unencrypted_DB;
if (sqlite3_open([originalDBPath UTF8String], &unencrypted_DB) == SQLITE_OK) {
    sqlite3_exec(unencrypted_DB, SQL, NULL, NULL, NULL);
    sqlite3_exec(unencrypted_DB, "SELECT sqlcipher_export('encrypted');", NULL, NULL, NULL);
    sqlite3_exec(unencrypted_DB, "DETACH DATABASE encrypted;", NULL, NULL, NULL);
    sqlite3_close(unencrypted_DB);
}

打开老数据库,通过执行 sqlcipher_export SQL语句完成创建新数据库完成加密。

操作加密数据库

每次操作加密库之前都需要设置Key。

FMDatabase *db = [FMDatabase databaseWithPath:newPath];
if (![db open]) {
    return;
}else{
    [db setKey:kDBKEY];
}
FMResultSet *s = [db executeQuery:@"SELECT * FROM Person"];
while ([s next]) {
    NSLog(@"%@", [s resultDictionary]);
}

获取SQLite元数据

数据库的元数据中描述了所有的Schema信息,包括表的定义,每个字段的数据类型定义信息。SQLite数据库提供了支持的专门的SQL语句来获取元数据信息。

我们需要将获取的元数据信息存储以备后用,因此先定义表和字段的数据模型类

1.表元数据模型Table类

包括表名 字段列表 主键列表

@interface Table : NSObject

/** Table name */
@property(nonatomic,strong)NSString *name;

/** Table Columns */
@property(nonatomic,strong)NSArray  *fields;

/** Table Primary Keys */
@property(nonatomic,strong)NSArray  *keys;
@end

2.字段元数据模型类Field

主要的属性是字段名称,数据库类型,objc类型,是否为主键。剩余的is开头的BOOL类型的仅仅是为了方便支持代码生成而增加。

@interface Field : NSObject
@property(nonatomic,strong)NSString  *name;//字段名称
@property(nonatomic,strong)NSString  *type;//字段数据库类型
@property(nonatomic,strong)NSString  *objcType;//字段数据库类型对应的objc类型
@property(nonatomic,assign)BOOL      isKey;//是否为主键

@property(nonatomic,assign) BOOL     isSimpleType;//是否为基本的值类型数据(数字,BOOL)
@property(nonatomic,assign) BOOL     isBOOL;
@property(nonatomic,assign) BOOL     isINTEGER;
@property(nonatomic,assign) BOOL     isNSString;
@property(nonatomic,assign) BOOL     isINT;
@property(nonatomic,assign) BOOL     isLONG;
@property(nonatomic,assign) BOOL     isDOUBLE;
@property(nonatomic,assign) BOOL     isFLOAT;
@property(nonatomic,assign) BOOL     isTEXT;
@property(nonatomic,assign) BOOL     isVARCHAR;
@property(nonatomic,assign) BOOL     isDATETIME;
@property(nonatomic,assign) BOOL     isNUMERIC;
@end

3.元数据获取

SQLite数据库中表sqlite_master存储了元数据信息,查询表信息的SQL如下

SELECT * FROM sqlite_master where type = 'table'

查询表相信信息,包括字段信息的SQL如下:

PRAGMA table_info (表名)

我们在之前的MDatabase的基础上增加一个Meta扩展,提供tables接口来查询Meta信息

#import "MDatabase.h"

@interface MDatabase(Meta)
- (NSArray*)tables;
@end
#import "MDatabase+Meta.h"
#import "FMDatabaseQueue.h"
#import "FMDatabase.h"
#import "Table.h"
#import "Field.h"
@implementation MDatabase (Meta)

- (NSArray*)tables {
    NSMutableArray *tables = [NSMutableArray array];
    [self.queue inDatabase:^(FMDatabase *db) {
        NSString *sql = @"SELECT * FROM sqlite_master where type = 'table' ";
        FMResultSet *rs = [db executeQuery:sql];
        while ([rs next]) {
            //NSDictionary *dict = [rs resultDictionary];
            NSString *tableName = [rs stringForColumn:@"tbl_name"];
            Table *table = [[Table alloc]init];
            table.name = tableName;
            [tables addObject:table];
        }
        [rs close];
        
        for(Table *table in tables){
            NSString *tableSQL = [[NSString alloc] initWithFormat:@" PRAGMA table_info ( %@ ) ", table.name];
            FMResultSet *rs = [db executeQuery:tableSQL];
            NSMutableArray *fields =  [NSMutableArray array];
            while ([rs next]) {
                //NSDictionary *dict = [rs resultDictionary];
                Field *field= [[Field alloc]init];
                NSString *fName = [rs stringForColumn:@"name"];
                NSString *fType = [rs stringForColumn:@"type"];
                field.name = fName;
                field.type = fType;
                [fields addObject:field];
            }
            [rs close];
            table.fields = fields;
        }
    }];
    return tables;
}
@end

查询的Person表元数据信息转换为JSON字典的打印:

{
    name = Person;
    rootpage = 2;
    sql = "CREATE TABLE Person (\"id\" INTEGER NOT NULL,\"name\" VARCHAR ,\"address\" VARCHAR ,\"age\" INTEGER ,PRIMARY KEY ( \"id\" ))";
   "tbl_name" = Person;
    type = table;
}

主键列信息JSON字典的打印:

{
    cid = 0;
    "dflt_value" = "<null>";
    name = id;
    notnull = 1;
    pk = 1;
    type = INTEGER;
}

模版引擎

Xcode中的模版

Xcode实际上就是使用模版来帮我们生成各种应用的框架代码和类文件代码。

打开Xcode安装目录里面的Templates路径

Mac的Template路径
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates/Source

iOS的Template路径
Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Templates/Source

会发现很多iOS的类模版文件
XcodeTemplate

我们找到UITableViewControllerObjective-C的模版文件___FILEBASENAME__.h 和 FILEBASENAME.m 打开内容看看里面是什么?

UITableViewController_Temp_H

UITableViewController_Temp_M

我们注意到这2个文件中抽取出了UITableViewController类的基本公共内容。
顶部是类的说明,接下来是包含的头文件。
2个文件中有很多__XXXX__ 样式的字串是每次需要动态生成的变化部分。具体来说就是文件名、类名、工程名、作者、日期、版权说明、包含的文件等。

模版引擎处理流程

Cocoa中有很多开源模版项目,MGTemplateEngine和DMTemplateEngine都是优秀的模版处理框架,我们以DMTemplateEngine为例来说明如何使用它来实现一个代码生成器。

模版引擎的处理流程:

  1. 引擎加载模版文件
  2. 使用配置参数数据对模版进行渲染
  3. 输出生成的新文件

TemplateRenderProgress

渲染的过程就是使用配置的参数数据对模版文件进行参数替换,输出新的处理结果。

DMTemplateEngine详细的语法参数定义说明请参考本书附录部分开源项目中对它的介绍。

下面来看看如何通过DMTemplateEngine引擎来进行模版化处理。

DMTemplateEngine中模版的参数定义格式为 {% xxxx %},xxxx为参数名。

DMTemplateEngine* engine = [DMTemplateEngine engine];
//加载模版
engine.template = @"Hello, my name is {% firstName %}.";

NSMutableDictionary* templateData = [NSMutableDictionary dictionary];
//参数设置
[templateData setObject:@"Dustin" forKey:@"firstName"];

//渲染
NSString* renderedTemplate = [engine renderAgainst:templateData];

渲染后的结果:
Rendered: Hello, my name is Dustin.

表模型自动化代码生成

我们已经完成了表元数据的获取,那么结合模版引擎处理技术,可以非常方便的实现数据库Schema定义完成后,自动化的生成表模型相关代码。

SQLite+Code

我们来开发一个SQLite数据库表属性模型的生成器,演示一下DMTemplateEngine的具体使用。

模版文件定义

先从表模型Model代码开始,分析代码中有规律的变化部分,确定出模版中的参数,定义出模版文件。

1.Person.h头文件代码

/*
 Person.h
 SQLite
 Created by author on 06/09/2016 10:57AM.
 Copyright (c) 2016 author. All rights reserved.
 */
#import "MModel.h"
@interface Person : MModel
@property (nonatomic, assign) NSInteger id;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *address;
@property (nonatomic, assign) NSInteger age;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@end

类名、工程名、作者、创建时间、版权人部分都定义为模版参数。

类的属性部分,通过DMTemplateEngine的循环语句,对每个field字段判断是简单的数据类型就生成assign类型的的属性定义,否则就是strong类型的定义。

下面是根据Person类头文件定义的模版文件Model.hp

/*
{% table.name %}.h
{%Project%}
Created by {%Author%} on {%CreateDate%}.
Copyright (c) 2016 {%CopyRights%}. All rights reserved.
*/
#import "MModel.h"
@interface {% table.name %} : MModel
{% foreach(field in table.fields) %}
{% if(field.isSimpleType==YES) %}
@property (nonatomic, assign) {% field.objcType %} {% field.name %};
{% else %}
@property (nonatomic, strong) {% field.objcType %} *{% field.name %};
{% endif %}
{% endforeach %}
-(id)initWithDictionary:(NSDictionary *)dictionary;
@end

2.Person.m 实现代码

/*
Person.m
SQLite
 Created by author on 06/09/2016 10:57AM.
 Copyright (c) 2016 author. All rights reserved.
*/
 #import "Person.h"
@interface Person()
@end
@implementation Person
/*init property member var by parsing NSDictionary parameter*/
- (id)initWithDictionary:(NSDictionary *)dictionary{
    if ((self = [super init]) && (dictionary)) {
        id value ;
        value = dictionary[@"id"];
        if(value) _id = [value longValue];

        value = dictionary[@"name"];
        if(value) _name = value;  

        value = dictionary[@"address"];
        if(value) _address = value;  

    }
    return self;  
}               
@end

跟头文件模版一样,类名、工程名、作者等都定义为模版参数。
不同的在循环处理表字段时、这里会根据每个表字段的类型判断做不同的类型转换处理,这就是为什么我们在设计前面“获取SQLite元数据”章节在看到Field类时增加了很多BOOL类型的原因。

下面是根据Person类头文件定义的模版文件Model.mp

/*
{% table.name %}.m
{%Project%}
Created by {%Author%} on {%CreateDate%}.
Copyright (c) 2014 {%CopyRights%}. All rights reserved.
*/
#import "{%table.name%}.h"
@interface {%table.name%}()
@end

@implementation {%table.name%}
/*init property member var by parsing NSDictionary parameter*/
-(id)initWithDictionary:(NSDictionary *)dictionary{
    if ((self = [super init]) && (dictionary)) {
        id value ;
       {% foreach(field in table.fields) %}
        value = dictionary[@"{%field.name%}"];
        {%if(field.isSimpleType==YES) %}
            {%if(field.isBOOL==YES) %}
               if(value) _{%field.name%} = [value boolValue];
            {% endif %}
           {%if(field.isINTEGER==YES) %}
              if(value) _{%field.name%} = [value longValue];
           {% endif %}
           {%if(field.isDOUBLE==YES) %}
              if(value) _{%field.name%} = [value doubleValue];
           {% endif %}
           {% if (field.isFLOAT==YES) %}
                if(value) _{%field.name%} = [value floatValue];
           {% endif %}
       {%else %}
           if(value) _{%field.name%} = value;  
       {% endif %}

       {% endforeach %}  
    }
    return self;  
}               
@end

代码实现

整个流程为加载模版文件,参数配置,渲染模版,最后是生成代码。

#import "CodeGenerate.h"
#import "DMTemplateEngine.h"
#import "MDatabase+Meta.h"
#import "Table.h"
#import "Field.h"
@implementation CodeGenerate

- (void)makeCode {
    NSArray *tables = [[MDatabase sharedInstance] tables];
    if([tables count]<=0){
        return;
    }
    
    DMTemplateEngine* engine = [DMTemplateEngine engine];
    NSString *headTemplate = [self loadModelTempalte:@"Model.hp"];
    NSString *sourceTemplate = [self loadModelTempalte:@"Model.mp"];
    
    for(Table *table in tables ){
        NSDictionary *templateData = [self templateParametersWithTable:table];
        //从文件加载头文件模版
        engine.template = headTemplate;
        //头文件源码渲染并写入文件
        NSString* renderedHeadTemplate = [engine renderAgainst:templateData];
        NSLog(@"renderedHeadTemplate\n %@ ",renderedHeadTemplate);
        NSString *headFileName = [NSString stringWithFormat:@"%@.h",table.name];
        [self createFileWithName:headFileName withFileConten:renderedHeadTemplate];
        
        //从文件加载实现文件模版
        engine.template = sourceTemplate;
        //实现文件源码渲染并写入文件
        NSString* renderedSourceTemplate = [engine renderAgainst:templateData];
        NSLog(@"renderedSourceTemplate\n %@ ",renderedSourceTemplate);
        NSString *sourceFileName = [NSString stringWithFormat:@"%@.m",table.name];
        [self createFileWithName:sourceFileName withFileConten:renderedSourceTemplate];
    }
}

- (NSString*)loadModelTempalte:(NSString*)modelName {
    NSString *filePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:modelName];
    NSURL *url = [NSURL fileURLWithPath:filePath];
    NSError *error;
    NSString *modelContent = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];
    return modelContent;
}

- (NSDictionary*)templateParametersWithTable:(Table*)table {
    NSMutableDictionary* templateData = [NSMutableDictionary dictionary];
    //参数设置
    [templateData setObject:table forKey:@"table"];
    [templateData setObject:@"Demo" forKey:@"Project"];
    [templateData setObject:@"MacDev" forKey:@"Author"];
    [templateData setObject:@"2016-06-09" forKey:@"CreateDate"];
    [templateData setObject:@"www.macdev.io" forKey:@"CopyRights"];
    
    return templateData;
}

- (void)createFileWithName:(NSString*)fileName withFileConten:(NSString*)content {
    NSFileManager *fm = [NSFileManager defaultManager];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *fileOutPath = [documentsDirectory stringByAppendingPathComponent:fileName];
    
    //创建输出文件
    [fm createFileAtPath:fileOutPath contents:nil attributes:nil];
    NSURL *outUrl = [NSURL fileURLWithPath:fileOutPath];
    
    //保存模版生成的内容到文件
    NSError *error;
    [content writeToURL:outUrl atomically:YES encoding:NSUTF8StringEncoding error:&error];
    if(error){
        NSLog(@"save file error %@",error);
    }
}

同样的思路我们可以实现表数据操作的DAO代码的自动化生成,这个留给读者自行完成。