MacOS / iOS 下數(shù)據(jù)庫(kù)使用及 ActiveRecord 的實(shí)現(xiàn)

在開發(fā)iOS / macOS 應(yīng)用中豺鼻, 經(jīng)常會(huì)使用數(shù)據(jù)庫(kù)(sqlite)來保存數(shù)據(jù)谆焊, Apple也提供了一個(gè)龐大的神器--CoreData, 不過個(gè)人感覺這個(gè)神器跟Java一樣,復(fù)雜的事情并沒有變的簡(jiǎn)單, 簡(jiǎn)單的事情反而變得不簡(jiǎn)單弛作,而且對(duì)開發(fā)者屏蔽了太多的細(xì)節(jié)涕蜂, 因此在一些稍微大一點(diǎn)的應(yīng)用中华匾, 對(duì)于使用CoreData都顯得頗為謹(jǐn)慎。

另外一個(gè)神器就是FMDB机隙, 通過對(duì)sqlite接口的輕量級(jí)封裝蜘拉, 給開發(fā)者提供了很方便的接口。 但是FMDB僅僅只是sqlite的包裝有鹿, 并沒有給上層提供更好的數(shù)據(jù)管理支持(其實(shí)這也是優(yōu)勢(shì)旭旭, 很好的體現(xiàn)了單一職責(zé)的思想), 在開發(fā)App的時(shí)候葱跋, 還需要自己實(shí)現(xiàn)數(shù)據(jù)庫(kù)管理邏輯持寄。

數(shù)據(jù)庫(kù)管理層的實(shí)現(xiàn), 就各顯神通了娱俺, 大牛小牛都根據(jù)自己的理解或者結(jié)合自己的業(yè)務(wù)造出自己的輪子稍味, 好不好用, 效果如何只有自己知道荠卷。 開源的各種輪子和介紹也很多模庐, 這里就不細(xì)說了。

好的輪子是什么樣子的

個(gè)人愚見油宜, 一個(gè)好用的數(shù)據(jù)管理庫(kù)掂碱, 應(yīng)該提供以下功能:

  • 支持連接多個(gè)數(shù)據(jù)庫(kù) 這個(gè)很好理解, 一般應(yīng)用慎冤, 都有多個(gè)模塊疼燥, 每個(gè)模塊之間, DB最好獨(dú)立管理蚁堤。 另外悴了, 不同賬戶之間, 也最好分開不同的DB违寿。
  • 支持?jǐn)?shù)據(jù)遷移 隨著應(yīng)用版本不停的迭代湃交, 數(shù)據(jù)庫(kù)字段/字段值可能也會(huì)發(fā)生變化, 這個(gè)時(shí)候就需要有一個(gè)數(shù)據(jù)遷移機(jī)制藤巢。
  • 友好的數(shù)據(jù)操作接口 雖然SQL對(duì)于程序員來說搞莺,不應(yīng)該成為一個(gè)門檻, 但是在APP中手工拼接SQL字符串掂咒,并不是一個(gè)令人愉快的開發(fā)體驗(yàn)才沧。
  • 自動(dòng)的 Model & DB mapping 作為一個(gè)總想著偷懶的程序員迈喉, 手寫那么一大堆的賦值取值操作, 我是忍受不了的温圆, 而且這種令人厭煩的體力活也容易出錯(cuò)(好吧挨摸, 我在煩躁的時(shí)候是無法仔細(xì)去檢查這些無聊的代碼的)。

老實(shí)說岁歉, 提供這些功能的輪子github上有很多 得运, 我也嘗試了很多, 比如 iActiveRecord, FCModel, ObjectiveRecord, FMDB-ActiveRecord等等锅移, 這些框架各自都有亮點(diǎn)熔掺, 但是總是沒有提供以上全部特性, 或者實(shí)現(xiàn)的并不是特別好非剃, 因此我決定自己造一個(gè)輪子(去年給自己挖的一個(gè)大坑置逻, 不過捋清思路之后, 很快就做出原型來了备绽, 之后用它完成了公司一個(gè)項(xiàng)目券坞,另外的幾個(gè)項(xiàng)目也在遷移中),暫時(shí)沒想到高大上的名字肺素, 就先叫patchwork, 意思是這個(gè)庫(kù)是把一些小工具拼起來恨锚, 組成一個(gè)快速開發(fā)的基礎(chǔ)工具庫(kù)。這個(gè)庫(kù)目前主要提供數(shù)據(jù)管理压怠,網(wǎng)絡(luò)中間層眠冈,model轉(zhuǎn)換等,還有一些常用的小工具菌瘫。 代碼見GitHub蜗顽。

