MacOS / iOS 下数据库使用及 ActiveRecord 的实现

在开发iOS / macOS 应用中, 经常会使用数据库(sqlite)来保存数据, Apple也提供了一个庞大的神器--CoreData, 不过个人感觉这个神器跟Java一样,复杂的事情并没有变的简单, 简单的事情反而变得不简单,而且对开发者屏蔽了太多的细节, 因此在一些稍微大一点的应用中, 对于使用CoreData都显得颇为谨慎。

另外一个神器就是FMDB, 通过对sqlite接口的轻量级封装, 给开发者提供了很方便的接口。 但是FMDB仅仅只是sqlite的包装, 并没有给上层提供更好的数据管理支持(其实这也是优势, 很好的体现了单一职责的思想), 在开发App的时候, 还需要自己实现数据库管理逻辑。

数据库管理层的实现, 就各显神通了, 大牛小牛都根据自己的理解或者结合自己的业务造出自己的轮子, 好不好用, 效果如何只有自己知道。 开源的各种轮子和介绍也很多, 这里就不细说了。

好的轮子是什么样子的

个人愚见, 一个好用的数据管理库, 应该提供以下功能:

  • 支持连接多个数据库 这个很好理解, 一般应用, 都有多个模块, 每个模块之间, DB最好独立管理。 另外, 不同账户之间, 也最好分开不同的DB。
  • 支持数据迁移 随着应用版本不停的迭代, 数据库字段/字段值可能也会发生变化, 这个时候就需要有一个数据迁移机制。
  • 友好的数据操作接口 虽然SQL对于程序员来说,不应该成为一个门槛, 但是在APP中手工拼接SQL字符串,并不是一个令人愉快的开发体验。
  • 自动的 Model & DB mapping 作为一个总想着偷懒的程序员, 手写那么一大堆的赋值取值操作, 我是忍受不了的, 而且这种令人厌烦的体力活也容易出错(好吧, 我在烦躁的时候是无法仔细去检查这些无聊的代码的)。

老实说, 提供这些功能的轮子github上有很多 , 我也尝试了很多, 比如 iActiveRecord, FCModel, ObjectiveRecord, FMDB-ActiveRecord等等, 这些框架各自都有亮点, 但是总是没有提供以上全部特性, 或者实现的并不是特别好, 因此我决定自己造一个轮子(去年给自己挖的一个大坑, 不过捋清思路之后, 很快就做出原型来了, 之后用它完成了公司一个项目,另外的几个项目也在迁移中),暂时没想到高大上的名字, 就先叫patchwork, 意思是这个库是把一些小工具拼起来, 组成一个快速开发的基础工具库。这个库目前主要提供数据管理,网络中间层,model转换等,还有一些常用的小工具。 代码见GitHub。

Patchwork的实现

Patchwork的数据库层结构如下:

MacOS / iOS 下数据库使用及 ActiveRecord 的实现_第1张图片
ALDB structs.png

FMDB

这个轮子就没必要再自己造了。 不过FMDatabaseQueue存在容易导致死锁的bug(inDatabase嵌套调用), 为了解决这个问题, 我稍微改造了一下, 换成了ALFMDatabaseQueue(代码在这里),并且已经经过实际项目检验。

// 判断是否嵌套inDatabase调用
- (void)safelyRun:(void (^)(void))block {
    ALFMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
    if (currentSyncQueue == self) {
        ALLogWarn(@"!!! Nested database operation blocks!");
        OP_BLOCK(block);
    } else {
        dispatch_sync(_queue, ^{
            OP_BLOCK(block);
        });
    }
}


- (void)inDatabase:(void (^)(FMDatabase *db))block {
    FMDBRetain(self);
    
    [self safelyRun:^{
        FMDatabase *db = [self database];
        block(db);
        
        if ([db hasOpenResultSets]) {
            ALLogWarn(@"!!! there is at least one open result set around after performing [ALFMDatabaseQueue inDatabase:]");
            
#if defined(DEBUG) && DEBUG
            NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]);
            for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
                FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
                ALLogWarn(@"unexpected opening result set query: '%@'", [rs query]);
            }
#endif
        }
    }];
    
    FMDBRelease(self);
}

