适用于喜欢直接SQL访问的人的Core Data的替代方案。
由Marco Arment编写。有关许可信息,请查看LICENSE文件(它是MIT许可)。
FCModel是在SQLite之上通用模型层的。它是为那些想要一些Core Data的便利性,但又想要更多对实现、性能、数据库模式、查询、索引和迁移的控制,以及直接使用原始SQL查询和SQLite功能的能力的人设计的。
FCModel实现了Brent Simmons在关于SQLite而不是Core Data的撰写的很多内容。这是我自己的版本。(你在阅读objc.io吗?你应该会。它很棒。)
这是一个测试版。我正在用它来构建一个应用程序,一些人正在使用它并做出贡献,到目前为止它对我们来说已经稳定。但变更仍然相对频繁,API可能仍然会以轻微但不兼容的方式改变。
NSMapTable
)sqlite3
(如果您使用CocoaPods则会自动设置)目前还不太多。请查看FCModel.h
头文件和示例项目。我将有机会在这里添加更多内容。
SQLite表与同名的FCModel子类相关联,数据库名称映射到具有相同名称的@property
声明。因此,您可以有一个这样的表
CREATE TABLE Person (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
createdTime REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS name ON Person (name);
需要一个单列主键。它可以是一个整数或一个字符串。如果您在创建对象时未指定键值,FCModel将生成一个随机的64位有符号整数键,它在表内是唯一的。您负责创建自己的表索引。
该表的模式可能看起来像这样
#import "FCModel.h"
@interface Person : FCModel
@property (nonatomic) int64_t id;
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSDate *createdTime;
@end
数据库映射的对象属性可以是
int
、double
、BOOL
、int64_t
等)或 NSNumber
,限于 SQLite的精度(整数64位有符号)
NSString
,始终以UTF-8存储和加载NSData
用于BLOB
列NSDate
,从1970年转换到/从 NSTimeInterval
(Unix时间戳的double
有符号版本)进行存储。在表中声明NSDate
列时,请声明为REAL
。NSURL
,在存储时将其转换为/从中转换为其absoluteString
表示形式。NSDictionary
或NSArray
,在存储时转换为/从binary plists转换为存储(因此每个内部对象都必须是NSData
、NSString
、NSArray
、NSDictionary
、NSDate
或NSNumber
)。要覆盖此行为或对其他类型进行自定义,模型可以覆盖下面的方法。数据库值可以是INTEGER
或FLOAT
或TEXT
列的NSString
或NSNumber
,或者是用于BLOB
列的NSData
。对于允许NULL
的列,这些方法可能接收或返回nil
。覆盖必须调用super
实现以转换未处理值的值。
- (id)serializedDatabaseRepresentationOfValue:(id)instanceValue forPropertyNamed:(NSString *)propertyName;
- (id)unserializedRepresentationOfDatabaseValue:(id)databaseValue forPropertyNamed:(NSString *)propertyName;
您可以将列属性的实例变量重命名为任何您喜欢的名称。FCModel将列与属性名相关联,而不与实例变量名相关联。
模型可以具有没有相应数据库列的属性。但如果有任何模型的表列没有相应的属性,FCModel将在启动时在控制台记录通知。
在您的application:didFinishLaunchingWithOptions:
方法中,在**访问任何模型之前**,调用FCModel的openDatabaseAtPath:withSchemaBuilder:
。这看起来有些疯狂,但请继续阅读——在概念上非常简单。
您的模式构建器块接收int *schemaVersion
,这是一个输入/输出参数
*schemaVersion
以反映新版本。以下是从上面描述的Person类中的示例
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *dbPath = [documentsPath stringByAppendingPathComponent:@"testDB.sqlite3"];
[FCModel openDatabaseAtPath:dbPath withSchemaBuilder:^(FMDatabase *db, int *schemaVersion) {
[db beginTransaction];
// My custom failure handling. Yours may vary.
void (^failedAt)(int statement) = ^(int statement){
int lastErrorCode = db.lastErrorCode;
NSString *lastErrorMessage = db.lastErrorMessage;
[db rollback];
NSAssert3(0, @"Migration statement %d failed, code %d: %@", statement, lastErrorCode, lastErrorMessage);
};
if (*schemaVersion < 1) {
if (! [db executeUpdate:
@"CREATE TABLE Person ("
@" id INTEGER PRIMARY KEY,"
@" name TEXT NOT NULL DEFAULT '',"
@" createdTime INTEGER NOT NULL"
@");"
]) failedAt(1);
if (! [db executeUpdate:@"CREATE INDEX IF NOT EXISTS name ON Person (name);"]) failedAt(2);
*schemaVersion = 1;
}
// If you wanted to change the schema in a later app version, you'd add something like this here:
/*
if (*schemaVersion < 2) {
if (! [db executeUpdate:@"ALTER TABLE Person ADD COLUMN title TEXT NOT NULL DEFAULT ''"]) failedAt(3);
*schemaVersion = 2;
}
// And so on...
if (*schemaVersion < 3) {
if (! [db executeUpdate:@"CREATE TABLE..."]) failedAt(4);
*schemaVersion = 3;
}
*/
[db commit];
}];
一旦将版本发布给客户,就永远不要更改代码中的构造方式。这样,在新的版本首次启动时,您的模式构建器会看到客户的现有数据库在例如模式版本2,并且您可以只执行将数据库升级到版本3所必需的。
创建新实例(INSERTS)
// If you want a random 64-bit signed integer primary key value for .id:
Person *bob = [Person new];
// If you want to specify your own .id value:
Person *bob = [Person instanceWithPrimaryKey:@(123)];
bob.name = @"Bob";
bob.createdTime = [NSDate date];
[bob save];
SELECT和UPDATE查询应该对FMDB粉丝来说很熟悉:所有内容都是通过?
占位符和varargs查询函数参数化的,它直接传递给FMDB。就像FMDB一样,您需要将原始类型封装在传递给查询参数时,例如@(1)
而不是1
。
// Find that specific Bob by ID
Person *bob = [Person instanceWithPrimaryKey:@(123)];
bob.name = @"Robert";
[bob save];
// Or find the first person named Bob
Person *firstBob = [Person firstInstanceWhere:@"name = ? ORDER BY id LIMIT 1", @"Bob"];
// Find all Bobs
NSArray *allBobs = [Person instancesWhere:@"name = ?", @"Bob"];
您可以在查询中使用两个快捷键
$T
:模型表名。(例如,“Person”)$PK
:模型主键列名。(例如,“id”)现在这里是最疯狂的部分。假设您想将所有Bob的名字改为Robert或将所有叫Sue的人删除,而不需要逐个加载它们和执行数百万个查询。(嘿,Core Data。)
// Suppose these are hanging out here, being retained somewhere (in the UI, maybe)
Person *bob = [Person instanceWithPrimaryKey:@(123)];
Person *sue = [Person firstInstanceWhere:@"name = 'Sue'"]; // you don't HAVE to parameterize everything
// ...
[Person executeUpdateQuery:@"UPDATE $T SET name = ? WHERE name = ?", @"Robert", @"Bob"];
NSLog(@"This Bob's name is now %@.", bob.name);
// prints: This Bob's name is now Robert.
[Person executeUpdateQuery:@"DELETE FROM $T WHERE name = 'Sue'"];
NSLog(@"Sue is %@.", sue.deleted ? @"deleted" : @"around");
// prints: Sue is deleted.
这是可能的。(至少,应该是这样的。如果您发现问题请告知我。)
FCModel设计时并未考虑自动处理此功能。您需要针对每个模型的实现情况手动编写代码。这样可以完全控制架构、索引使用、自动获取查询(或非自动获取),和缓存。
如果您希望实现自动关系映射,请考虑使用Core Data。它在这方面做得非常好。
每个FCModel实例在内存中都是根据其表和主键值独立存在的。如果您加载了ID为1的个人,并且稍后某个其他查询也加载了ID为1的个人,它们将是同一个实例(除非第一个实例在此期间被释放)。
FCModels在一段时间内是安全的保留状态,甚至可以被UI保留。您可以使用KVO来观察变化。只需检查实例的deleted
属性,并等待变化通知。
FCModels默认按主键进行缓存
NSArray *allBobs = [Person instancesWhere:@"name = ?", @"Bob"];
// executes query: SELECT * FROM Person WHERE name = 'Bob'
Person *bob = [Person instanceWithPrimaryKey:@(123)];
// cache hit, no query executed
...但只缓存了您的应用保留的内容。如果您想缓存整个表格,例如,您可能需要在某个长期存在的位置(如应用程序代理)保留它的allInstances
数组。
FCModels可以从任何线程访问和修改(据我所知),但所有数据库操作都在一个序列队列上同步运行,所以您不太可能通过并发访问看到性能上的提升。
FCModel的公共通知(如FCModelInsertNotification
等)始终在主线程上发布。
目前,支持信息仅位于GitHub上。
...是受欢迎的,以下是一些指导方针
最重要的是,我希望FCModel保持小、简单,并且易于适应您的心理L2缓存。