在開發(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)如下:
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}
同上桩盲。
- 屬性值轉(zhuǎn)換到數(shù)據(jù)庫(kù)字段值:
-
指定索引字段(可選):
- 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
- primary key:
想了解更多有關(guān)patchwork
如何自動(dòng)實(shí)現(xiàn)創(chuàng)建DB的細(xì)節(jié), 請(qǐng)參考ALDatabase
和 ALDBMigrationHelper席舍。
如何使用:
"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];
歡迎拍磚。