//
// beginTransaction 等方法类似
//

ALDatabase

  • 管理数据库连接
  • 数据迁移管理
extern NSString * const kALInMemoryDBPath;  // in-memory db
extern NSString * const kALTempDBPath;      // temp db;

@interface ALDatabase : NSObject

@property(readonly) ALFMDatabaseQueue *queue;
@property(readonly, getter=isReadonly) BOOL               readonly; // Is databas opened in readonly mode?

// The following methods open a database with specified path,
// @see: http://www.sqlite.org/c3ref/open.html

// database opened in default mode.
+ (nullable instancetype)databaseWithPath:(NSString *)path;

// database opened in readonly mode. -- experimental
+ (nullable instancetype)readonlyDatabaseWithPath:(NSString *)path;

// database opened in readonly mode, and bind to caller's thread local -- experimental
+ (nullable instancetype)threadLocalReadonlyDatabaseWithPath:(NSString *)path;

- (void)close;

@end

ALDatabase 支持在同一个App中打开多个数据库,也支持同一个数据库多个连接(1写N读, 未经正式项目验证)。
Example:

    NSString *dbpath = testDBPath();
    [[NSFileManager defaultManager] removeItemAtPath:dbpath error:nil];
    
    // Create and open a default Database connection (Readable & writeable)
    ALDatabase *db = [ALDatabase databaseWithPath:dbpath];
    BOOL ret = db.INSERT().INTO(@"users").VALUES_DICT(@{@"name": @"Alex Lee", @"age": @36}).EXECUTE_UPDATE();
    XCTAssertTrue(ret);
    [db close];
    
    // Set database file to READONLY
    [[NSFileManager defaultManager] setAttributes:@{ NSFilePosixPermissions: [NSNumber numberWithShort:0444] } ofItemAtPath:dbpath error:nil];
    db = [ALDatabase databaseWithPath:dbpath];
    XCTAssertNotNil(db); // database opened 
    
    ret = db.INSERT().INTO(@"users").VALUES_DICT(@{@"name": @"Alex Lee", @"age": @36}).EXECUTE_UPDATE();
    XCTAssertFalse(ret); // database is in readonly model
    
    
    db = [ALDatabase readonlyDatabaseWithPath:dbpath];
    ALLogInfo(@"readonly DB: %@", db);
    XCTAssertNotNil(db); // open shared readonly connection
    
    
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // open shared readonly connection  
        XCTAssertEqualObjects(db, [ALDatabase readonlyDatabaseWithPath:dbpath]);
        
        // open thread local readonly connection  
        XCTAssertNotEqualObjects(db, [ALDatabase threadLocalReadonlyDatabaseWithPath:dbpath]);
    });

ALDBConnectionProtocol

@protocol ALDBConnectionProtocol 

+ (BOOL)canHandleDatabaseWithPath:(NSString *)path;

@optional
- (void)databaseDidOpen:(FMDatabase *)db;
- (void)databaseDidReady:(FMDatabase *)db;

- (void)databaseWillClose:(FMDatabase *)db;
- (void)databaseWithPathDidClose:(NSString *)dbpath;
@end

可以在数据库打开/关闭的时候进行额外处理。

Examples:

@interface TestDBOpenHelper : NSObject 
@end

@implementation TestDBOpenHelper

+ (BOOL)canHandleDatabaseWithPath:(NSString *)path {
    return [path isEqualToString:testDBPath()];
}
- (void)databaseDidOpen:(FMDatabase *)db {
    [db executeUpdate:@"PRAGMA journal_mode=WAL;"];
}
@end

ALDBMigrationProtocol

@protocol ALDBMigrationProtocol 

+ (BOOL)canMigrateDatabaseWithPath:(NSString *)path;
- (NSInteger)currentVersion;

