创建单独的NSManagedObjectContext时出错 [英] Error creating a separate NSManagedObjectContext

查看:1116
本文介绍了创建单独的NSManagedObjectContext时出错的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在处理我的问题之前,请先看看这张图片。





这里是实际的数据模型:



我从Web API检索一组记录,从中创建对象,保存并在今天视图中显示它们。默认情况下,会为当前日期返回这些记录。



用户可以点击过去按钮转到单独的视图,他可以选择过去或未来的日期日期选择器视图和视图所选日期的记录。这意味着我必须再次调用API传递所选日期,检索数据并将该数据保存在核心数据中并显示。当用户离开此视图时,应舍弃此数据。



这是重要的部分。即使我获得了一组新的数据,今天视图中当前日期的旧原始数据也不能消失。因此,如果/当用户返回到今天视图时,该数据应该随时可用,因为他离开它,而应用程序不必调用API,并再次获取当前日期的数据。



我想到创建一个单独的 NSManagedObjectContext 来保存这些临时数据。



我有一个名为 DatabaseManager 的类来处理核心数据相关的任务。这个类用'NSManagedObjectContext'的实例初始化。它在给定的上下文中创建托管对象类。

  import CoreData 
import Foundation
import MagicalRecord
import SwiftyJSON

public class DatabaseManager {

private let context:NSManagedObjectContext!

init(context:NSManagedObjectContext){
self.context = context
}

public func insertRecords(data:AnyObject,success: > Void,failure:(error:NSError?) - > Void){
let json = JSON(data)
如果let records = json.array {
for recordObj in records {
let record = Record.MR_createInContext(context)as Record
record.id = recordObj [Id]。int
record.name = recordObj [Name]。string!
record.date = NSDate(string:recordObj [Date]。string!)
}
context.MR_saveToPersistentStoreAndWait()
success()
}
}
}



因此在Today视图中,我传递 NSManagedObjectContext.MR_defaultContext() insertRecords()方法。我也有一个方法从给定的上下文获取记录。

  func fetchRecords(context:NSManagedObjectContext) [记录]? {
return Record.MR_findAllSortedBy(name,ascending:true,inContext:context)as? [记录]
}

数据从API检索,保存在核心数据中,成功显示。到目前为止所有好的。



在过去视图中,我基本上做同样的事情。但是因为我不想改变原始数据。



尝试#1 - NSManagedObjectContext.MR_context / code>



我用 NSManagedObjectContext.MR_context()创建一个新的上下文。我在过去视图中更改日期,所选日期的数据将被检索并成功保存在数据库中。但这里的问题。当我从核心数据中获取对象时,我也得到了旧的数据。例如,每天只有10条记录。在今天视图中,我显示10条记录。当获取对象在过去视图中,我得到20个对象!我认为它是旧的10对象加上新的。此外,当我尝试在tableview中显示它们时,它在 cellForRowAtIndexPath 方法中出现 EXC_BAD_ACCESS 错误。

  override func tableView(tableView:UITableView,cellForRowAtIndexPath indexPath:NSIndexPath) - > UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(Cell,forIndexPath:indexPath)as UITableViewCell

let record = records [indexPath.row]
cell.textLabel? = record.name // EXC_BAD_ACCESS
cell.detailTextLabel?.text = record.date.toString()

return cell
}



尝试#2 - NSManagedObjectContext.MR_newMainQueueContext() p>

当我更改日期时出现以下错误,应用程式崩溃。



'+ entityForName:nil不是用于搜索实体名称Record的法定NSPersistentStoreCoordinator



尝试#3 - NSManagedObjectContext .MR_contextWithParent(NSManagedObjectContext.MR_defaultContext())



与尝试#1相同的结果。



尝试#4 - 来自Hal的回答我了解到,即使我创建了两个MOC ,它们都指向相同的 NSPersistentStore 。所以我创建了另一个新的商店来保存临时数据在我的AppDelegate。

  MagicalRecord.setupCoreDataStackWithStoreNamed(Records)
MagicalRecord.setupCoreDataStackWithStoreNamed(Records-Temp)

然后当我更改日期以获得新的数据,我将临时存储设置为默认存储。

  func getDate(date:NSDate){
let url = NSPersistentStore.MR_urlForStoreName(Records-Temp)
let store = NSPersistentStore(persistentStoreCoordinator:NSPersistentStoreCoordinator.MR_defaultStoreCoordinator(),configurationName:nil,URL:url,options:nil)
NSPersistentStore.MR_setDefaultPersistentStore存储)

let context = NSManagedObjectContext.MR_defaultContext()
viewModel.populateDatabase(date,context:context)
}
pre>

