创建单独的NSManagedObjectContext时出错

时间:2015-03-05 13:40:26

标签: ios swift core-data nsmanagedobjectcontext magicalrecord

在讨论我的问题之前,请看一下这张图片。

enter image description here

这是实际的数据模型: data model

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

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

这是重要的部分。即使我获得了一组新数据,“今日”视图中当前日期的旧原始数据也不会消失。因此,如果/当用户返回Today视图时,该数据应该随时可用,而不必让应用程序调用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)
        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()
        }
    }
}

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

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

从API检索数据,保存在核心数据中并成功显示。到目前为止一切都很好。

在过去的观点中,我必须做同样的事情。但是因为我不希望原始数据发生变化。我试着通过MagicalRecord提供的几种方式来做到这一点。

尝试#1 - NSManagedObjectContext.MR_context()

我使用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?.text = record.name // EXC_BAD_ACCESS
    cell.detailTextLabel?.text = record.date.toString()

    return cell
}

尝试#2 - NSManagedObjectContext.MR_newMainQueueContext()

当我使用以下错误更改日期时应用程序崩溃。

' + entityForName:nil不是合法的NSPersistentStoreCoordinator,用于搜索实体名称'记录'

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

与尝试#1相同的结果。

尝试#4 - 来自哈尔的Answer我了解到,尽管我创建了两个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(store)

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

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

我还发现了别的东西。在App Delegate中创建商店后,我在Today's视图中打印出默认商店名称println(MagicalRecord.defaultStoreName())。奇怪的是,它没有打印我给商店的名称记录。相反,它显示了 Reports.sqlite 。报告是项目的名称。怪异。

为什么我也会获得旧数据?在初始化新上下文时,我是否正在做些什么?

很抱歉,如果我的问题有点令人困惑,那么我将一个演示项目上传到我的Dropbox。希望这会有所帮助。

感谢任何帮助。

谢谢。

2 个答案:

答案 0 :(得分:2)

线程安全

首先,我想提一下核心数据的黄金法则。 NSManagedObject不是线程安全的,因此,"你不能跨越流" (WWDC)。这意味着您应该始终在其上下文中访问托管对象,并且永远不要将其传递到其上下文之外。这就是您的导入器类让我担心的原因,您将一堆对象插入到上下文中不保证您在Context中运行插入。

一个简单的代码更改可以解决这个问题:

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()
        }
    }
}

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

不要忘记MagicalRecord还有方便的保存工具,可以创建保存成熟的环境:

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

显示旧记录

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

  

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

我不喜欢您放弃用户在离开该视图时请求的信息。作为用户,我希望能够导航回旧列表并查看我刚刚查询的结果,而无需其他不必要的网络请求。可能有一个删除实用程序在启动时修剪旧对象而不是在用户访问它们时更有意义。

无论如何,我无法说明您熟悉NSFetchedResultsController

的重要性
  

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

     

使用获取请求配置此类的实例   指定实体,可选的过滤谓词和数组   包含至少一个排序顺序。当你执行fetch时,   实例有效地收集有关结果的信息   需要将所有结果对象同时存入内存。   在访问结果时,对象会自动出现故障   批量存储以匹配可能的访问模式和来自的对象   以前访问过的。这种行为进一步有助于保持   内存要求低,所以即使你遍历一个集合   包含数万个对象,你应该永远不会有更多   同时记忆中的几十个。

取自Apple

它确实为您做了一切,并且应该是显示Core Data中对象的任何列表的首选。

  

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

这是预期的,您还没有指定您的提取应包含特定日期范围内的报告的任何位置。这是一个示例获取:

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)

导入可废弃记录

最后,您说要查看旧报告并使用不会保存到持久性存储的单独上下文。这也很简单,你的导入器需要一个上下文,所以你需要做的就是确保你的导入器可以支持导入而不保存到持久存储。这样你就可以丢弃上下文,对象就会随之而来。所以你的方法签名可能如下所示:

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()
    }
}

答案 1 :(得分:0)

持久存储中存在并且使用原始MOC寻址的旧数据仍然存在,并且将在第二个MOC执行提取时检索。他们都在看同一个持久性商店。只是第二个MOC也有从您的API获取的新数据。

保存到Core Data的同步网络操作会挂起您的应用程序,并且(对于足够大的记录集)会导致系统终止您的应用程序,并在崩溃时显示给用户。你的客户在这一点上是错的,需要接受教育。

打破了获取,保存和查看的逻辑。显示特定日期记录的视图应该只是这样 - 如果它接受日期并使用谓词,它可以做到。

你的'cellForRowAtIndexPath'崩溃有点像标识符丢失或拼写错误的问题。如果你硬编码字符串而不是使用'record.name'会发生什么?