- (BOOL)migrateFromVersion:(NSInteger)fromVersion to:(NSInteger)toVersion databaseHandler:(FMDatabase *)db;

@optional
- (BOOL)setupDatabase:(FMDatabase *)db;

@end

在数据库打开的时候, 执行指定的数据迁移逻辑。

另外,ALDatabase也实现了自动数据迁移, 完成数据表(Table), 索引(Index)等结构的自动更新:

- (BOOL)migrateDatabase:(FMDatabase *)db {
    id migrationProcessor = [self dbMigrationProcessor];
    
    if (migrationProcessor == nil) {
        _ALDBLog(@"Not found database migration processor, try auto-migration. database path: %@", _queue.path);
        
        if (!_dbFileExisted) {
            [ALDBMigrationHelper setupDatabase:db];
        } else {
            [ALDBMigrationHelper autoMigrateDatabase:db];
        }
        return YES;
    } else {
        
        // all the database version should begins from 1 (DO NOT begins from 0 !!!)
        NSInteger newVersion = [migrationProcessor currentVersion];
        if (newVersion < 1) {
            NSAssert(NO, @"*** Database version must be >= 1, but was %d", (int)newVersion);
            return NO;
        }
        
        if (!_dbFileExisted) { // create database directly
            BOOL created = NO;
            if ([migrationProcessor respondsToSelector:@selector(setupDatabase:)]) { // manually setup database
                created = [migrationProcessor setupDatabase:db];
            } else {
                [ALDBMigrationHelper setupDatabase:db];
                created = YES;
            }
            
            if (created) {
                return [self updateDatabaseVersion:newVersion dbHandler:db];
            } else {
                NSAssert(NO, @"Can not setup database: %@", _queue.path);
                return NO;
            }
        } else {
            NSInteger dbVersion = [db intForQuery:@"PRAGMA user_version;"];
            
            if (dbVersion < newVersion) {
                if ([migrationProcessor migrateFromVersion:dbVersion to:newVersion databaseHandler:db]) {
                    return [self updateDatabaseVersion:newVersion dbHandler:db];
                } else {
                    NSAssert(NO, @"migrate from version %@ to %@ failed!!! database: %@", @(dbVersion),
                             @(newVersion), _queue.path);
                    return NO;
                }
            } else if (dbVersion > newVersion) {
                NSAssert(NO, @"Illegal database version. original:%@, new version:%@",
                         @(dbVersion), @(newVersion));
                return NO;
            }
        }
    }
    return YES;
}

ALSQLClause

一个数据结构, 用来表示一个SQL语句或者SQL语句的一部分, 并且实现了SQL语句的组合。

Examples:

    // sql function with arguments
    sql = sqlFunc(@"substr", @"column_name", @5, nil);
    XCTAssertEqualObjects(sql.SQLString, @"SUBSTR(column_name, 5)");
    XCTAssertEqualObjects(sqlFunc(@"replace", @"col_name", [@5 SQLClauseArgValue], [@3 SQLClauseArgValue], nil).SQLString,
                          @"REPLACE(col_name, ?, ?)");

    sql = @"col1".NEQ(@1).OR(@"col1".NEQ(@2)).AND(@"col2".EQ(@0).OR(@"col2".GT(@100)));
    XCTAssertEqualObjects(sql.SQLString, @"(col1 != ? OR col1 != ?) AND (col2 = ? OR col2 > ?)");

    XCTAssertEqualObjects((@"col1".IN(@[@1, @2, @3])).SQLString, @"col1 IN (?, ?, ?)");

    ALSQLClause *sql = [@"" SQLClause]
                           .CASE(@"col1")
                           .WHEN(@0)
                           .THEN([@"zero" SQLClauseArgValue])
                           .WHEN(@1)
                           .THEN([@"one" SQLClauseArgValue])
                           .ELSE([@"others" SQLClauseArgValue])
                           .END();
    XCTAssertEqualObjects(sql.SQLString, @"CASE col1 WHEN ? THEN ? WHEN ? THEN ? ELSE ? END");

ALSQLStatement

