在开发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
的数据库层结构如下:
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
, 一般情况下不建议指定(sqlite有默认的rowid主键), 如果有必要自己指定主键字段, 并且该字段是*)primaryKeys; 整型
, 则需要在model的.m中用宏SYNTHESIZE_ROWID_ALIAS
标注一下(详见测试用例)。 - unique keys:
+ (nullable NSArray
, 一个model可以有多个unique key, 每个unique key可以包含多个字段。*> *)uniqueKeys - 其他indexs:
+ (nullable NSArray
*> *)indexKeys
- primary key:
想了解更多有关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];
欢迎拍砖。