Patchwork的實(shí)現(xiàn)

Patchwork的數(shù)據(jù)庫(kù)層結(jié)構(gòu)如下:

ALDB structs.png

FMDB

這個(gè)輪子就沒必要再自己造了。 不過FMDatabaseQueue存在容易導(dǎo)致死鎖的bug(inDatabase嵌套調(diào)用), 為了解決這個(gè)問題雨让, 我稍微改造了一下雇盖, 換成了ALFMDatabaseQueue(代碼在這里),并且已經(jīng)經(jīng)過實(shí)際項(xiàng)目檢驗(yàn)栖忠。

// 判斷是否嵌套inDatabase調(diào)用
- (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

  • 管理數(shù)據(jù)庫(kù)連接
  • 數(shù)據(jù)遷移管理
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 支持在同一個(gè)App中打開多個(gè)數(shù)據(jù)庫(kù)崔挖,也支持同一個(gè)數(shù)據(jù)庫(kù)多個(gè)連接(1寫N讀, 未經(jīng)正式項(xiàng)目驗(yà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 <NSObject>

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

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

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

可以在數(shù)據(jù)庫(kù)打開/關(guān)閉的時(shí)候進(jìn)行額外處理狸相。

Examples:

@interface TestDBOpenHelper : NSObject <ALDBConnectionProtocol>
@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 <NSObject>

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

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

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

@end

在數(shù)據(jù)庫(kù)打開的時(shí)候, 執(zhí)行指定的數(shù)據(jù)遷移邏輯捐川。

另外脓鹃,ALDatabase也實(shí)現(xiàn)了自動(dòng)數(shù)據(jù)遷移, 完成數(shù)據(jù)表(Table), 索引(Index)等結(jié)構(gòu)的自動(dòng)更新:

- (BOOL)migrateDatabase:(FMDatabase *)db {
    id<ALDBMigrationProtocol> 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

一個(gè)數(shù)據(jù)結(jié)構(gòu)古沥, 用來表示一個(gè)SQL語句或者SQL語句的一部分瘸右, 并且實(shí)現(xiàn)了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是一段可以執(zhí)行的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<ALSQLClause *>
        .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:

  • 一個(gè)數(shù)據(jù)模型(Model)對(duì)應(yīng)數(shù)據(jù)庫(kù)中的一張表(Table)龄章。
  • 一個(gè)Model的實(shí)例(Instance)吃谣, 對(duì)應(yīng)該表的一條記錄(Record)。
  • 通過對(duì)Model的操作來進(jìn)行數(shù)據(jù)庫(kù)的更新瓦堵。

Patchwork 如何實(shí)現(xiàn)Active Record:

創(chuàng)建自己的model, 繼承于ALModel, 并且實(shí)現(xiàn)以下方法基协, patchwork將會(huì)自動(dòng)幫你創(chuàng)建數(shù)據(jù)庫(kù)歌亲。

#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<NSString *> *)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<NSString *> *)recordPropertyWhitelist;

// @{propertyName: columnName}
+ (nullable NSDictionary<NSString *, NSString *>  *)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<NSString *>            *)primaryKeys;
+ (nullable NSArray<NSArray<NSString *> *> *)uniqueKeys;
+ (nullable NSArray<NSArray<NSString *> *> *)indexKeys;

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

@end
  • 綁定數(shù)據(jù)庫(kù):+ (nullable NSString *)databaseIdentifier;菇用, 返回對(duì)應(yīng)數(shù)據(jù)庫(kù)的文件路徑, 返回nil則表明model不跟任何數(shù)據(jù)庫(kù)關(guān)聯(lián)陷揪。注意:即使父類已經(jīng)綁定了數(shù)據(jù)庫(kù)惋鸥, 子類如果需要跟數(shù)據(jù)庫(kù)綁定, 仍然需要override這個(gè)方法悍缠。

  • 自定義表名(可選):+ (nullable NSString *)tableName;

  • 自定義屬性名和字段名映射(可選):+ (nullable NSDictionary<NSString *, NSString *> *)modelCustomColumnNameMapper;

  • 指定綁定/禁止綁定到數(shù)據(jù)表的屬性(可選):

    • 指定白名單:+ (nullable NSArray<NSString *> *)recordPropertyWhitelist;
    • 指定黑名單 + (nullable NSArray<NSString *> *)recordPropertyBlacklist;
  • 自定義屬性值轉(zhuǎn)換(可選):

    • 屬性值轉(zhuǎn)換到數(shù)據(jù)庫(kù)字段值:- (id)customColumnValueTransformFrom{PropertyName};, {PropertyName}是具體屬性的名稱卦绣, 首字母大寫, 其余保持原樣聂薪。
    • 數(shù)據(jù)庫(kù)值轉(zhuǎn)換到屬性值:- (void)customTransform{PropertyName}FromRecord:(in FMResultSet *)rs columnIndex:(int)index, {PropertyName}同上桩盲。
  • 指定索引字段(可選):

    • primary key:+ (nullable NSArray<NSString *> *)primaryKeys;灾前, 一般情況下不建議指定(sqlite有默認(rèn)的rowid主鍵), 如果有必要自己指定主鍵字段溅漾, 并且該字段是整型, 則需要在model的.m中用宏SYNTHESIZE_ROWID_ALIAS標(biāo)注一下(詳見測(cè)試用例)添履。
    • unique keys: + (nullable NSArray<NSArray<NSString *> *> *)uniqueKeys, 一個(gè)model可以有多個(gè)unique key, 每個(gè)unique key可以包含多個(gè)字段暮胧。
    • 其他indexs: + (nullable NSArray<NSArray<NSString *> *> *)indexKeys

想了解更多有關(guān)patchwork如何自動(dòng)實(shí)現(xiàn)創(chuàng)建DB的細(xì)節(jié), 請(qǐng)參考ALDatabaseALDBMigrationHelper席舍。

如何使用:

"Talk is cheap, show me the code"

Example:

// 創(chuàng)建 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<NSArray<NSString *> *> *)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;
    
    // 復(fù)雜一點(diǎn)的查詢
    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];

歡迎拍磚。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末珊泳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子秧了,更是在濱河造成了極大的恐慌验毡,老刑警劉巖晶通,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異喉脖,居然都是意外死亡动看,警方通過查閱死者的電腦和手機(jī)菱皆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篷店,“玉大人疲陕,你說我怎么就攤上這事蹄殃∽缪遥” “怎么了吩谦?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵式廷,是天一觀的道長(zhǎng)草描。 經(jīng)常有香客問我策严,道長(zhǎng)妻导,這世上最難降的妖魔是什么倔韭? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮醇疼,結(jié)果婚禮上秧荆,老公的妹妹穿的比我還像新娘乙濒。我一直安慰自己么库,他們只是感情好廊散,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缭受,像睡著了一般米者。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上喂分,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音梆掸,去河邊找鬼。 笑死卑硫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的颜懊。 我是一名探鬼主播匠璧,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了指黎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后钾腺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姻报,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年笆焰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捏检。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出坦胶,到底是詐尸還是另有隱情税弃,我是刑警寧澤幔翰,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布做修,位于F島的核電站饰及,受9級(jí)特大地震影響腿短,放射性物質(zhì)發(fā)生泄漏铣除。R本人自食惡果不足惜郎嫁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一杠茬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舀透,春花似錦栓票、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惑芭,卻和暖如春坠狡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背强衡。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國(guó)打工擦秽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人漩勤。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像缩搅,于是被迫代替她去往敵國(guó)和親越败。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容