跟ALSQLClause类似, 但是ALSQLStatement是一段可以执行的SQL语句。

Examples:

  • Select Statements

    {
        // multi-table query
        stmt = [ALSQLSelectStatement statementWithDatabase:self.db];
        stmt.SELECT(nil)
            .FROM(@[ @"students", @"student_courses" ])
            .WHERE([@"students._id = student_courses._id" SQLClause]);
        XCTAssertEqualObjects(stmt.SQLString, @"SELECT * FROM students, student_courses WHERE students._id = student_courses._id");
        XCTAssertTrue([stmt validateWitherror:nil]);
    }

    {
        // SubQuery
        stmt    = [ALSQLSelectStatement statementWithDatabase:self.db];
        subStmt = [ALSQLSelectStatement statementWithDatabase:self.db];
        stmt.SELECT(SQL_COUNT(@"*"))
            .FROM(subStmt.SELECT(@[ @"name", @"age" ]).FROM(@"students").WHERE(@"name".PREFIX_LIKE(@"alex")))
            .WHERE(@"age".GT(@(30)));
        ALLogInfo(@"%@", stmt.SQLString);
        XCTAssertEqualObjects(stmt.SQLString,
                              @"SELECT COUNT(*) FROM (SELECT name, age FROM students WHERE name LIKE ?) WHERE age > ?");
    }

    {
        // SubQuery (add alias)
        stmt    = [ALSQLSelectStatement statementWithDatabase:self.db];
        subStmt = [ALSQLSelectStatement statementWithDatabase:self.db];
        stmt.SELECT(SQL_COUNT(@"*"))
            .FROM(subStmt.SELECT(@[ @"name", @"age" ])
                      .FROM(@"students")
                      .WHERE(@"name".PREFIX_LIKE(@"alex"))
                      .AS(@"sub_tbl"))
            .WHERE(@"age".GT(@(30)));
        ALLogInfo(@"%@", stmt.SQLString);
        XCTAssertEqualObjects(
            stmt.SQLString,
            @"SELECT COUNT(*) FROM (SELECT name, age FROM students WHERE name LIKE ?) AS sub_tbl WHERE age > ?");
    }

    {
        // complete select-core statement
        stmt = [ALSQLSelectStatement statementWithDatabase:self.db];
        stmt.SELECT(@[ @"COUNT(*) AS num", SQL_UPPER(@"province").AS(@"province") ])
            .FROM(@"students")
            .WHERE(SQL_LOWER(@"gender").EQ(@"1"))
            .GROUP_BY(@"province")
            .HAVING(@"age".GT(@18))
            .ORDER_BY(@"num".DESC())
            .ORDER_BY(@"province")
            .LIMIT(@5)
            .OFFSET(@3);
        XCTAssertEqualObjects(stmt.SQLString, @"SELECT COUNT(*) AS num, UPPER(province) AS province FROM students "
                                              @"WHERE LOWER(gender) = ? GROUP BY province HAVING age > ? ORDER BY num "
                                              @"DESC, province LIMIT 5 OFFSET 3");
        XCTAssertTrue([stmt validateWitherror:nil]);
    }
  • Insert Setatements
    {
        // insert using values, we can repeat calling 'VALUES' multiple times to insert multiple rows
        NSArray *u1 = @[ @"alex", @30, @"1", @"BJ/CN" ];
        NSArray *u2 = @[ @"Jim", @"18", @1, @"SF/US" ];
        stmt        = [ALSQLInsertStatement statementWithDatabase:self.db];
        stmt.INSERT()
            .OR_REPLACE(YES)
            .INTO(@"students")
            .COLUMNS(@[ @"name", @"age", @"gender", @"address" ])
            .VALUES(u1)
            .VALUES(u2);
        XCTAssertEqualObjects(
            stmt.SQLString,
            @"INSERT OR REPLACE INTO students (name, age, gender, address) VALUES (?, ?, ?, ?), (?, ?, ?, ?)");
        values = [u1 arrayByAddingObjectsFromArray:u2];
        XCTAssertEqualObjects(stmt.argValues, values);
        XCTAssertTrue([stmt validateWitherror:nil]);
    }

    {
        // insert using values dictionary: insert only a row
        stmt               = [ALSQLInsertStatement statementWithDatabase:self.db];
        NSDictionary *dict = @{ @"name" : @"Roger", @"age" : @34, @"gender" : @"1", @"address" : @"AB/CA" };
        stmt.INSERT().INTO(@"students").VALUES_DICT(dict);
        NSArray *keys = dict.allKeys;
        NSString *sql = [NSString
            stringWithFormat:@"INSERT INTO students (%@) VALUES (?, ?, ?, ?)", [keys componentsJoinedByString:@", "]];
        values = [dict objectsForKeys:keys notFoundMarker:NSNull.null];
        XCTAssertEqualObjects(stmt.SQLString, sql);
        XCTAssertEqualObjects(stmt.argValues, values);
        XCTAssertTrue([stmt validateWitherror:nil]);
    }

    {
        // insert using selection results
        stmt = [ALSQLInsertStatement statementWithDatabase:self.db];
        stmt.INSERT()
            .INTO(@"students")
            .SELECT_STMT([ALSQLSelectStatement statementWithDatabase:self.db]
                             .SELECT(nil)
                             .FROM(@"students")
                             .WHERE(@"age".NEQ(@0))
                             .SQLClause);
        XCTAssertEqualObjects(stmt.SQLString, @"INSERT INTO students SELECT * FROM students WHERE age != ?");
        values = @[ @0 ];
        XCTAssertEqualObjects(stmt.argValues, values);
        XCTAssertTrue([stmt validateWitherror:nil]);
    }
  • Update Statements
    ALSQLUpdateStatement *stmt = [ALSQLUpdateStatement statementWithDatabase:self.db];
    stmt.UPDATE(@"students")
        .OR_REPLACE(YES)
        .SET(@{@"age": @30})    // NSDictionary
        .SET(@"gender".EQ(@"2"))   // ALSQLClause
        .SET(@[@"name".EQ(@"sindy"), @"address".EQ(@"AB/CA")]) // NSArray
        .WHERE(@"name".EQ(@"Roger"));
    XCTAssertEqualObjects(stmt.SQLString, @"UPDATE OR REPLACE students SET age = ?, gender = ?, name = ?, address = ? WHERE name = ?");
    NSArray *values = @[@30, @"2", @"sindy", @"AB/CA", @"Roger"];
    XCTAssertEqualObjects(stmt.argValues, values);
    XCTAssertTrue([stmt validateWitherror:nil]);
  • Delete Statements
    ALSQLDeleteStatement *stmt = [ALSQLDeleteStatement statementWithDatabase:self.db];
    stmt.DELETE().FROM(@"students").WHERE(@1);
    XCTAssertEqualObjects(stmt.SQLString, @"DELETE FROM students WHERE 1");
    XCTAssertTrue([stmt validateWitherror:nil]);
    
    stmt = [ALSQLDeleteStatement statementWithDatabase:self.db];
    stmt.DELETE().FROM(@"students").WHERE(@"name".SUBFIX_LIKE(@"lee")).ORDER_BY(@"age".DESC()).LIMIT(@5);
    XCTAssertEqualObjects(stmt.SQLString, @"DELETE FROM students WHERE name LIKE ? ORDER BY age DESC LIMIT 5");
    XCTAssertTrue([stmt validateWitherror:nil]);

