Core Data - detect equal properties in fetched objects - ios

My core data model:
Contact
=======
name
phone number (unique)
I need to display a table view of contacts with the following condition:
if there is more than one contact with the same name - show name+number
otherwise - show only name
Fo for example, if my core data contacts are:
Michael, 11112221
Jon, 33438282
Jon, 72727272
Lisa, 99939393
My table view should present:
Jon (33438282)
Jon (72727272)
Lisa
Michael
Currently I'm using the following NSFetchRequest to show the contact list:
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Contact"];
request.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(caseInsensitiveCompare:)]];
request.fetchBatchSize = 20;
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:#"nameFirstLetter"
cacheName:nil];
I'm looking for the most efficient way to know which name appears more than once so I can show the number next to the name.

If you're using NSSQLiteStoreType as your persistent store you can fetch duplicate items with an NSExpression:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Contact" inManagedObjectContext:self.managedObjectContext];
NSAttributeDescription *nameDesc = [entity.attributesByName objectForKey:#"name"];
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"name"];
NSExpression *countExpression = [NSExpression expressionForFunction:#"count:" arguments:#[keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName: #"count"];
[expressionDescription setExpression: countExpression];
[expressionDescription setExpressionResultType: NSInteger32AttributeType];
NSError *error = nil;
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:#"Contact"];
[fetch setPropertiesToFetch:#[nameDesc, expressionDescription]];
[fetch setPropertiesToGroupBy:#[nameDesc]];
[fetch setResultType:NSDictionaryResultType];
NSArray *results = [self.managedObjectContext executeFetchRequest:fetch error:&error];
NSArray *duplicates = [results filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"count > 1"]];
NSArray *duplicateNames = [duplicates valueForKeyPath:#"name"];
duplicateNames contains the name of contacts which name appears multiple times in the db. So, whenever a contact displayed in the table view, just query the duplicateNames array to check whether the actual contact is a duplicate or not.

The easiest way if you only want to modify the display I would modify tableView:cellForRowAt... to check the name of the person above and below the current one
in basic mock code:
- tableView:cellForRowAtIndexPath:(id)path {
....
Person *prior = ...
Person *next = ...
if(prior.name == current.name || next.name == current.name) {
//show name+number
}
}

Related

How to fetch specific records in Core data

I am a new bid in iOS development. I am using NSManagedObject of Core Data to perform Insert and Fetch operations. It works perfectly fine. But the problem is, I want to fetch only some selected records (where condition in MySQL) from the table.
For e.g. "select name from users where city='Pune'";
I found NSPredicate to fetch filtered data. But it gives all the data in array and not just the selected one. For e.g. if result for above query is:
Anu
Then the NSPredicate result will give:
fname = Anu
lname = Padhye
city = Pune
id = 3
Is there a way to only fetch selected record/s in iOS Objective-c? Following is the code snippet I am using for NSManagedObject:
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"User"];
valueData = [[managedObjectContext executeFetchRequest:fetchRequest error:nil] mutableCopy];
NSEntityDescription *productEntity=[NSEntityDescription entityForName:#"User" inManagedObjectContext:managedObjectContext];
NSFetchRequest *fetch=[[NSFetchRequest alloc] init];
[fetch setEntity:productEntity];
NSPredicate *p=[NSPredicate predicateWithFormat:#"id == %d", 3];
[fetch setPredicate:p];
//... add sorts if you want them
NSError *fetchError;
NSArray *fetchedProducts=[valueData filteredArrayUsingPredicate:p];
Try this:
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"User"];
request.predicate = [NSPredicate predicateWithFormat:#"city == %# && id == %d", #"Pune", 3];
request.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"id" ascending:YES]];
NSArray *results = [managedObjectContext executeFetchRequest:request error:nil];
Results array should now contain all records who have Pune as their city with the id of 3.

Is it possible to use a group by count in the havingPredicate for a CoreData fetch (for dupe detection)?

For reference, the problem I'm trying to solve is efficiently finding and removing duplicates in a table that could have a lot of entries.
The table I am working with is called PersistedDay with a dayString object in it (it's a string. :-P). There are more columns that aren't relevant to this question. I'd like to find any PersistedDay's that have duplicates.
In SQL, this is one of the efficient ways you can do that (FYI, I can do this query on the CoreData backing SQLite DB):
SELECT ZDAYSTRING FROM ZPERSISTEDDAY GROUP BY ZDAYSTRING HAVING COUNT(ZDAYSTRING) > 1;
This returns ONLY the dayStrings that have duplicates and you can then get all of the fields for those objects by querying using the resulting day strings (you can use it as a sub query to do it all in one request).
NSFetchRequest seems to have all of the required pieces to do this too, but it doesn't quite seem to work. Here's what I tried to do:
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"PersistedDay" inManagedObjectContext:context];
[request setEntity:entity];
NSPropertyDescription* dayStringProperty = entity.propertiesByName[#"dayString"];
request.propertiesToFetch = #[dayStringProperty];
request.propertiesToGroupBy = #[dayStringProperty];
request.havingPredicate = [NSPredicate predicateWithFormat: #"dayString.#count > 1"];
request.resultType = NSDictionaryResultType;
NSArray *results = [context executeFetchRequest:request error:NULL];
That doesn't work. :-P If I try that I get an error "Unsupported function expression count:(dayString)" when trying to do the fetch. I don't think the dayString in "dayString.#count" even matters in that code above...but, I put it in for clarity (SQL count just operates on the grouped rows).
So, my question is: is this possible and, if so, what is the syntax to do it? I couldn't find anything in the CoreData docs to indicate how to do this.
I found one similar SO posts that I now unfortunately can't find again that was about running a count in a having clause (I don't think there was a group by). But, the poster gave up and did it a different way after not finding a solution. I'm hoping this is more explicit so maybe someone has an answer. :)
For reference, this is what I am doing for now that DOES work, but requires returning almost all the rows since there are very few duplicates in most cases:
NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"PersistedDay"
inManagedObjectContext:context];
[request setEntity:entity];
NSPropertyDescription* dayStringProperty = entity.propertiesByName[#"dayString"];
// Get the count of dayString...
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: #"dayString"]; // Does not really matter
NSExpression *countExpression = [NSExpression expressionForFunction: #"count:" arguments: [NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName: #"dayStringCount"];
[expressionDescription setExpression: countExpression];
[expressionDescription setExpressionResultType: NSInteger32AttributeType];
request.propertiesToFetch = #[dayStringProperty, expressionDescription];
request.propertiesToGroupBy = #[dayStringProperty];
request.resultType = NSDictionaryResultType;
NSArray *results = [context executeFetchRequest:request error:NULL];
I then have to loop over the result and only return the results that have dayStringCount > 1. Which is what the having clause should do. :-P
NOTE: I know CoreData isn't SQL. :) Just would like to know if I can do the equivalent type of operation with the same efficiency as SQL.
Yes it is possible. You cannot reference count as key path, however you can reference it as variable. Just like in SQL. In my example I have cities created with duplicate names.
let fetchRequest = NSFetchRequest(entityName: "City")
let nameExpr = NSExpression(forKeyPath: "name")
let countExpr = NSExpressionDescription()
let countVariableExpr = NSExpression(forVariable: "count")
countExpr.name = "count"
countExpr.expression = NSExpression(forFunction: "count:", arguments: [ nameExpr ])
countExpr.expressionResultType = .Integer64AttributeType
fetchRequest.resultType = .DictionaryResultType
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "name", ascending: true) ]
fetchRequest.propertiesToGroupBy = [ cityEntity.propertiesByName["name"]! ]
fetchRequest.propertiesToFetch = [ cityEntity.propertiesByName["name"]!, countExpr ]
// filter out group result and return only groups that have duplicates
fetchRequest.havingPredicate = NSPredicate(format: "%# > 1", countVariableExpr)
Complete playground file at:
https://gist.github.com/pronebird/cca9777af004e9c91f9cd36c23cc821c
Best I can come up with is:
NSError* error;
NSManagedObjectContext* context = self.managedObjectContext;
NSEntityDescription* entity = [NSEntityDescription entityForName:#"Event" inManagedObjectContext:context];
// Construct a count group field
NSExpressionDescription* count = [NSExpressionDescription new];
count.name = #"count";
count.expression = [NSExpression expressionWithFormat:#"count:(value)"];
count.expressionResultType = NSInteger64AttributeType;
// Get list of all "value" fields (only)
NSPropertyDescription* value = [entity propertiesByName][#"value"];
NSFetchRequest* request = [[NSFetchRequest alloc] initWithEntityName:#"Event"];
request.propertiesToFetch = #[ value, count];
request.propertiesToGroupBy = #[ value ];
request.resultType = NSDictionaryResultType;
NSArray* values = [context executeFetchRequest:request error:&error];
// Filter count > 1
values = [values filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"count > 1"]];
// slice to get just the values
values = [values valueForKeyPath:#"value"];
But that's not really much different from what you're using.
The best way finding duplicates in Core Data depends on your data. According to Efficiently Importing Data and assuming that you have to import less than 1000 PersistedDays, I suggest this solution:
NSFetchRequest* fetchRequest = [NSFetchRequest new];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"PersistedDay" inManagedObjectContext:myMOC]];
[fetchRequest setSortDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"dayString" ascending:NO]]];
NSArray* persistedDays = [myMOC executeFetchRequest:fetchRequest error:nil];
for (NSUInteger i = persistedDays.count - 1; i > 0; --i) {
PersistedDay *currentDay = persistedDays[i];
PersistedDay *nextDay = persistedDays[i-1];
if ([currentDay.dayString isEqualToString:nextDay.dayString]) {
/* Do stuff/delete with currentDay */
}
}
For speed up can index dayString in Core Data.
You also can reduce the the data set if you remember a timestamp or a date of the last duplicate clean up:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"importDate > %#", lastDuplicateCleanUp];

Retrieving count of a core data relation

I searched high and low but I couldn't find exactly what I was looking for. My question is similar to this, but slightly different:
Core Data - Count of Related Records
Let's say I have a Car entity which has a one to many relation with a Person entity. This means that the car could have multiple people driving it, but each person drives only one car.
I want to be able to execute only one predicate wherein I could achieve the following:
All cars which are 'red'.
Return only the 'Year' and 'Color' attributes of the matching car.
Return a count of how many people are driving this car (i.e the size of the NSSet of People inside each resulting Car).
Is it possible to do all this with one query?
I know how to do this with multiple queries. I would just use setPropertiesToFetch and use a filtered predicate to achieve 1 and 2 above. I would then perform another count query (countForFetchRequest) on the Persons entity for every car to find how many Person(s) drive each car.
The key is the 3rd requirement above. I want to do everything in one predicate and I don't want to bring all of the Person entity objects into memory (performance) on the initial query. Furthermore it hurts to call another countForFetchRequest query for each car.
What's the best way to do this?
Thanks!
I cannot test this at the moment, but that should be possible by adding the following expression description to the "properties to fetch":
NSExpression *countExpression = [NSExpression expressionForFunction: #"count:" arguments: [NSArray arrayWithObject:[NSExpression expressionForKeyPath: #"drivers"]]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName: #"driversCount"];
[expressionDescription setExpression: countExpression];
[expressionDescription setExpressionResultType: NSInteger32AttributeType];
Return only 'red' cars:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"color LIKE 'red'"];
Return a count of how many people are driving this car:
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"people"];
NSExpression *countExpression = [NSExpression expressionForFunction:#"count:"
arguments:#[keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:#"count"];
[expressionDescription setExpression:countExpression];
[expressionDescription setExpressionResultType:NSInteger32AttributeType];
Return only the 'year' and 'color' attributes (and the count):
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Car"
inManagedObjectContext:context];
NSDictionary *attributes = [entity attributesByName];
NSArray *properties = #[expressionDescription, attributes[#"year"], attributes[#"color"]];
Build and execute the fetch request:
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
[request setResultType:NSDictionaryResultType];
[request setPropertiesToFetch:properties]; // return only count, year & color
[request setPredicate:predicate]; // return only red cars
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
Process the results:
if (results) {
for (NSDictionary *result in results) {
NSLog(#"Year: %#", result[#"year"]);
NSLog(#"Color: %#", result[#"color"]);
NSLog(#"Drivers: %#", result[#"count"]);
}
}
else {
NSLog(#"Error: %#", error);
}

how to group by day with core data?

I have a Entity called deal and deal has a property called date which is the time this deal object inserted into the store.
and one day may have several deals.
So I want count some data group by day, I want fetch dayand countofsomething
like:
2013-06-03 3
2013-06-02 4
and I don't want to use sectionPath because it only put deals into section.
I know I can have this done by have another property(type:string) like dayOfTheDate which is like 2013-06-03 in each object.
btw, transient property don't seem to work in this situation
Could you understand what I am looking for?
Comment here so I can provide more detail
Thanks all of you.
Sample of how I did it when I counted number of same notes
NSEntityDescription* entity = [NSEntityDescription entityForName:#"Assets"
inManagedObjectContext:[appDelegate managedObjectContext]];
NSAttributeDescription* statusDesc = [entity.attributesByName objectForKey:#"notes"];
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: #"assetUrl"]; // Does not really matter
NSExpression *countExpression = [NSExpression expressionForFunction: #"count:"
arguments: [NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName: #"count"];
[expressionDescription setExpression: countExpression];
[expressionDescription setExpressionResultType: NSInteger32AttributeType];
[searchFetchRequest setPropertiesToFetch:[NSArray arrayWithObjects:statusDesc,expressionDescription, nil]];
[searchFetchRequest setPropertiesToGroupBy:[NSArray arrayWithObject:statusDesc]];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"timestamp" ascending:NO];
[searchFetchRequest setSortDescriptors:#[sortDescriptor]];
[searchFetchRequest setFetchLimit:10];
NSPredicate *query = [NSPredicate predicateWithFormat:#"notes contains[cd] %#",_txtCameraNote.text];
[searchFetchRequest setPredicate:query];
[searchFetchRequest setResultType:NSDictionaryResultType];
NSArray *fetchedObjects = [appContext executeFetchRequest:searchFetchRequest error:nil];
fetchedObjects would be something like this.
({
count = 1;
notes = "glenny and me";
},
{
count = 6;
notes = macair;
})

Getting a maximum value from a core data relationship

I search the net for answers to this problem, but unfortunately I don't think I am fluent enough in core data procedures to actually include the right combo of keywords.
I have two entities... Users and Bookmarks with a one-to-many relationship.
Users : string:firstName, string:lastname, string:iconImage
with a relationship to Bookmarks - inverse
Bookmarks : string:title, string:url, string:content, image:Binary Data, order:Integer32
with a relationship to Users - inverse
My goal here is to query the bookmarks for a particular user and find the highest number for the key:order, so that when I add an new bookmark, it will be one larger than the max. I have seen the Apple example, which works and makes sense, but I need a little more. The example returns the maximum value for all records in that entity.
-(NSNumber*) getNextBookmarksOrderForUser:(NSManagedObjectID*)userID
{
NSNumber *highOrder;
Users *user =[self getUserByID:userID];
if (user)
{
NSExpression *keyPathExpression = [NSExpression expressionForKeyPath:#"order"];
NSExpression *highestOrderingNumber = [NSExpression expressionForFunction:#"max:" arguments:[NSArray arrayWithObject:keyPathExpression]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc]init];
[expressionDescription setName:#"maxOrdering"];
[expressionDescription setExpression:highestOrderingNumber];
[expressionDescription setExpressionResultType:NSDecimalAttributeType];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Bookmarks" inManagedObjectContext:[_dataContext managedObjectContext]];
NSFetchRequest *request = [[NSFetchRequest alloc]init];
[request setEntity:entity];
[request setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
[request setResultType:NSDictionaryResultType];
NSError *error = nil;
NSArray *objects = [[_dataContext managedObjectContext] executeFetchRequest:request error:&error];
if(objects == nil) {
// Handle the error
}
else {
if ([objects count] > 0) {
highOrder = [NSNumber numberWithInt:[[[objects objectAtIndex:0] valueForKey:#"maxOrdering"]intValue]];
NSLog(#"Highest ordering number: %#", highOrder);
}
}
return highOrder;
}
return nil;
}
So if I have two users, one with 10 bookmark and the other with 25, the above code will always return 25. I tried adding a predicate:
NSPredicate *byUser = [NSPredicate predicateWithFormat:#"self == %#", user];
[request setPredicate:byUser];
Does anyone have any suggestions? I Know that I could return all bookmark for a user and the write the code to find my value, but I would like to do it the most efficient way.
Thank you for any help.
From my comment
I think you should use a predicate like this [NSPredicate predicateWithFormat:#"users == %#", user]; Since you are querying against Bookmarks.

Resources