CoreDataMonk 0.9.7

CoreDataMonk 0.9.7

测试已测试
语言编程语言 SwiftSwift
许可证 MIT
发布最新版本2015年10月
SPM支持 Swift Package Manager

Steve K. Chiu 维护。



  • 作者:
  • Steve K. Chiu

CoreDataMonk

CoreDataMonk 是一个帮助库,可以帮助你在并发设置中使用 CoreData 更加容易和安全。CoreDataMonk 的主要特性包括

  • 允许您轻松地以不同方式设置 CoreData(三层次结构、两层结构带自动合并、多个主上下文带手动重新加载等…)
  • 易于使用和理解的 API
  • 友好的 Swift 查询表达式
  • 序列化更新以避免数据一致性问题的处理(可选)
  • 使用异常进行错误处理

CoreDataMonk 类

CoreDataMonk 提供了一些可能需要了解的类,以下是它们与 CoreData 类的关系

CoreDataMonk 类 CoreData 类
CoreDataStack NSPersistentStoreCoordinatorNSManagedObjectContext (私有队列并发类型,根保存上下文,可选)
CoreDataMainContext NSManagedObjectContext (主队列并发类型,主上下文),它是 CoreDataContext 的子类
CoreDataContext 无,但它充当 CoreDataUpdateContext 的工厂
CoreDataUpdateContext NSManagedObjectContext (私有队列并发类型,更新上下文)
CoreDataUpdate CoreDataUpdateContext 的接口

您只需要显式创建 CoreDataStackCoreDataMainContext,就像入门部分中描述的那样,其他类通过方法创建,用户不能创建。

如果您正在使用自定义设置,则可能需要创建 CoreDataContext,更多详细信息请参阅高级设置部分。

入门

设置 CoreDataMonk 很简单,默认提供三层次的 NSManagedObjectContext 设置,这适用于大多数应用程序。您可以使用以下方式完成此操作

// first pick the name you want for the global main context
var World: CoreDataMainContext!

// then in your AppDelegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    do {
        let dataStack = try CoreDataStack()
        try dataStack.addDatabaseStore()
        World = try CoreDataMainContext(stack: dataStack)

        ...
    } catch let error {
        fatalError("fail to init core data: \(error)")
    }
    ...
}

您可以使用 Xcode 模型中的配置使用多个存储

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    do {
        let dataStack = try CoreDataStack()
        try dataStack.addInMemoryStore(configuration: "InMemory")
        try dataStack.addDatabaseStore(configuration: "Database", resetOnFailure: true)
        World = try CoreDataMainContext(stack: dataStack)

        ...
    } catch let error {
        fatalError("fail to init core data: \(error)")
    }
    ...
}

检索对象

现在您可以从视图控制器中的主上下文中检索数据

// you can add % prefix to tell CoreDataMonk it is for key name
let monk = try World.fetch(Person.self, %"name" == "Monk")

let warrior = try World.fetch(Person.self, %"name" == "Warrior" && %"location" == "Taipei")

要检索一组对象

let monks = try World.fetchAll(Person.self)

要添加更多条件

let monks = try World.fetchAll(Person.self,
    %"age" >= 18 || %"location" == "Taipei",
    orderBy: .Ascending("name") | .Descending("age"),
    options: .Limit(100) | .Offset(20)
)

您还可以查询值

let age = try World.queryValue(Person.self, .Average("age")) as! NSNumber

当然,还有更多值

let info_list = try World.query(Person.self,
    .Select("location") | .Average("age"),
    %"name" == "Monk",
    orderBy: .Descending("age"),
    groupBy: %"location"
)

for info in info_list {
    let location = info["location"] as! String
    let age = info["age"] as! NSNumber
    ...
}

创建和更新对象

要创建或更新对象,您必须调用 .beginUpdate(),并且需要在块结束前调用 .commit(),否则所有更改都将被丢弃。

World.beginUpdate() {
    update in

    let farmer = try update.create(Person.self)
    farmer.name = "Framer"
    farmer.age = 28

    let knight = try update.fetchOrCreate(Person.self, %"name" == "Knight" && %"age" == 18)
    knight.friend = farmer

    let monk = try update.fetch(Person.self, %"name" == "Monk")
    monk.age = 44
    monk.friend = knight

    try update.commit()
}

// or you prefer to wait for the update to complete
World.beginUpdateAndWait() {
    update in

    ...
}

如果在主上下文中已经获取了对象,您可以在更新中使用它

let warrior = try World.fetch(Person.self, %"name" == "Warrior" && %"location" == "Taipei")