Active Record

自从ROR引入了Active Record模式以后, 这种模式变得越来越流行。

什么是Active Record:

  • 一个数据模型(Model)对应数据库中的一张表(Table)。
  • 一个Model的实例(Instance), 对应该表的一条记录(Record)。
  • 通过对Model的操作来进行数据库的更新。

Patchwork 如何实现Active Record:

创建自己的model, 继承于ALModel, 并且实现以下方法, patchwork将会自动帮你创建数据库。

#pragma mark - table mappings (override by subclasses)
@interface ALModel (ActiveRecord_Protected)

/**
 * @return The name of database table that associates with this model.  
 * Normally, the model name should be a noun of English. so the default value return would be the pluralize of model name.
 * a) If the model name ends with "Model", the subfix "Model" will be removed in the table name.
 * b) If the model name is not ends with English letter, the subfix "_list" will be added to table name.
 * c) If the model name is CamelCase style, the table name will be converted to lowercase words and joined with "_".
 *
 * eg: "UserModel" => "users", "fileMeta" => "file_metas".
 */
+ (nullable NSString *)tableName;

/**
 *  @return The database identifier (normally the database file path) that associates with this model.
 *  Return nil if the model doesn't bind to any database.
 */
+ (nullable NSString *)databaseIdentifier;