注意,我使用默认上下文。我得到的数据,但它是与尝试1和3相同的结果。我得到20记录。它们包括来自旧日期和新日期的数据。如果我使用 NSManagedObjectContext.MR_context(),它会像尝试1中一样崩溃。



其他。在App Delegate中创建商店后,我在今天视图中打印出默认商店名称 println(MagicalRecord.defaultStoreName())。奇怪的是,它没有打印我给商店的名称是记录。它显示了 Reports.sqlite 。报告是项目的名称。很奇怪。



为什么我还得到旧的数据?



对不起,如果我的问题有点混乱,所以我上传一个演示项目到我的 Dropbox 。希望这将有所帮助。



任何帮助都很感激。



谢谢。

解决方案

线程安全



首先我要提到核心数据的黄金法则。 NSManagedObject不是线程安全的,因此,你不能跨越流( WWDC)。这意味着,您应该总是在其上下文中访问受管对象,而不要在其上下文之外传递它。这是为什么你的进口商类会担心我,你在一个上下文中插入一堆对象,而不保证你正在Context中运行插入。



一个简单的代码更改将修复此问题:

  public func insertRecords(data:AnyObject,success: NSError?) - > Void){
let json = JSON(data)
context.performBlock {() - > Void in
//现在我们是线程安全的:)
如果let records = json.array {
for recordObj in records {
let record = Record.MR_createInContext(context)as记录
record.id = recordObj [Id]。int
record.name = recordObj [Name]。string!
record.date = NSDate(string:recordObj [Date]。string!)
}
context.MR_saveToPersistentStoreAndWait()
success()
}
}
}

唯一不需要担心的是当你使用主队列上下文和访问主线程上的对象,如在tableview的等。



不要忘记MagicalRecord也有方便的保存实用程序创建上下文成熟的保存:

  MagicalRecord.saveWithBlock {(context) - >无效
// save me baby
}



显示旧记录< h2>

现在您的问题,您的帖子中的以下段落与我有关:


用户可以点击过去按钮进入单独的视图,他可以
从日期选择器视图中选择过去或将来的日期,并查看该选定日期的记录
。这意味着我必须再次调用
传递所选日期,检索数据并将该数据保存在
核心数据中并显示它们。当用户离开此视图时,应舍弃此数据


我不喜欢你的想法丢弃用户离开该视图后请求的信息。作为用户,我希望能够导航回旧的列表,并查看我刚刚查询的结果,没有另一个不必要的网络请求。这可能更有意义,也许有一个删除实用程序,在启动时修剪你的旧对象,而不是当用户正在访问他们。



无论如何,我不能说明它的重要性是您熟悉 NSFetchedResultsController


此类旨在有效地管理
a从核心数据获取请求返回的结果。



您使用获取请求配置此类的实例,
指定实体,谓词和一个包含至少一个排序顺序的数组
。当你执行fetch时,
实例有效地收集关于结果的信息,而没有
需要同时将所有结果对象带入内存。
当您访问结果时,对象会自动错误地分入
内存中,以匹配可能的访问模式,并从以前访问的
对象中进行处理。这种行为进一步有助于保持
的内存需求低,所以即使你遍历一个包含成千上万个对象的集合
,你应该永远不会有更多的
比在内存中的几十个


取自苹果



它字面上为你做的一切,应该是任何列表的对象来自Core Data。


当我从核心数据中提取对象时,我也得到旧数据


无论如何,您并未指定您的抓取包含特定日期范围内的报告。这里是一个示例fetch:

  let fetch = Record.MR_createFetchRequest()
let maxDateForThisController = NSDate您的日期
fetch.predicate = NSPredicate(格式:date<%@,argumentArray:[maxDateForThisController])
fetch.fetchBatchSize = 10 //或任意数字
let dateSortDescriptor = NSSortDescriptor(key:date,ascending:false)
let nameSortDescriptor = NSSortDescriptor(key:name,ascending:true)

fetch.sortDescriptors = [dateSortDescriptor,nameSortDescriptor]它们在数组中的排列顺序问题

let controller = NSFetchedResultsController(fetchRequest:fetch,
managedObjectContext:NSManagedObjectContext.MR_defaultContext(),
sectionNameKeyPath:nil,cacheName: nil)



导入可废弃记录



,你说你想看到旧的报告,并使用一个单独的上下文,不会保存到持久存储。这也很简单,您的导入器需要一个上下文,所以您需要做的是确保您的导入器可以支持导入,而不保存到持久存储。这样,你可以丢弃上下文,对象将与它一起去。所以你的方法签名应该是这样的:

  public func insertRecords(data:AnyObject,canSaveToPersistentStore:Bool = true,success: ) - > Void,failure:(error:NSError?) - > Void){

/ **
导入一些东西
* /
如果canSaveToPersistentStore {
context.MR_saveToPersistentStoreWithCompletion({(完成,错误) - >在
(如果完成)中失效{
success()
} else {
error
}
})
} else {
success()
}
}