World.beginUpdate() {
    update in

    let warrior = try update.use(warrior)

    let monk = try update.fetch(Person.self, %"name" == "Monk")
    monk.friend = warrior

    try update.commit()
}

更新可以在嵌套块中使用。更改仅在销毁后才会丢弃。例如,如果需要从服务器获取数据以更新数据

World.beginUpdate() {
    update in

    let monk = try update.fetch(Person.self, %"name" == "Monk")

    // this will start network connection and return result in callback block
    remote_server.findAge(monk.name, location: monk.location) {
        age in

        // you need to call .perform() in nested block
        update.perform() {
            update in

            // it is safe to use the object directly in the same update
            monk.age = age

            try update.commit()
        }
    }
}

如果更新需要多个步骤不能在一个块中完成,可以使用 .beginUpdateContext()

let context = World.beginUpdateContext()

context.perform() {
    update in

    ...
}

...

context.perform() {
    update in

    ...
}

...

context.perform() {
    update in

    ...
    try update.commit()
}

事实上,.beginUpdate() 只是一个带有 perform 的临时更新上下文

public func beginUpdate(block: (CoreDataUpdate) throws -> Void) {
    beginUpdateContext().perform(block)
}

谓词表达式

CoreDataMonk 为谓词表达式添加了一些语法糖,让代码看起来更自然。

首先,您需要一种指定键名的方法,以避免与常量值混淆

表达式 示例 描述
%String %"name" 键“name”
%String%.any %"friend.age"%.any 带有 ANY 修饰符的键“friend.age”
%String%.all %"friend.age"%.all 带有 ALL 修饰符的键“friend.age”

CoreDataMonk 对谓词有一些映射

表达式 示例 描述
%String == Any %"name" == "monk" 等同于 NSPredicate(format: "%K == %@", "name", "monk")
%String == %String %"name" == %"location" 等同于 NSPredicate(format: "%K == %K", "name", "location")
!=, >, <, >=, <= 就像 ==
.Where(String, Any...) .Where("name like %@", pattern) 等同于 NSPredicate(format: "name like %@", pattern)
.Predicate(NSPredicate) .Predicate(my_predicate) 等同于 my_predicate

您使用 &&||! 运算符来组合谓词

运算符 示例 描述
&& %"name" == name && %"age" > age 等同于 NSPredicate(format: "name == %@ and age > %@", name, age)
|| %"name" == name || %"name" == name + " sam" 等同于 NSPredicate(format: "name == %@ or name = %@", name, name + " sam")
! !(%"name" == name && %"age" > age) 等同于 NSPredicate(format: "not (name == %@ and age > %@)", name, age)

orderBy: 表达式

orderBy:.fetchAll.fetchResults.query 方法支持

表达式 示例 描述
.Ascending(String) .Ascending("name") 等同于 NSSortDescriptor(key: "name", ascending: true)
.Descending(String) .Descending("name") 等同于 NSSortDescriptor(key: "name", ascending: false)

您可以使用 | 运算符组合两个或更多表达式

运算符 示例 描述
| .Ascending("name") | .Descending("location") 等同于 [NSSortDescriptor(key: "name", ascending: true), NSSortDescriptor(key: "location", ascending: false)]

options: 表达式

您可以将选项设置为调整 NSFetchRequest,它受所有 .fetch.query 方法支持

表达式 描述
.NoSubEntities fetchRequest.includesSubentities = false
.NoPendingChanges fetchRequest.includesPendingChanges = false
.NoPropertyValues fetchRequest.includesPropertyValues = false
.Limit(Int) fetchRequest.fetchLimit = Int
.Offset(Int) fetchRequest.fetchOffset = Int
.Batch(Int) fetchRequest.fetchBatchSize = Int
.Prefetch([String]) fetchRequest.relationshipKeyPathsForPrefetching = [String]
.PropertiesOnly([String]) fetchRequest.propertiesToFetch = [String] // 在 .query 中被忽略
.Distinct fetchRequest.returnsDistinctResults = true
.Tweak(NSFetchRequest -> Void) 允许块修改fetchRequest

您可以使用 | 运算符组合两个或多个选项

运算符 示例 描述
| .Limit(200) | .Offset(100) [.Limit(200), .Offset(100)] 相同

.query.queryValue 选择表达式

.query 中,您需要指定要返回的选择目标。您可以指定属性、表达式或聚合函数。聚合函数可能有可选别名,如果用户未指定,则默认使用属性名称。确保每个选择目标具有唯一的别名非常重要,因为它用作返回字典中的键。

