All the while, my thoughts on Batch Update Operations are that they are much faster than updating via NSManagedObject. However, as per my testing on a 2000 rows production data set, using Batch Update Operations is 2 to 3 times slower than updating via NSManagedObject.
My update pattern is, there are around 2000 rows. I am updating each of their column named "order", with different value for each row.
The goal is simple, we want to make the current row's order
value, is larger than previous row's order
value.
Here's my code snippet. nsPlainNote
is an NSManagedObject
private func _updateOrdersIfPossible2(context: NSManagedObjectContext, updateOrders: [UpdateOrder]) {
let count = updateOrders.count
if count < 2 {
return
}
var prevOrder = updateOrders[0].order
var updatedObjectIDs = [NSManagedObjectID]()
if !Utils.isValidOrder(prevOrder) {
prevOrder = prevOrder - 1
precondition(Utils.isValidOrder(prevOrder))
// Time-consuming operation.
if let updatedObjectID = _updateWithoutMerge(context: context, objectID: updateOrders[0].objectID, propertiesToUpdate: [
"order": prevOrder
]) {
updatedObjectIDs.append(updatedObjectID)
}
}
for index in 1..<count {
precondition(Utils.isValidOrder(prevOrder))
let updateOrder = updateOrders[index]
if !Utils.isValidOrder(updateOrder.order) || updateOrder.order <= prevOrder {
var newOrder = prevOrder + 1
if !Utils.isValidOrder(newOrder) {
newOrder = newOrder + 1
}
precondition(newOrder > prevOrder)
prevOrder = newOrder
precondition(Utils.isValidOrder(newOrder))
// Time-consuming operation.
if let updatedObjectID = _updateWithoutMerge(context: context, objectID: updateOrder.objectID, propertiesToUpdate: [
"order": newOrder
]) {
updatedObjectIDs.append(updatedObjectID)
}
} else {
// Skip from updating. Fast!
prevOrder = updateOrder.order
}
} // for index in 1..<count
if !updatedObjectIDs.isEmpty {
let changes = [NSUpdatedObjectsKey : updatedObjectIDs]
CoreDataStack.INSTANCE.mergeChanges(changes)
}
}
private func _updateWithoutMerge(context: NSManagedObjectContext, objectID: NSManagedObjectID, propertiesToUpdate: [AnyHashable : Any]) -> NSManagedObjectID? {
return RepositoryUtils._updateWithoutMerge(
context: context,
entityName: "NSPlainNote",
objectID: objectID,
propertiesToUpdate: propertiesToUpdate
)
}
static func _updateWithoutMerge(context: NSManagedObjectContext, entityName: String, objectID: NSManagedObjectID, propertiesToUpdate: [AnyHashable : Any]) -> NSManagedObjectID? {
var result: NSManagedObjectID? = nil
do {
let batchUpdateRequest = NSBatchUpdateRequest(entityName: entityName)
batchUpdateRequest.predicate = NSPredicate(format: "self = %@", objectID)
batchUpdateRequest.propertiesToUpdate = propertiesToUpdate
batchUpdateRequest.resultType = .updatedObjectIDsResultType
let batchUpdateResult = try context.execute(batchUpdateRequest) as? NSBatchUpdateResult
if let managedObjectIDs = batchUpdateResult?.result as? [NSManagedObjectID] {
result = managedObjectIDs.first
}
} catch {
context.rollback()
error_log(error)
}
return result
}
As per my benchmark, most time are spent in the loop of calling _updateWithoutMerge
.
private func _updateOrdersIfPossible(context: NSManagedObjectContext, updateOrders: [UpdateOrder]) {
let count = updateOrders.count
if count < 2 {
return
}
var prevOrder = updateOrders[0].order
var updatedObjectIDs = [NSManagedObjectID]()
if !Utils.isValidOrder(prevOrder) {
prevOrder = prevOrder - 1
precondition(Utils.isValidOrder(prevOrder))
// Time-consuming operation.
if let nsPlainNote = NSPlainNoteRepository.getNSPlainNote(context: context, objectID: updateOrders[0].objectID, propertiesToFetch: ["order"]) {
nsPlainNote.order = prevOrder
}
}
for index in 1..<count {
precondition(Utils.isValidOrder(prevOrder))
let updateOrder = updateOrders[index]
if !Utils.isValidOrder(updateOrder.order) || updateOrder.order <= prevOrder {
var newOrder = prevOrder + 1
if !Utils.isValidOrder(newOrder) {
newOrder = newOrder + 1
}
precondition(newOrder > prevOrder)
prevOrder = newOrder
precondition(Utils.isValidOrder(newOrder))
// Time-consuming operation.
if let nsPlainNote = NSPlainNoteRepository.getNSPlainNote(context: context, objectID: updateOrder.objectID, propertiesToFetch: ["order"]) {
nsPlainNote.order = newOrder
}
} else {
// Skip from updating. Fast!
prevOrder = updateOrder.order
}
} // for index in 1..<count
RepositoryUtils.saveContextIfPossible(context)
}
static func getNSPlainNote(context: NSManagedObjectContext, objectID: NSManagedObjectID, propertiesToFetch: [Any]?) -> NSPlainNote? {
var nsPlainNote: NSPlainNote? = nil
context.performAndWait {
let fetchRequest = NSPlainNote.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "self = %@", objectID)
fetchRequest.propertiesToFetch = propertiesToFetch
fetchRequest.fetchLimit = 1
do {
let nsPlainNotes = try fetchRequest.execute()
if let _nsPlainNote = nsPlainNotes.first {
nsPlainNote = _nsPlainNote
}
} catch {
error_log(error)
}
}
return nsPlainNote
}
I thought using Batch Update Operation should be faster than using NSManagedObject?
Am I having wrong expectation, or there is an error in my implementation?
Thanks.
I do not see how you are really using a batch update in your code. You create a new NSBatchUpdateRequest with a predicate for every single objectID, defeating the purpose of it being a batch update.
Also, a batch update's purpose is to change a field for all objects matching a criteria to the same new value. If you want to bump each respective order one by one, it means it has to load each object's value, increase it, and then save it again. NSBatchUpdateRequest is not the correct tool for that purpose.