/**
 *  All properties in blacklist would not be mapped to the database table column.
 *  return nil to ignore this feature.
 *
 *  @return an Array of property name, or nil;
 */
+ (nullable NSArray *)recordPropertyBlacklist;

/**
 *  Only properties in whitelist would be mapped to the database table column.
 *  The Order of table columns is the same as the order of whitelist.
 *
 *  return nil to ignore this feature.
 *
 *  @return an Array of property name, or nil;
 */
+ (nullable NSArray *)recordPropertyWhitelist;

// @{propertyName: columnName}
+ (nullable NSDictionary  *)modelCustomColumnNameMapper;

/**
 *  The comparator to sort the table columns
 *  The default order is:
 *      if "-recordPropertyWhitelist" is not nil, using the same order of properties in whitelist.
 *      else the order should be: "primary key columns; unique columns; index columns; other columns"
 *
 *  @return typedef NSComparisonResult (^NSComparator)(ALDBColumnInfo *_Nonnull col1, ALDBColumnInfo *_Nonnull col2)
 */
+ (NSComparator)columnOrderComparator;

+ (void)customColumnDefine:(ALDBColumnInfo *)cloumn forProperty:(in YYClassPropertyInfo *)property;

/**
 *  Custom transform property value to save to database
 *
 *  @return value to save to database
 */
//- (id)customColumnValueTransformFrom{PropertyName};

/**
 *  Custom transform property value from resultSet
 *  @see "+modelsWithCondition:"
 */
//- (void)customTransform{PropertyName}FromRecord:(in FMResultSet *)rs columnIndex:(int)index;

/**
 * key: the property name
 * specified the model's primary key, if it's not set and '+withoudRowId' returns NO,  'rowid' is set as default.
 * If the model cantains only one primary key, and the primary key is type of "NSInteger", please use 'rowid' property directly.
 *
 * @see http://www.sqlite.org/rowidtable.html
 * @see http://www.sqlite.org/withoutrowid.html
 * @see "rowid" property
 */
+ (nullable NSArray            *)primaryKeys;
+ (nullable NSArray *> *)uniqueKeys;
+ (nullable NSArray *> *)indexKeys;

// default is NO, if return YES, prmaryKeys must be set.
+ (BOOL)withoutRowId;

@end
  • 绑定数据库:+ (nullable NSString *)databaseIdentifier;, 返回对应数据库的文件路径, 返回nil则表明model不跟任何数据库关联。注意:即使父类已经绑定了数据库, 子类如果需要跟数据库绑定, 仍然需要override这个方法。

  • 自定义表名(可选):+ (nullable NSString *)tableName;

  • 自定义属性名和字段名映射(可选):+ (nullable NSDictionary *)modelCustomColumnNameMapper;

  • 指定绑定/禁止绑定到数据表的属性(可选):

    • 指定白名单:+ (nullable NSArray *)recordPropertyWhitelist;
    • 指定黑名单 + (nullable NSArray *)recordPropertyBlacklist;
  • 自定义属性值转换(可选):

    • 属性值转换到数据库字段值:- (id)customColumnValueTransformFrom{PropertyName};, {PropertyName}是具体属性的名称, 首字母大写, 其余保持原样。
    • 数据库值转换到属性值:- (void)customTransform{PropertyName}FromRecord:(in FMResultSet *)rs columnIndex:(int)index, {PropertyName}同上。
  • 指定索引字段(可选):

    • primary key:+ (nullable NSArray *)primaryKeys;, 一般情况下不建议指定(sqlite有默认的rowid主键), 如果有必要自己指定主键字段, 并且该字段是整型, 则需要在model的.m中用宏SYNTHESIZE_ROWID_ALIAS标注一下(详见测试用例)。
    • unique keys: + (nullable NSArray *> *)uniqueKeys, 一个model可以有多个unique key, 每个unique key可以包含多个字段。
    • 其他indexs: + (nullable NSArray *> *)indexKeys