表达式 描述
.Select(String...) 要获取属性值,.Select("name", "age") 将添加两个目标
.Expression(NSExpressionDescription) 获取 my_expression 的值
.Average(String, alias: String? = nil) 获取属性的平均值
.Sum(String, alias: String? = nil) 获取属性的总和
.StdDev(String, alias: String? = nil) 获取属性的标准差
.Min(String, alias: String? = nil) 获取属性的最小值
.Max(String, alias: String? = nil) 获取属性的最大值
.Median(String, alias: String? = nil) 获取属性的中位数
.Count(String, alias: String? = nil) 获取返回值数量

您可以使用 | 运算符组合两个或更多选择目标

运算符 示例 描述
| .Average("age") | .Min("age", alias: "min_age") 将它们合并为选择目标

groupBy: 表达式

与相同的关键表达式,但仅应用于 .query 方法

表达式 示例 描述
%String %"name" “name” 作为对象属性名

您可以使用 | 运算符组合两个或更多的键

运算符 示例 描述
| %"name" | %"location" ["name", "location"] 相同

having: 表达式

与谓词表达式相同,但仅应用于具有 groupBy:.query 方法。

UITableView 和 UICollectionView 的数据源

大多数应用将使用 NSFetchedResultsControllerUITableViewUICollectionView 一起使用。CoreDataMonk 有两个类以简化此过程,即 TableViewDataProviderCollectionViewDataProvider

TableViewDataProvider 为例

class MyViewController : UIViewController, UITableViewDelegate {
    @IBOutlet weak var tableView: UITableView!
    var dataProvider: TableViewDataProvider<Person>!

    override func viewDidLoad() {
        super.viewDidLoad()

        do {
            self.dataProvider = TableViewDataProvider(context: World).bind(self.tableView) {
                person, indexPath in

                let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! MyTableCell
                cell.title.text = person.title
                ...
                return cell
            }

            try self.dataProvider.query(orderBy: .Ascending("title"))
        } catch let error {
            fatalError("fail to query main context: \(error)")
        }
    }
}

高级设置:默认三层设置

默认情况下,CoreDataMonk 提供了三个层级的 NSManagedObjectContext 设置,如下所示。注意 CoreDataUpdateContext 是临时的,它创建后再释放。

UPDATE                                     <-- MAIN.beginUpdate
[CoreDataUpdateContext]
(NSManagedObjectContext/PrivateQueue)
        |
        |
MAIN (Global)                               --> MAIN.fetch
[CoreDataMainContext]
(NSManagedObjectContext/MainQueue)
        |
        V
ROOT (OPTIONAL)
[CoreDataStack]
(NSManagedObjectContext/PrivateQueue)
        |
        |
STORE   V
[CoreDataStack]
(NSPersistentStoreCoordinator)

如果您查看 CoreDataStack.init,您会发现您可以跳过根上下文。

public enum RootContextType {
    case None
    case Shared
}

public init(modelName: String? = nil,
        bundle: NSBundle? = nil,
        rootContext: RootContextType = .Shared) throws

高级设置:带有自动合并的两层设置

设置 CoreData 不只有一种方式,这里还有另一种流行的设置。事务上下文将其父设置为根,主上下文从通知中获取合并数据。优点是主上下文不需要处理所有的合并工作,它只需要合并已注册的对象,在某些情况下可能更快。缺点是可能无法获取所有数据,特别是如果您需要从关系中获取某些属性,您可能根本无法接收通知。

UPDATE                                     <-- MAIN.beginUpdate
[CoreDataUpdateContext]
(NSManagedObjectContext/PrivateQueue)
        |
        |           MAIN (Global)           --> MAIN.fetch
        |           [CoreDataMainContext]
        |           (NSManagedObjectContext/MainQueue)
        |              |         ^
        V              |         |
ROOT (OPTIONAL)        |         | MERGE via notification
[CoreDataStack]        V         |
(NSManagedObjectContext/PrivateQueue)
        |
        |
STORE   V
[CoreDataStack]
(NSPersistentStoreCoordinator)

您可以通过CoreDataMonk轻松设置它,让我们一起看看CoreDataMainContext.init方法。

public enum UpdateTarget {
    case MainContext
    case RootContext(autoMerge: Bool)
    case PersistentStore
}

public enum UpdateOrder {
    case Serial
    case Default
}

public init(stack: CoreDataStack,
        updateTarget: UpdateTarget = .MainContext,
        updateOrder: UpdateOrder = .Default) throws

