Is NSInMemoryStoreType Incompatible with NSBatchDe

2019-06-07 18:55发布

问题:

I am currently unit testing a layer that interacts with Core Data. It saves, deletes, and updates an Item object. However, my test that attempts to save a couple of Items and then perform a batch deletion keeps failing.

This is Item:

extension Item {

    // MARK: - Properties

    @NSManaged public var date: NSDate

    @NSManaged public var isTaxable: Bool

    @NSManaged public var name: String

    @NSManaged public var price: NSDecimalNumber

    @NSManaged public var quantity: Double

    // MARK: - Fetch Requests

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> { return NSFetchRequest<Item>(entityName: "Item") }

    // MARK: - Validation

    // Manual validation for `Decimal` values is needed. A radar is still open, which is located at https://openradar.appspot.com/13677527.
    public override func validateValue(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey key: String) throws {
        if key == "price", let decimal = value.pointee as? Decimal { if decimal < Decimal(0.01) { throw NSError(domain: NSCocoaErrorDomain, code: 1620, userInfo: ["Item": self]) } }
        if key == "quantity", let double = value.pointee as? Double { if double == 0 { throw NSError(domain: NSCocoaErrorDomain, code: 1620, userInfo: ["Item": self]) } }
    }

}

This is the object that interacts with Core Data, CoreDataStack:

internal class CoreDataStack {

    // MARK: - Properties

    private let modelName: String

    internal lazy var storeContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: self.modelName)
        container.loadPersistentStores { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }
        return container
    }()

    internal lazy var managedContext: NSManagedObjectContext = { return self.storeContainer.viewContext }()

    // MARK: - Initialization

    internal init(modelName: String = "Cart") { self.modelName = modelName }

    // MARK: - Saving

    internal func saveContext() throws {
        guard managedContext.hasChanges else { return }
        do { try managedContext.save() } catch let error as NSError { throw error }
    }

}

This is the object that manages persistence with Core Data:

internal final class ItemPersistenceService {

    // MARK: - Properties

    private let coreDataStack: CoreDataStack

    // MARK: - Initialization

    internal init(coreDataStack: CoreDataStack) {
        self.coreDataStack = coreDataStack
        print("init(coreDataStack:) - ItemPersistenceService")
    }

    // MARK: - Saving

    @discardableResult internal func saveItem(withInformation information: ItemInformation) throws -> Item {
        let item = Item(context: coreDataStack.managedContext)
        item.name = information.name
        item.quantity = information.quantity
        item.price = information.price as NSDecimalNumber
        item.date = information.date as NSDate
        item.isTaxable = information.isTaxable
        do {
            try coreDataStack.saveContext()
        } catch let error as NSError {
            throw error
        }
        return item
    }

    // MARK: - Deleting

    internal func delete(item: Item) throws {
        coreDataStack.managedContext.delete(item)
        do {
            try coreDataStack.saveContext()
        } catch let error as NSError {
            throw error
        }
    }

    internal func deleteAllItems() throws {
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: Item.description())
        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        do {
            try coreDataStack.managedContext.persistentStoreCoordinator?.execute(deleteRequest, with: coreDataStack.managedContext)
        } catch let error as NSError {
            throw error
        }
    }

     // MARK: - Fetching

    internal func itemsCount() throws -> Int {
        let fetchRequest = NSFetchRequest<NSNumber>(entityName: Item.description())
        fetchRequest.resultType = .countResultType
        do {
            let result = try coreDataStack.managedContext.fetch(fetchRequest)
            guard let count = result.first?.intValue else { fatalError("Invalid result") }
            return count
        } catch {
            throw error
        }
    }

}

This is the CoreDataStack subclass that I use for testing, which contains an in-memory store:

internal final class TestCoreDataStack: CoreDataStack {

    // MARK: - Initialization

    internal override init(modelName: String = "Cart") {
        super.init(modelName: modelName)
        let persistentStoreDescription = NSPersistentStoreDescription()
        persistentStoreDescription.type = NSInMemoryStoreType
        let container = NSPersistentContainer(name: modelName)
        container.persistentStoreDescriptions = [persistentStoreDescription]
        container.loadPersistentStores { (storeDescription, error) in
            if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") }
            self.storeContainer = container
        }
    }

}

Finally, this is the test that keeps failing:

internal func test_ItemPersistenceService_Delete_All_Managed_Object_Context_Saved() {
    do {
        try service.saveItem(withInformation: information)
        try service.saveItem(withInformation: information)
    } catch { XCTFail("Expected `Item`") }
    expectation(forNotification: .NSManagedObjectContextDidSave, object: coreDataStack.managedContext) { (notification) in return true }
    do { try service.deleteAllItems() } catch { XCTFail("Expected deletion") }
    waitForExpectations(timeout: 2.0) { error in XCTAssertNil(error, "Expected save to occur") }
}

Questions

Is NSInMemoryStoreType incompatible with NSBatchDeleteRequest?

If not, then what am I doing incorrectly that is causing my test to fail repeatedly?

回答1:

You can always create a persistent store of type SQLite and store it at /dev/null. Here's the code to do it on a swift XCTest class:

var container: NSPersistentContainer!

override func setUp() {
    super.setUp()
    container = NSPersistentContainer(name: "ModelName")
    container.persistentStoreDescriptions[0].url = URL(fileURLWithPath: "/dev/null")
    container.loadPersistentStores { (description, error) in
        XCTAssertNil(error)
    }
}


回答2:

I was hoping to use the same method for deleting a large number of objects efficiently too, but this page states that the NSBatchDeleteRequest is only compatible with SQLite persistent store types, In-memory store types are not supported.

https://developer.apple.com/library/content/featuredarticles/CoreData_Batch_Guide/BatchDeletes/BatchDeletes.html

Important: Batch deletes are only available when you are using a SQLite persistent store

The different persistent store types are listed here:

https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/persistent_store_types