想了解更多有关patchwork如何自动实现创建DB的细节, 请参考ALDatabase 和 ALDBMigrationHelper。

如何使用:

"Talk is cheap, show me the code"

Example:

// 创建 model
@interface Student : ALModel
@property(nonatomic, assign)    NSInteger    sid;
@property(nonatomic, copy)      NSString    *name;
@property(nonatomic, strong)    NSNumber    *age;
@property(nonatomic, assign)    NSInteger    gender;
@property(nonatomic, copy)      NSString    *province;
@property(nonatomic, strong)    NSDate      *birthday;
@property(nonatomic, strong)    UIImage     *icon;

@end

@implementation Student

+ (NSString *)databaseIdentifier {
    return kTmpDBPath1;
}

// It's better use 'rowid' instead of defining a property named 'sid' as primary key.
// If you DO want to do that, you need to make it as alias of 'rowid' and use it as primary key.
SYNTHESIZE_ROWID_ALIAS(sid); 

+ (NSArray *> *)uniqueKeys {
    return @[ @[ keypathForClass(Student, name) ] ]; // just for test
}
@end

#pragma mark - 如何使用active record
    Student *student = [[Student alloc] init];
    student.name = @"Alex Lee";
    student.age  = @(19);
    student.gender   = 1;
    student.province = @"GD/HS";
    student.birthday = [NSDate date];
    student.icon = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://baidu.com/favicon.ico"]]];
    //插入一条记录, 返回model的rowid,  rowid > 0 表示成功
    XCTAssertGreaterThan([student saveOrReplce:YES], 0);
    XCTAssertEqual([Student fetcher].FETCH_COUNT(nil), 1);
    
    student.age = @(student.age.integerValue + 1);
    // 更新记录
    [student updateOrReplace:YES];
    XCTAssertEqual([[Student modelWithId:student.rowid] age].integerValue, 20);
    
    // 删除记录
    [student deleteRecord];
    XCTAssertEqual([Student fetcher].FETCH_COUNT(nil), 0);
    
    // 指定条件查询
    student = [Student modelsWithCondition:AS_COL(Student, name)
                                               .PREFIX_LIKE(@"Alex")
                                               .AND(AS_COL(Student, age).NLT(@10))
                                               .AND(AS_COL(Student, age).LT(@20))
                                               .AND(AS_COL(Student, gender).EQ(@1))]
                  .firstObject;
    
    // 复杂一点的查询
    student = [Student fetcher]
                  .WHERE(AS_COL(Student, name).PREFIX_LIKE(@"Alex"))
                  .ORDER_BY(AS_COL(Student, age).DESC())
                  .LIMIT(@1)
                  .OFFSET(@5)
                  .FETCH_MODELS()
                  .firstObject;

    //更新指定属性
    [student updateProperties:@[keypath(student.age), keypath(student.province)] repleace:NO];

    //更新符合条件的记录
    [Student updateProperties:@{
        keypathForClass(Student, age) : @20,
        keypathForClass(Student, gender) : @1
    }
                withCondition:AS_COL(Student, birthday).EQ([NSDate dateWithTimeIntervalSinceNow:-20 * 365 * 86400])
                     repleace:NO];

欢迎拍砖。

你可能感兴趣的:(MacOS / iOS 下数据库使用及 ActiveRecord 的实现)