updateTarget指定了CoreDataUpdateContext的父上下文,默认值是.MainContext。现在我们只需要将其更改为.RootContext(autoMerge: true),使其与ROOT关联并自动合并,然后我们就完成了。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    do {
        let dataStack = try CoreDataStack()
        try dataStack.addDatabaseStore(resetOnFailure: true)
        World = try CoreDataMainContext(stack: dataStack, updateTarget: .RootContext(autoMerge: true))

        ...
    } catch let error {
        fatalError("fail to init core data: \(error)")
    }
    ...
}

请注意,您必须拥有ROOT上下文才能使自动合并生效。如果在创建CoreDataStack时跳过了ROOT,那么您必须自己合并并重新加载数据上下文。

高级设置:每个ViewController都有自己的CoreDataMainContext

这是一个有趣的设置,您根本不需要合并,只需在收到提交通知后简单重置MAIN上下文即可。

UPDATE                                 <-- (UPDATER or MAIN).beginUpdate
[CoreDataUpdateContext]
(NSManagedObjectContext/PrivateQueue)
        |                                          UPDATER
        |                                          [CoreDataContext]
        |                                          (no NSManagedObjectContext)
        |
        |
        |       MAIN (ViewController1)             MAIN (ViewController2)
        |      [CoreDataMainContext]               [CoreDataMainContext]
        |      (NSManagedObjectContext/MainQueue)  (NSManagedObjectContext/MainQueue)
        |                |                             |
        |                |                             |
STORE   V                |                             |
[CoreDataStack]          V                             V
(NSPersistentStoreCoordinator)

没有全局的CoreDataMainContext,您可以在UIViewController.viewDidLoad方法中创建CoreDataMainContext

也许您还需要创建全局的UPDATER(一个或多个,或者只需使用MAIN)。所有更新都通过UPDATER进行,UIViewController需要通过CoreDataContext.observeCommit注册通知观察者。

在这个设置中,可能不需要根(ROOT),只需传递.PersistentStore,或者如果仍然需要根,则传递.RootContext(autoMerge: false)

var DataStack: CoreDataStack!
var Updater: CoreDataContext!

class AppDelegate {
    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        do {
            DataStack = try CoreDataStack(rootContext: .None)
            try DataStack.addDatabaseStore(resetOnFailure: true)
            Updater = try CoreDataContext(stack: DataStack, updateTarget: .PersistentStore)
            ...
        } catch let error {
            fatalError("fail to init core data: \(error)")
        }
        ...
    }
    ...
}

class ViewController : UIViewController {
    var context: CoreDataMainContext!

    // it is important to keep this reference, it will removeObserver after it is de-inited
    var observer: AnyObject!

    override func viewDidLoad() {
        super.viewDidLoad()

        do {
            self.context = try CoreDataMainContext(stack: DataStack, updateTarget: .PersistentStore)
            self.observer = Updater.observeCommit() {
                self.context.reset()
                ...
                // reload data here
            }
            ...
        } catch let error {
            fatalError("fail to init main context: \(error)")
        }
    }
}

class BackgroundWorker {
    func process() {
        Updater.beginUpdate() {
            update in

            ...
        }
    }
}

高级设置:强制顺序更新

默认情况下,CoreDataMonk将允许不同的线程同时调用.beginUpdate。这对性能有好处,但正如您所想的,如果有不同的线程在处理同一个实体,可能会出现数据竞争问题。

这个问题可能没有您想象的那么严重,这就是我们为什么默认允许它的原因。关键是不同线程如何处理实体。在大多数情况下,大多数应用程序使用不同的线程处理不同的实体,因此您没有数据竞争问题。

但是,如果确实如此,可以在创建CoreDataMainContext时传递.Serial

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    do {
        let dataStack = try CoreDataStack()
        try dataStack.addDatabaseStore(resetOnFailure: true)
        World = try CoreDataMainContext(stack: dataStack, updateOrder: .Serial)

        ...
    } catch let error {
        fatalError("fail to init core data: \(error)")
    }
    ...
}

为了清楚,同一更新上下文中的每个.perform总是按顺序进行,不同的更新上下文可能会同时运行其.perform

updateOrder: .Serial确保全局每次只有一个.perform运行。但是,如果在长运行更新上下文中调用.perform时,中间可能还有来自其他更新上下文的.perform,这可能导致数据一致性问题的出现。

避免这种情况的规则实际上非常简单,即如果您正在使用updateOrder: .Serial,则不要调用.beginUpdateContext(),而应专门使用.beginUpdate()