NSFetchedResultsController with data from other model - ios

I'm trying to build app with UITableView with data from NSFetchedResultsController. The problem is - I need to show some additional data from other models in table view.
Assume I have 2 models: Students and Groups (I think the relation is obvious: group consists of many students and student can be only in one group). I'm trying to build UITableView with list of groups. I would also like number of students in each group.
(NSFetchedResultsController *) fetchController {
if (!_fetchController) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"GroupModel" inManagedObjectContext:[NSManagedObjectContext MR_defaultContext]];
[fetchRequest setEntity:entity];
NSPredicate *messageInChatPredicate = [GroupModel getMyChatsPredicate];
[fetchRequest setPredicate:messageInChatPredicate];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
_fetchController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[NSManagedObjectContext MR_defaultContext] sectionNameKeyPath:nil cacheName:#"main"];
_fetchController.delegate = self;
}
return _fetchController;
}
I wonder how I can I add additional information (such as number of students in each group)?

Why not just use group.students.count to get the value from the group entity? You only need one fetchedResultsController and I think you will find Core Data performance is fine.
Your terminology is a bit confusing, usually a model is a core data model created in Xcode and has a number of entities e.g. Group, Student. Sometimes you can use two different models and/or different versions of the models.
Your objects are usually know as Entities and both would belong to the same Core Data model.
EDIT:
To get the count of subgroups, such as girls or boys you can do the following
NSMutableArray *boys = [[NSMutableArray alloc] init];
[NSPredicate predicateWithFormat:#"gender = %#",#"MALE"];
[boys addObjectsFromArray:[group.students filteredArrayUsingPredicate:predicate]];
NSLog(#" boys count is %d", boys.count);
Assuming of course that gender is an attribute of Student.

Create another NSFetchedResultsController that is responsible for the other model (i.e student).

You have basically two options to get additional information such as the number of students in each group.
The first option is to count the students in the Group-Student relation. To serve this request the NSManagedObjectContext will load all students in memory to get a suitable result. Personally, I don't really like this option.
The second option is to store the number of students who belong to a group in a property of the group so that it can be directly add. Yes, you have to maintain the correct number manually. Depending on the amount of data that has to be loaded, this approach is more preferable. Crucially since it's way fas

Related

NSFetchRequest GroupBy an attribute of to-many relationship

I have been persisting some records in core data.
Records have to-many relationship.
searching the persist data by to-many relationship seems to be simple and I used "SUBQUERY" to achieve it. I am facing problem with grouping records.
I need to group "ZCMORecord" by stringValue of "ZCMORecordValue"
Since "ZCMORecordValue" is a to-many relationship.
I even need to group ZCMORecords with one or more "ZCMORecordValue".
NSManagedObjectContext *context= [[[UIApplication sharedApplication]delegate ]managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *valueDesc = [NSEntityDescription entityForName:#"ZCMORecord" inManagedObjectContext:context];
[fetchRequest setResultType:NSDictionaryResultType];
[fetchRequest setFetchLimit:PAGE_SIZE];
NSPropertyDescription *recordRelationShip = [fetchRequest.entity.relationshipsByName objectForKey:#"recordValueSet.stringValue"];
[fetchRequest setPropertiesToGroupBy:[NSArray arrayWithObjects:recordRelationShip,nil]];
NSError *error;
id fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
Getting errors when I try this.
SELECT clauses in queries with GROUP BY components can only contain
properties named in the GROUP BY or aggregate functions
I need to fetch records in group.
I could't figure out what to set in "setPropertiesToGroupBy:" and "setHavingPredicate:" to achieve proper results.
OKAY!! Example of how I Store my data
ZCMORecordValues
id stringValue<NSString> dateValue<NSDate> record
name Alex - record1<ZCMORecord>
DOB - 10/10/1990 record1<ZCMORecord>
name Anto - record2<ZCMORecord>
DOB - 05/05/1990 record2<ZCMORecord>
name Max - record3<ZCMORecord>
DOB - 10/10/1990 record3<ZCMORecord>
name Mary - record4<ZCMORecord>
DOB - 01/01/1990 record4<ZCMORecord>
Now I want to group my "ZCMORecord" with with respect to DOB.
Any suggestion on changing the model to facilitate grouping is also welcome
The easiest way to do this is to use a NSFetchedResultsController, fetch the record values and pass the stringValue as the sectionNameKeyPath.

Duplicate Entries for A Core Data Table View Controller and How To Handle This With NSFetchedResultsController

I have a simple application with the following premise and a few minor issues:
I have 4 Tabs, each with a table view displaying different information and each with a plus button going modally to the same view controller, allowing users to add in information to the app.
The user adds a Name, Title, Date and Amount in the view controller and when they press save, it gets saved to the Core Data table view.
The first tab displays ALL for bits of information above (Name, Title, Date, Amount) in a custom table cell with labels, etc. That works well.
The second tab displays only information on the Name, the third tab displays only the Titles. This way, a user sees everything in the first Tab, but only the "Names" in the second tab which they can go select to gather information about that name.
The app is working well when there are only one entry per name (or date or title), but the moment I add a second entry, it duplicates in the Name tab.
For example, if I have an entry for John Smith with only a single entry, it will display that in the Name tab with one entry and when I go in, it shows me only transactions that have John's name attached to it (1 in this case). All good.
However, if I add another Entry for John (spelt exactly the same), it treats it as a separate entry and suddenly, the Name tab now has 2 John's.
I have not put any unique IDs in the attributes of the Core Data Model, but do I have to do this, or could I just use a predicate to ask "if this already exists, don't display it twice" in the Name Tab Bar?
Any assistance would be massively appreciated!
Edit:
Here is the fetchRequest for the Name tab:
- (NSFetchedResultsController *)fetchedResultsController
{
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Transaction" inManagedObjectContext:managedObjectContext];
fetchRequest.entity = entity;
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"whoBy.name" ascending:NO];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
As we can see, there are no predicates or anything similar. What I want is for a simple check to see "if name exists, don't add it twice" to the Name table view. I hope this makes sense?
The entry duplicates because in the Names tab you're querying the Transactions table and then getting the name of the person who did that transaction. Because John has two transactions, you get his name twice.
Try something like this:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:managedObjectContext];
fetchRequest.entity = entity;
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
Like this you should get every name (person) only once (assuming the relationship between Names and Transactions is one-to-many).
EDIT: Every time you create a Transaction, there are two things that can happen with the Person:
it can be a new Person, and you should create a new entity; or,
it is a new Person.
The logic is like this, and you'll have to figure out the code:
When the user is adding a Transaction, you'll need to check if it's for a new person or an existing one — if it's an existing one, maybe he can choose it from a list of Persons (and when he selects a person, you get its NSManagedObjectID). If it's a new one, you create it on the spot.
After creating the Transaction object, you need to add the Person object to its relationship. If I saw correctly, you have a whoBy relationship in the Transaction object. Set it to the Person object you got in step 1.
Set all the other fields of the Transaction object (amount, etc.).
Save the NSManagedObjectContext when you're done with all that.
What you're doing in your code at the moment, is create a new Person for every Transaction, which is why you see multiple entries of the same name.
Yes you can tell CoreData that you want only distinct values by setting:
NSFetchRequest *request;
request.propertiesToFetch = [NSArray arrayWithObject:#"Name"];
request.returnsDistinctResults = YES;
request.resultType = NSDictionaryResultType;
...
But if you don't want 2 Johns in database then it is better practice that you don't put them there than to filter results. So when you insert data check if you already have that entry and If you do don't insert it (you can just update that record)...

Core Data - Select distinct

I know there have been several discussions about this but none of them resolved my simple problem.
I have an Entity called Character and inside there are 4 columns:
character_id, episode_id, title, desc
there can be several same character_ids values but with different episode_id.
When I perform fetch\select I do it for whole table and wishes to get it distinctly by character_id. so this is what I do:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:moc];
[fetchRequest setEntity:entity];
// Add a sort descriptor. Mandatory.
if(sortDescriptors != nil) {
[fetchRequest setSortDescriptors:sortDescriptors];
}
fetchRequest.predicate = predicate;
// Required! Unless you set the resultType to NSDictionaryResultType, distinct can't work.
// All objects in the backing store are implicitly distinct, but two dictionaries can be duplicates.
// Since you only want distinct names, only ask for the 'name' property.
fetchRequest.resultType = NSDictionaryResultType;
fetchRequest.propertiesToFetch = [NSArray arrayWithObject:[[entity propertiesByName] objectForKey:#"title"]];
fetchRequest.returnsDistinctResults = YES;
NSArray *fetchResults = [moc executeFetchRequest:fetchRequest error:&error];
The 'fetchResults' array contains 3 out of 10 rows which is the right result!
The problem: None of the object within the array is accessible.
If I try the following:
NSDictionary item1 = [fetchResults objectAtIndex:0];
NSString *title = [item1 objectForKey:#title"];
I get an exception!
What am I doing wrong?? how can I translate back the dictionary into NSManagedObjects??
Thank you!
First, when using Core Data you should not use foreign keys. Rather, it is preferable to use Core Data's relationships and let the framework deal with the ids in an opaque manner. Maybe you are synching with a web service and need to keep track of the ids but your program logic actually should not depend on it.
Second, if you need an object, it is really better to use the NSManagedObjectResultType rather than the NSDictionaryResultType. You can still obtain distinct results. If you are not experiencing performance issues, this is the preferred pattern. The code is also much more readable.
Your data structure would be this, with a many-to-many relationship:
Character <<--->> Episode
All characters of an episode or all episodes with a certain character is simple. These will be "distinct" results dictated by the logic of the data model:
NSArray *allCharactersInEpisode = episode.characters;
NSArray *allEpisodesWithCharacter = character.episodes;
To select all characters of all episodes you just select all characters. Much simpler than a "distinct" query.

How To Implement Sections with NSFetchedResultsController

I've done some reading up on multiple similar issues and sites and I just cannot make any understanding of this.
I have a NSFetchedResultsController which displays information from a user adding information into a ModalViewController with text fields. The user is presented with a ModalView and they put in some words into the fields, click save and that makes up the single sectioned row of tables in the TableView, which implements the NSFetchedResultsController protocol.
What I would like to do now is:
On one of the text fields, be able to create a new section out of the information provided in that one textField.
My fetchrequest is:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Transaction" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"whoBy.name" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:nil];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
The attribute I want to do display is occasion.date, which means it is a relationship from the Transaction entity to the Occasion Entity (occasion) and the createdDate is an attribute on the Occasion entity.
I can see the sectionNameKeyPath:nil needs to be updated, but what do I update it to, and also do I have to change any of the datasource methods, titles, etc?
Any help on this would be massively appreciated.
Thanks,
The precise answer depends on what should be displayed as section headers and how the
sections should be sorted.
But generally, you have to add a property to the entity, which is then used as sectionNameKeyPath parameter. In addition, you have to add a first sort descriptor
using the same key (this condition can be relaxed slightly).
For example, if you simply add a string attribute sectionName to the entity and use
that as sectionNameKeyPath and as the first sort descriptor key, then all objects
with the same value for sectionName will be grouped into one section,
and the sections will be sorted by sectionName.
The remaining
sort descriptors are used to sort the objects within each section.
Another example would be to define a separate entity "Section" with a string attribute
"title" and an integer attribute "index", add a to-one relationship "section" from
"Transaction" to "Section", and use section.index as sectionNameKeyPath.
Again, all objects with the same section are grouped together, but the sections are
now sorted by the value of section.index, and you can modify
tableView:titleForHeaderInSection: so that the title is displayed in the section
header instead of the index.
This is quite general, but I hope that it gets you started.

Value of sectionNameKeyPath for many-to-many relationship in Core Data

I have a Core Data model with three entities: Notification, Group and Customer. These are the relationships between them:
A customer belongs to many groups and a group can have many customers.
A notification is sent (belongs) to a group and a group can receive (have) many notifications.
I would like to show all notifications in a UITableView grouped by customer. I have created a NSFetchedResultsController like this:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.fetchBatchSize = 10;
fetchRequest.predicate = nil;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Notification"
inManagedObjectContext:self.managedObjectContext];
fetchRequest.entity = entity;
// Default sort descriptors are built in a separate custom method
NSArray *sortDescriptors = [self getDefaultSortDescriptorsForEntity:entity];
fetchRequest.sortDescriptors = sortDescriptors;
return [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"group.customers.firstName"
cacheName:nil];
Assuming this is a valid way to retrieve all notifications grouped by customer (I'm not sure about that either) iOS is throwing the following exception:
#"Failed to fetch all Notification objects"
#"Reason: Invalid to many relationship in setPropertiesToFetch: (group.customers.firstName) (NSInvalidArgumentException)"
I have reviewed the relationships again and again to see if something is missing and everything seem to be correct. I can create and delete objects for all entities and links between them are correct as well.
My question is: Is it possible to traverse through several relationships in a sectionNameKeyPath value? How should many-to-many relationships be handled in this kind of scenario?
Yes, you can do this. Just fetch the customers with the FRC and set the sectionNameKeyPath to nil.
Sections
The number of returned results is your number of sections. Populate the section headers with the customer data.
Rows
The number of rows in section would be customer.notifications.count. To populate the row, make sure the notifications are sorted in some way (say, by date) and display them accordingly with something like this:
NSArray *orderedNotifications =
[customerForSection.notifications sortedArrayUsingDescriptors:
#[[NSSortDescriptor sortDescriptorWithKey:#"date" ascending:NO]]];
Notification *notificationToBeDisplayed =
[orderedNotifications objectAtIndex:indexPath.row];
An alternative - recommended - solution is to change the data model. You could associate the notification with all customers directly. That would have the added advantage, that notifications remain associated with the correct customers even if group memberships change.

Resources