测试已测试 | ✗ |
语言编程语言 | SwiftSwift |
许可证 | MIT |
发布最新版本 | 2015年10月 |
SPM支持 Swift Package Manager | ✗ |
由Steve K. Chiu 维护。
CoreDataMonk 是一个帮助库,可以帮助你在并发设置中使用 CoreData 更加容易和安全。CoreDataMonk 的主要特性包括
CoreDataMonk 提供了一些可能需要了解的类,以下是它们与 CoreData 类的关系
CoreDataMonk 类 | CoreData 类 |
---|---|
CoreDataStack | NSPersistentStoreCoordinator ,NSManagedObjectContext (私有队列并发类型,根保存上下文,可选) |
CoreDataMainContext | NSManagedObjectContext (主队列并发类型,主上下文),它是 CoreDataContext 的子类 |
CoreDataContext | 无,但它充当 CoreDataUpdateContext 的工厂 |
CoreDataUpdateContext | NSManagedObjectContext (私有队列并发类型,更新上下文) |
CoreDataUpdate | 是 CoreDataUpdateContext 的接口 |
您只需要显式创建 CoreDataStack
和 CoreDataMainContext
,就像入门部分中描述的那样,其他类通过方法创建,用户不能创建。
如果您正在使用自定义设置,则可能需要创建 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
方法。
大多数应用将使用 NSFetchedResultsController
与 UITableView
或 UICollectionView
一起使用。CoreDataMonk 有两个类以简化此过程,即 TableViewDataProvider
和 CollectionViewDataProvider
。
以 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,那么您必须自己合并并重新加载数据上下文。
这是一个有趣的设置,您根本不需要合并,只需在收到提交通知后简单重置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()
。