FCModel 0.9.0

FCModel 0.9.0

测试已测试
语言语言 Obj-CObjective C
许可协议 MIT
发布最后发布2014年12月

Marco Arment.维护。



FCModel 0.9.0

  • 作者:
  • Marco Arment

适用于喜欢直接SQL访问的人的Core Data的替代方案。

Marco Arment编写。有关许可信息,请查看LICENSE文件(它是MIT许可)。

FCModel是在SQLite之上通用模型层的。它是为那些想要一些Core Data的便利性,但又想要更多对实现、性能、数据库模式、查询、索引和迁移的控制,以及直接使用原始SQL查询和SQLite功能的能力的人设计的。

FCModel实现了Brent Simmons在关于SQLite而不是Core Data的撰写的很多内容。这是我自己的版本。(你在阅读objc.io吗?你应该会。它很棒。)

测试状态

这是一个测试版。我正在用它来构建一个应用程序,一些人正在使用它并做出贡献,到目前为止它对我们来说已经稳定。但变更仍然相对频繁,API可能仍然会以轻微但不兼容的方式改变。

要求

  • Xcode 5或更高版本
  • 在iOS 6或更高版本上部署,或在Mac OS X 10.8或更高版本上(需要NSMapTable
  • 仅ARC
  • FMDB,Gus Mueller出色的Objective-C SQLite封装器(如果您使用CocoaPods则自动设置)
  • 将项目链接到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

属性类型

数据库映射的对象属性可以是

  • 原始类型(intdoubleBOOLint64_t等)或 NSNumber,限于 SQLite的精度(整数64位有符号

  • NSString,始终以UTF-8存储和加载
  • NSData用于BLOB
  • NSDate,从1970年转换到/从 NSTimeInterval(Unix时间戳的double有符号版本)进行存储。在表中声明NSDate列时,请声明为REAL
  • NSURL,在存储时将其转换为/从中转换为其absoluteString表示形式。
  • NSDictionaryNSArray,在存储时转换为/从binary plists转换为存储(因此每个内部对象都必须是NSDataNSStringNSArrayNSDictionaryNSDateNSNumber)。

要覆盖此行为或对其他类型进行自定义,模型可以覆盖下面的方法。数据库值可以是INTEGERFLOATTEXT列的NSStringNSNumber,或者是用于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,这是一个输入/输出参数

  • FCModel在 vásvalidated传入时通知您当前的架构(在一个空的数据库中,它从0开始)。
  • 您执行任何模式创建或迁移语句,以获得下一个模式版本。
  • 更新*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缓存。