Before getting into my issue, please have a look at this image.

Here is the actual data model:

I retrieve a set of Records from a web API, create objects out of them, save them in core data and display them in the Today view. By default these records are returned for the current date.

The user can tap on Past button to go to a separate view where he can choose a past or future date from a date picker view and view Records for that selected date. This means I have to call the API again passing the selected date, retrieve the data and save that data in core data and display them. When the user leaves this view, this data should be discarded.

This is the important part. Even though I get a new set of data, the old original data for the current date in the Today view must not go away. So if/when the user returns to the Today view, that data should be readily available as he left it without the app having to call the API and get the data for the current date again.

I thought of creating a separate NSManagedObjectContext to hold these temporary data.

I have a separate class called DatabaseManager to handle core data related tasks. This class initializes with an instance of `NSManagedObjectContext. It creates the managed object classes in the given context.

import CoreData
import Foundation
import MagicalRecord
import SwiftyJSON

public class DatabaseManager {

    private let context: NSManagedObjectContext!

    init(context: NSManagedObjectContext) {
        self.context = context
    }

    public func insertRecords(data: AnyObject, success: () -> Void, failure: (error: NSError?) -> Void) {
        let json = JSON(data)
        if let records = json.array {
            for recordObj in records {
                let record = Record.MR_createInContext(context) as Record
                record.id = recordObj["Id"].int
                record.name = recordObj["Name"].string!
                record.date = NSDate(string: recordObj["Date"].string!)
            }
            context.MR_saveToPersistentStoreAndWait()
            success()
        }
    }
}

So in the Today view I pass NSManagedObjectContext.MR_defaultContext() to insertRecords() method. I also have a method to fetch Records from the given context.

func fetchRecords(context: NSManagedObjectContext) -> [Record]? {
    return Record.MR_findAllSortedBy("name", ascending: true, inContext: context) as? [Record]
}

The data is retrieved from the API, saved in core data and gets displayed successfully. All good so far.

In the Past View, I have to do basically the same thing. But since I don't want the original data to change. I tried to do this a few ways which MagicalRecord provides.

Attempt #1 - NSManagedObjectContext.MR_context()

I create a new context with NSManagedObjectContext.MR_context(). I change the date in Past view, the data for that selected date gets retrieved and saved in the database successfully. But here's the issue. When I fetch the objects from core data, I get that old data as well. For example, each day has only 10 records. In Today view I display 10 records. When the fetch objects in the Past view, I get 20 objects! I assume it's the old 10 objects plus the new ones. Also when I try to display them in the tableview, it crashes with a EXC_BAD_ACCESS error in the cellForRowAtIndexPath method.

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell

    let record = records[indexPath.row]
    cell.textLabel?.text = record.name // EXC_BAD_ACCESS
    cell.detailTextLabel?.text = record.date.toString()

    return cell
}

Attempt #2 - NSManagedObjectContext.MR_newMainQueueContext()

The app crashes when I change the date with the following error.

'+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Record''

Attempt #3 - NSManagedObjectContext.MR_contextWithParent(NSManagedObjectContext.MR_defaultContext())

Same result as Attempt #1.

Attempt #4 - From Hal's Answer I learned that even though I create two MOCs, they both refer to the same NSPersistentStore. So I created another new store to hold the temporary data in my AppDelegate.

MagicalRecord.setupCoreDataStackWithStoreNamed("Records")
MagicalRecord.setupCoreDataStackWithStoreNamed("Records-Temp")

Then when I change the date to get the new data, I set that temporary store as the default store like this.

func getDate(date: NSDate) {
    let url = NSPersistentStore.MR_urlForStoreName("Records-Temp")
    let store = NSPersistentStore(persistentStoreCoordinator: NSPersistentStoreCoordinator.MR_defaultStoreCoordinator(), configurationName: nil, URL: url, options: nil)
    NSPersistentStore.MR_setDefaultPersistentStore(store)

    let context = NSManagedObjectContext.MR_defaultContext()
    viewModel.populateDatabase(date, context: context)
}

Note that I'm using the default context. I get the data but it's the same result as Attempt 1 and 3. I get 20 records. They include data from both the old date and the new date. If I use NSManagedObjectContext.MR_context(), it would simply crash like in Attempt 1.

I also discovered something else. After creating the stores in App Delegate, I printed out the default store name println(MagicalRecord.defaultStoreName()) in the Today's view. Strangely it didn't print the name I gave the store which is Records. Instead it showed Reports.sqlite. Reports being the project's name. Weird.

Why do I get the old data as well? Am I doing something with when initializing a new context?

Sorry if my question is a little confusing so I uploaded a demo project to my Dropbox. Hopefully that will help.

Any help is appreciated.

Thank you.

解决方案

Thread Safety

First of all I want to mention the Golden Rule of Core Data. NSManagedObject's are not thread safe, hence, "Thou shalt not cross the streams" (WWDC). What this means is that you should always access a Managed Object in its context and never pass it outside of its context. This is why your importer class worries me, you are inserting a bunch of objects into a context without guaranteeing that you are running the insert inside the Context.

One simple code change would fix this:

public func insertRecords(data: AnyObject, success: () -> Void, failure: (error: NSError?) -> Void) {
    let json = JSON(data)
    context.performBlock { () -> Void in
        //now we are thread safe :)
        if let records = json.array {
            for recordObj in records {
                let record = Record.MR_createInContext(context) as Record
                record.id = recordObj["Id"].int
                record.name = recordObj["Name"].string!
                record.date = NSDate(string: recordObj["Date"].string!)
            }
            context.MR_saveToPersistentStoreAndWait()
            success()
        }
    }
}

The only time you don't need to worry about this is when you are using the Main Queue Context and accessing objects on the main thread, like in tableview's etc.

Don't forget that MagicalRecord also has convenient save utilities that create context's ripe for saving :

MagicalRecord.saveWithBlock { (context) -> Void in
  //save me baby
}

Displaying Old Records

Now to your problem, the following paragraph in your post concerns me:

The user can tap on Past button to go to a separate view where he can choose a past or future date from a date picker view and view Records for that selected date. This means I have to call the API again passing the selected date, retrieve the data and save that data in core data and display them. When the user leaves this view, this data should be discarded.

I don't like the idea that you are discarding the information the user has requested once they leave that view. As a user I would expect to be able to navigate back to the old list and see the results I just queried without another unecessary network request. It might make more sense to maybe have a deletion utility that prunes your old objects on startup rather than while the user is accessing them.

Anyways, I cannot illustrate how important it is that you familiarize yourself with NSFetchedResultsController

This class is intended to efficiently manage the results returned from a Core Data fetch request.

You configure an instance of this class using a fetch request that specifies the entity, optionally a filter predicate, and an array containing at least one sort ordering. When you execute the fetch, the instance efficiently collects information about the results without the need to bring all the result objects into memory at the same time. As you access the results, objects are automatically faulted into memory in batches to match likely access patterns, and objects from previous accessed disposed of. This behavior further serves to keep memory requirements low, so even if you traverse a collection containing tens of thousands of objects, you should never have more than tens of them in memory at the same time.

Taken from Apple

It literally does everything for you and should be your go-to for any list that shows objects from Core Data.

When I fetch the objects from core data, I get that old data as well

Thats to be expected, you haven't specified anywhere that your fetch should include the reports in a certain date range. Here's a sample fetch:

let fetch = Record.MR_createFetchRequest()
let maxDateForThisController = NSDate()//get your date
fetch.predicate = NSPredicate(format: "date < %@", argumentArray: [maxDateForThisController])
fetch.fetchBatchSize = 10// or an arbitrary number
let dateSortDescriptor = NSSortDescriptor(key: "date", ascending: false)
let nameSortDescriptor = NSSortDescriptor(key: "name", ascending: true)

fetch.sortDescriptors = [dateSortDescriptor,nameSortDescriptor]//the order in which they are placed in the array matters

let controller = NSFetchedResultsController(fetchRequest: fetch,
        managedObjectContext: NSManagedObjectContext.MR_defaultContext(),
        sectionNameKeyPath: nil, cacheName: nil)

Importing Discardable Records

Finally, you say that you want to see old reports and use a separate context that won't save to the persistent store. Thats also simple, your importer takes a context so all you would need to do is make sure that your importer can support imports without saving to the persistent store. That way you can discard the context and the objects will go with it. So your method signature could look like this:

public func insertRecords(data: AnyObject, canSaveToPersistentStore: Bool = true,success: () -> Void, failure: (error: NSError?) -> Void) {

    /**
      Import some stuff
    */
    if canSaveToPersistentStore {
        context.MR_saveToPersistentStoreWithCompletion({ (complete, error) -> Void in
            if complete {
                success()
            } else {
                error
            }
        })
    } else {
        success()
    }
}

这篇关于创建单独的NSManagedObjectContext时出错的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