Efficient way to update large quantity of NSManagedObject - ios

There are quite many questions about this kind of question, but I can't find any for CoreData. The problem is: I want to update a large amount of NSManagedObject (you can think of resetting a property's of a large amount of NSManagedObject).
Right now, all I can think of is:
Fetch all object.
Loop through all of it, using forin-loop.
Setting the property in each for block.
The data might be large, and I also want to write a Utility for this action. So the question is:
Is there any more efficient way to perform this? I don't believe using for-loop is efficent.
BONUS QUESTION
Also, I would like to delete all object that satisfied a condition (most likely a boolean flag). My solution is rather simple like the one above:
Fetch all object, with NSPredicate and condition.
Loop through all, forin.
Delete each one of it.
Same question for solution.
The real problem
The real problem is, I want to set all the object's flag (call it willDelete) = YES. Then synchronize from server, then update them and set willDelete = NO. After that, whichever has willDelete = YES would be delete from context.
EDIT 1
My question might be different this one. I want to update the property first. And I care about performance time, not the memory.
EDIT 2
Okay, I managed to use NSBatchUpdateRequest. But the problem is: I got nsmergeConflict. Not sure what to do with this progress. Here's the code anyway:
- (void)resetProductUpdatedStatus
{
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.propertiesToUpdate = #{#"hasUpdated" : #(NO)};
request.resultType = NSUpdatedObjectIDsResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[[CoreDataUtil managedObjectContext] executeRequest:request error:nil];
[result.result enumerateObjectsUsingBlock:^(NSManagedObjectID *objId, NSUInteger idx, BOOL *stop) {
NSManagedObject *obj = [[CoreDataUtil managedObjectContext] objectWithID:objId];
if (!obj.isFault) {
[[CoreDataUtil managedObjectContext] refreshObject:obj mergeChanges:YES];
}
}];
}
This will set all hasUpdated = NO. Next, I'll perform the sync progress. With all the products caught from the synchronization will update the hasUpdated = YES. Next perform delete:
- (void)updateProductActiveStatus
{
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.predicate = [NSPredicate predicateWithFormat:#"hasUpdated = NO"];
request.propertiesToUpdate = #{#"isActive" : #(NO)};
request.resultType = NSUpdatedObjectIDsResultType;
[[CoreDataUtil managedObjectContext] executeRequest:request error:nil];
}
As you can see, I've deleted the merge in the update status. So probably, it cause merge conflict in the reset status. So, I guess I will have to fix the merge progress. So I'll ask to people here if you have any idea about this.

Here you can also check the time taken for badge update is quite less than what happens in normal update .As in batchupdate we don't need to fetch the data from file itself. Also check my github link : https://github.com/tapashm42/CoreDataInSwift.git .
I am working on the batchdelete part as well.
func batchUpdate(entityName: String,context:NSManagedObjectContext) {
let batchUpdateRequest = NSBatchUpdateRequest(entityName: entityName)
batchUpdateRequest.propertiesToUpdate = ["name":"Tapash Mollick","designation":"Associate IT Consultant"]
batchUpdateRequest.resultType = .updatedObjectIDsResultType
do {
let start = Date().currentTimeMillis
let batchUpdateResult = try context.execute(batchUpdateRequest) as! NSBatchUpdateResult
let result = batchUpdateResult.result as! [NSManagedObjectID]
print("time taken to update\(Date().currentTimeMillis - start)")
for objectId in result {
let manageObject = context.object(with: objectId)
if (!manageObject.isFault) {
context.stalenessInterval = 0
context.refresh(manageObject, mergeChanges: true)
}
}
}
catch{
fatalError("Unable to batchUpdate")
}
}
extension Date{
var currentTimeMillis: Int64 {
return Int64(Date().timeIntervalSince1970 * 1000)
}
}

Related

NSArray as parameter in Core Data fetchRequest

I have database of about 10.000 entities and dynamic NSArray of 50 NSString elements. Would like to check if each of those elements exist in database and create new array of ones that exist. I don't need to return whole entities, just those NSString titles ( which are the same as in NSString array)
NSPredicate should compare entity.title to NSString element with EXACT match.
What is the best and processor/memory efficient way to do it?
I think that you should use the 'in' operation in your predicate to get your result. This lets you leverage the database to perform the comparison, instead of bringing back all of the 10,000 records to compare yourself. If you take this approach, your code could look like this:
// Assuming that arrayName is your existing array of values to match, that
// EntityName is the object in CoreData that you’re looking at, and context
// is your moc
var newArray: [String] = []
let fetchRequest = NSFetchRequest<EntityName>(entityName: "EntityName")
fetchRequest.predicate = NSPredicate(format: "title in %#", arrayName)
do {
result = try context.fetch(fetchRequest)
for element in result {
newArray.append(element.title)
}
} catch {
… manage any errors …
}
Note - I'm targeting Swift 3.0 compatible code - not sure if swift was what you're after.

Parsing JSON into usable dictionaries

I'm currently building an food tracker iOS app (using swift 2) and I would like to have a database with all the foods (and their info) stored in the app and accessible.
The idea is that when some add a 'ice cream' to their meal, his calories/sugar/fat 'counters' increase by the respective nutritional value of the ice cream. (so that this data can be processed later on)
I have found a database of food in what seems like JSON format (see bellow) but I have no idea how to process all this data with swift so that I could access the number of calories in a specific ingredient for example.
So far I tried this:
let url = NSURL(string: "myURL")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url!, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error)
} else {
let jsonResult = (try! NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)) as! NSDictionary
print(jsonResult)
}
})
task.resume()
}
It allows me to process the JSON format into a dictionary that I can access but what I need (I think) would be maybe a dictionary of arrays and I can't manage to make it work with the JSON format that I have bellow.
[
{
"Description": "Juice",
"Energy(kcal)Per 100 g": 29,
},
{
"Description": "Alcoholic beverage, daiquiri, canned",
"Energy(kcal)Per 100 g": 125,
}
...
]
I admit my question wasn't quite clear at first (I'm really new at this I apologize) but I actually tried to research it on Stackoverflow before posting, but I haven't find something that works for my case. Sorry again and many thank you for taking the time to still answer it :)
Have a look into NSJSONSerialization. That is what you get for free once installed xcode and the SDK. And it is not that bad actually.
This is Ray's approach to Swifty Json:
http://www.raywenderlich.com/82706/working-with-json-in-swift-tutorial
This is what you find when you use the search. You will have to "translate" it to swift though.
How do I parse JSON with Objective-C?
You may want to look at RestKit for some more convenient way of dealing with JSON sources.
Give it a try. And when you run into concrete problems, then get back to SO.
Just give it a try
var arrDicts: [Dictionary<String, AnyObject>] = []
arrDicts = try! NSJSONSerialization.JSONObjectWithData(dataFromService!, options: NSJSONReadingOptions.AllowFragments) as! [Dictionary<String, AnyObject>]
dataFromService is the data that you have received from web service.
Answer for reference pruposes. How to do this in Objective-C
1- First get the info
a) If you are getting the JSON from an API or any online site:
//Set url of the JSON
NString *urlReq = #"http://www.URLHERE.com/PATH/TO/THE/JSON"
//Get data from the JSON
NSData *jsonData = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlReq]];
//Parse JSON
if(jsonData != nil){ //If the response is nil, the next line will crash
NSArray *resultArray = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
//Do stuff with the result...
}
b) If you are getting the information from the Core Data:
//Get context
NSManagedObjectContext *context = [self managedObjectContext];
//Preapre your fetch
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Products" inManagedObjectContext:context];
NSFetchRequest *requestCoreData = [[NSFetchRequest alloc] init];
[requestCoreData setEntity:entityDescription];
//add predicates if needed
//Execute fetch
NSArray *resultArray = [context executeFetchRequest:requestCoreData error:nil];
//do stuff with the result....
2- Then parse the retrieved info
a) If you want a specific index:
NSString *description = resultArray[0][#"description"];
b) if you don't know what is the index you want (most likely what happens in your JSON):
BOOL found = NO;
int index = 0;
while(index < [resultArray count] && !found){
if([resultArray[index][#"description"] isEqualToString:#"Juice"])
found = YES;
else
++index;
}
if(found){
//'index' => index where the info you are searching can be found
}
else{
//The info couldn't be found in the array
}

Return unique/distinct values with Realm query

I have a Message/RLMObject model that has a NSString *jabberID property/row and I want to retrieve every unique value inside that row.
In other word, I want to retrieve non-repeated jabberID values from my Message model. Can anyone help out figuring this?
The way I use to do with coredata was using returnsDistinctResults setting on the NSFetchRequest.
Functional programming approach since Swift has it, and Realm lazy loads; Not as easy/available a solution in Objective-C but for Swift at least:
Swift
let distinctTypes = reduce(Realm().objects(User), []) { $0 + (!contains($0, $1.type) ? [$1.type] : [] ) }
UPDATED:
Swift reduce is kind of a performance intensive, allocating a bunch of intermediate array's, instead the following should be much better performance wise, but must be explicitly cast
let distinctTypes = Array(Set(Realm().objects(User).valueForKey("type") as! [String]))
I found out Realm doesn't fully support distinct queries yet. The good news is I also found a workaround for it, on this github issue.
Objective-c
RLMResults *messages = [Message allObjects];
NSMutableArray *uniqueIDs = [[NSMutableArray alloc] init];
NSMutableArray *uniqueMessages = [[NSMutableArray alloc] init];
for (Message *msg in messages) {
NSString *jabberID = msg.jabberID;
Message *uniqueMSG = (Message *)msg;
if (![uniqueIDs containsObject:jabberID]) {
[uniqueMessages addObject:uniqueMSG];
[uniqueIDs addObject:jabberID];
}
}
Swift 3.0
let realm = try! Realm()
let distinctIDs = Set(realm.objects(Message.self).value(forKey: "jabberID") as! [String])
var distinctMessages = [Message]()
for jabberID in distinctIDs {
if let message = realm.objects(Message.self).filter("jabberID = '\(jabberID)'").first {
distinctMessages.append(message)
}
}

Core Data returns nil for relationship with valid object

I have a weird error. I have a relationship between a call object and status object. the relationship name from the call object is statusForCall.
the data is imported into core data programmatically from JSON. it will not create a call without a status. the user has no way of changing the statusForCall to nil.
however after a period of time (seems to be days) when accessing calls via a fetch request into an array, when I access aCall.StatusForcall.statusID it starts returning nil. There is no way the code can have updated the statusForCall to nil.
Any ideas what might be causing this and where to start looking?
the cachename is nil throughout the application.
interestingly, if the user redownload the application the problem is solved. The code doesn't change and the data doesn't change (there is no migration - it's the same version) - but for some reason this ALWAYS fixes the issue.
i'm really struggling to know what to look at to get to the bottom of this.
code for setting the status below (edited down for conciseness). As i say the job initially has a status but seems to lose the relationship after a period od of time (i can't tell how long this is as the user is unreliable on this)
Call *theCall;
//see if we have the call on the device - if not create a new one, if we do update that one
BOOL contentIsInCoreData = NO;
for (Call *thisCall in existingCalls)
{
if ([thisCall.callID isEqualToNumber:[checkDic objectForKey:#"callID"]])
{
theCall=thisCall;
contentIsInCoreData = YES;
}
}
NSError * error = nil;
NSDateFormatter *dateFormat =[[NSDateFormatter alloc] init];
[dateFormat setDateFormat:#"dd-MMM-yyyy"];
if (contentIsInCoreData==NO)
{
[self.postDelegate updateGetJobsAndTimesProgress:[NSString stringWithFormat:#"Adding new job: %#",[checkDic objectForKey:#"woRef"]]];
//new call add bits that will not change
theCall = (Call*)[NSEntityDescription insertNewObjectForEntityForName:#"Call" inManagedObjectContext:self.managedObjectContext];
}
//set various properties from the JSON dictrionary – these have been cut down for the example code
theCall.woRef=[checkDic objectForKey:#"woRef"];
theCall.shortAddress=[checkDic objectForKey:#"shortAddress"];
theCall.address=[[checkDic objectForKey:#"address"] stringByReplacingOccurrencesOfString:#"\\n" withString:#"\n"];
theCall.postCode=[checkDic objectForKey:#"postCode"];
theCall.callID=[checkDic objectForKey:#"callID"];
//****THIS IS WHERE THE STATUS IS SET – PLEASE NOTE THE STATUS WILL EXIST IN CD ALREADY and the JSON WILL have a valid statusID
NSFetchRequest *request2=[NSFetchRequest fetchRequestWithEntityName:#"CallStatus"];
request2.predicate=[NSPredicate predicateWithFormat:#"callStatusID ==%#",[checkDic objectForKey:#"callStatusID"]];
error = nil;
NSArray * existingStatus = [self.managedObjectContext executeFetchRequest:request2 error:&error];
CallStatus *selectedStatus;
selectedStatus=[existingStatus firstObject];
theCall.statusForCall = selectedStatus;
error = nil;
if ([self.managedObjectContext save:&error])
{
//NSLog(#"saved job");
}
else
{
NSLog (#"***BROKEN GET JSON!!!\r\r%#",error);
}
turns out that a relationship property had been accidently changed - it was cascading a delete from the many end - which resulted in the one end being deleted.

Fetch request returns old data

I'm getting outdated results when executing a fetch request upon a NSManagedObjectContextObjectsDidChangeNotification.
As an example, consider directories with documents that can be deleted logically with a boolean attribute (softDeleted). The fetch request should return all directories that have at least one non-deleted document (directoriesWithDocuments).
The initial state is single directory with a single deleted document. directoriesWithDocuments returns an empty array.
The following code restores the document by setting the softDeleted boolean to NO.
[_context.undoManager beginUndoGrouping];
[_context.undoManager setActionName:#"undelete"];
document.softDeleted = #(NO);
NSError *error;
BOOL success = [_context save:&error]; // This triggers the notification
[_context.undoManager endUndoGrouping];
The save triggers a NSManagedObjectContextObjectsDidChangeNotification. I expected directoriesWithDocuments to return the directory, but instead it still returns an empty array.
- (void)objectsDidChangeNotification:(NSNotification*)notification
{
NSArray *objects = [self directoriesWithDocuments]; // Still empty!
}
Yet, if I execute directoriesWithDocuments after saving the context, either right away or in the next run loop, it returns the directory as expected.
This is the code of the fetch request:
- (NSArray*)directoriesWithDocuments
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANY documents.softDeleted == NO"];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Directory"];
fetchRequest.predicate = predicate;
fetchRequest.includesPendingChanges = YES; // Just in case
NSError *error = nil;
NSArray *objects = [_context executeFetchRequest:fetchRequest error:&error];
return directories;
}
I suspect that the context has some kind of cache for fetch requests that is not cleared until the notification is handled. Is this how Core Data is expected to behave? Or am I doing something wrong?
Workaround
I'm currently delaying the execution of the fetch request like this:
- (void)objectsDidChangeNotification:(NSNotification*)notification
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// HACK: Give time to Core Data to process pending changes or invalidate caches
NSArray *objects = [self directoriesWithDocuments]; // Returns the directory as expected
}];
}
Experiments
Per #MikePollard's suggestion, I checked the returned value of directoriesWithDocuments in NSManagedObjectContextWillSaveNotification and NSManagedObjectContextDidSaveNotification. The results are (in order):
NSManagedObjectContextObjectsDidChangeNotification: empty (wrong)
NSManagedObjectContextWillSaveNotification: empty (wrong)
NSManagedObjectContextDidSaveNotification: 1 directory (correct)
First of all, it looks like you have a boolean attribute called "deleted" defined in your model.
I recall that doing so can be a significant problem because it conflicts (at a KVC level) with NSManagedObject isDeleted. You might want to change that just to make sure it's not the culprit.
Edit
Thanks for replying. I used deleted as a simple example; it's not the
actual attribute I'm using. Will change it to softDeleted to avoid
confusion
I've updated my suggestion below to match the softDeleted in your example.
That said, I think what's at work here boils down to the question of what constitutes a "change" for includesPendingChanges = YES.
Before the context has completed its save, the only 'change' is to Document entities, not Directory entities.
So when the fetch request includes pending changes, there are no Directory entities with any pending changes so you end up with the previous results.
To test that theory, give this a shot:
[_context.undoManager beginUndoGrouping];
[_context.undoManager setActionName:#"delete"];
document.softDeleted = #(NO);
[document.directory willChangeValueForKey:#"documents"] // any attribute will do really
[document.directory didChangeValueForKey:#"documents"]
NSError *error;
BOOL success = [_context save:&error];
[_context.undoManager endUndoGrouping];
What you're doing with the fake will/did changeValueForKey is to "dirty" the associated Directory object. My guess is that it will then be considered "changed" and, as such, included the fetch results.
It strikes me that the predicate is going to be translated into a SQL statement so until the save hits the DB you're not going to get the result you want ... (Not that'd you'd image that from just reading the NSFetchRequest documentation.)
So try doing the fetch as a result of the NSManagedObjectContextDidSaveNotification.
I bet if you turn on -com.apple.CoreData.SQLDebug 1 you'll see the SQL statement.

Resources