CoreData NSPredicate with two one-to-many relations - ios

I have a CoreData model data structure like this for restricting information based on the group a user belongs to:
Category <->> Information <->> Groups.
I have an NSSet of UserGroups objects. I want to be able to filter categories based on the NSSet of Group objects, such that if a category does not contain any pieces of information which have any groups inside my NSSet, they will not be returned by my predicate.
For information I can do
[NSPredicate predicateWithFormat:#"ANY groups in (%#)",groups];
For categories I've tried the following with only a crash:
[NSPredicate predicateWithFormat:#"ANY information.groups in (%#)",groups];
But I need to write a predicate at the Category level. I am programming under the assumption that the information in my data set is sufficiently large that I cannot pull them all of them out and process them to find my Categories. I want to create predicate that will fetch only the categories that are relevant to the user based on his/her groups.
Thanks for your help!

The following predicate on Category should work (assuming that information is the to-many relationship from Category to Information):
[NSPredicate predicateWithFormat:#"SUBQUERY(information, $i, ANY $i.groups in %#).#count > 0",groups];
Alternatively, you could use the inverse relationships:
// your set of Group objects:
NSSet *groups = ...;
// all Information objects that are related to any group object:
NSSet *information = [groups valueForKey:#"information"] ;
// all Category objects that are related to any information object:
NSSet *categories = [information valueForKey:#"category"];
which can be combined to
NSSet *categories = [groups valueForKeyPath:#"information.category"];
A disadvantage of this alternative solution might be that it creates also the intermediate set of groups in memory.

Related

Best practice for preloading core data with many to many relationship

I am developing an IOS application which has core data and I have to preload data many to many relationship to the core data. I have three tables with supermarket, product and intermediate table called supermarketToproduct which stores the relationship between supermarkets to products. As I stated, there is many to many relationship between supermarket and product entities hence I needed to create an intermediate table to indicate the relationships.
My question is that what is the best practice to preload JSON formatted data into my core data with many to many relationship. In Apples reference documents, it is stated that there is no need to create intermediate table as coredata creates one for us. However, How can I define many to many relationship while preloading the date without intermediate table?
By the way, all the data is static so no need to insert new data into tables, I only need fetch the data and its related supermarket or product.
Please note that joined table has two independent attributes called priceofProductforGivenMarket and primaryProductforGivenMarket
Here is the JSON representation of my data, also it is the model that I would create in core data:
Supermarket:
name
location
phone number
Product:
name
price
company
supermarketToproduct:
nameofsupermarket
nameofproduct
priceofProductforGivenMarket
primaryProductforGivenMarket
I would create three entities, Supermarket, Product and SupermaketProductDetails, with attributes and relationships as follows:
Supermarket:
Attributes: name, location, phone number
Relationship (to many): productDetails
Product:
Attributes: name, price, company
Relationship (to many): supermarketDetails
SupermarketProductDetails:
Attributes: priceofProductforGivenMarket, primaryProductforGivenMarket
Relationships: (to one) supermarket, (to one) product
In Xcode, indicate that the "destination" for the productDetails relationship in the Supermarket entity is the SupermarketProductDetails entity, with inverse supermarket, likewise set the SupermarketProductDetails entity as the destination for the supermarketDetails in the Product entity, with inverse product.
Then I would parse your JSON data for supermarkets first, creating Supermarket objects and setting the name, location and phone number but without entering anything for the products relationship. Likewise, parse the products JSON and create all the Products objects. Then I would parse the join table, and create the SupermarketProductDetails objects. Set the attributes based on your JSON data, and execute a fetch to get the Supermarket entity with the right name, likewise fetch the Product entity with the right name, and then set these relationships directly.
EDIT:
Assume that you parse each line in your join table into four NSStrings: nameofsupermarket, nameofproduct, priceofProductforGivenMarket and primaryProductforGivenMarket. Then for each line...
// First fetch the correct Supermarket...
NSFetchRequest *supermarketFetch = [NSFetchRequest fetchRequestWithEntityName:#"Supermarket"]
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name like %#",nameofsupermarket];
supermarketFetch.predicate = predicate;
NSError *error;
NSArray *results = [context executeFetchRequest:supermarketFetch error:&error];
// should really check for errors, and that we get one and only one supermarket
NSLog(#"Supermarket fetch returned %i results",[results count]); // should be only 1!
mySupermarket = (NSManagedObject *)[results firstObject];
if (![[mySupermarket valueForKey:#"name"] isEqualToString:nameofsupermarket]) {
NSLog(#"Wrong supermarket fetched");
}
// Now fetch the product...
NSFetchRequest *productFetch = [NSFetchRequest fetchRequestWithEntityName:#"Product"]
predicate = [NSPredicate predicateWithFormat:#"name like %#",nameofproduct];
productFetch.predicate = predicate;
results = [context executeFetchRequest:productFetch error:&error];
// should really check for errors, and that we get one and only one product
NSLog(#"Product fetch returned %i results",[results count]); // should be only 1!
myProduct = (NSManagedObject *)[results firstObject];
if (![[myProduct valueForKey:#"name"] isEqualToString:nameofproduct]) {
NSLog(#"Wrong product fetched");
}
// Now create the SupermarketProductDetails entity:
NSManagedObject *mySupermarketProductDetails = [NSEntityDescription insertNewObjectForEntityForName:#"SupermarketProductDetails" inManagedObjectContext:context];
// set attributes...
[mySupermarketProductDetails setValue:primaryProductForGivenMarket forKey:#"primaryProductForGivenMarket"];
[mySupermarketProductDetails setValue:priceOfProductForGivenMarket forKey:#"priceOfProductForGivenMarket"];
// set relationships...
[mySupermarketProductDetails setValue:mySupermarket forKey:#"supermarket"];
[mySupermarketProductDetails setValue:myProduct forKey:#"product"];
[context save:&error];
// should check for errors...
Note that you only need to set one side of these relationships - CoreData updates the other side for you (i.e. it will add mySupermarketDetails to the set of supermarketDetails for myProduct, etc). Note also that the "value" for a (to-one) relationship (e.g. supermarket) is the destination object itself (e.g. mySupermarket); you don't use the name or any other key. Coredata is (hidden in the underlying sql tables) using unique objectIDs to do the linking up.
(There are probably more efficient means of doing this, rather than doing two fetches for every SupermarketProductDetails entry, but this will work.)
EDIT2: Note that the above assumes that your entities are all implemented as NSManagedObjects. If you have created separate subclasses for each entity, then you can simplify some of the above code. For example, valueForKey: and setValue:forKey: can be replaced by the equivalent property accessor methods using dot notation, eg.:
[mySupermarketProductDetails setValue:primaryProductForGivenMarket forKey:#"primaryProductForGivenMarket"];
would become:
mySupermarketProductDetails.primaryProductForGivenMarket = primaryProductForGivenMarket;
and
[mySupermarket valueForKey:#"name"]
can be replaced by:
mySupermarket.name
Likewise, objects should be created with the appropriate subclass rather than NSManagedObject. eg.
NSManagedObject *mySupermarketProductDetails = [NSEntityDescription insertNewObjectForEntityForName:#"SupermarketProductDetails" inManagedObjectContext:context];
would become
SupermarketProductDetails *mySupermarketProductDetails = [NSEntityDescription insertNewObjectForEntityForName:#"SupermarketProductDetails" inManagedObjectContext:context];
I think the above approach would be hard on prefetching and would prefetch almost all matching entries and the real motive of prefetch would be gone.
also I would suggest to add topProduct in Supermarket table.
You must define many-to-many relationships in both directions—that is, you must specify two relationships, each being the inverse of the other
Edit 01:
Product (Attributes):
name
price
company
Supermarket (Attributes):
name
location
phone number
supermarket top Product
Once you create Attributes like above and add the many to many relationship.
Coredata will add below to the respective entity for relationships.
**NSSet products, [supermarket table] and NSSet supermarkets [product table]**
So now you can actually update this relationship after inserting the data as bellow:
i.e.
Insert
// Create Product
NSManagedObject *product = [[NSManagedObject alloc] initWithEntity:#"Product" insertIntoManagedObjectContext:self.managedObjectContext];
// Set details
[product setValue:#"xxx" forKey:#"name"];
...
// Create Supermarket
NSManagedObject *supermarket = [[NSManagedObject alloc] initWithEntity:#"SuperMarket" insertIntoManagedObjectContext:self.managedObjectContext];
// Set First and Last Name
[supermarket setValue:#"Main Street" forKey:#"name"];
...
// Create Relationship
[supermarket setValue:[NSSet setWithObject:product] forKey:#"products"];
// Save Managed Object Context
NSError *error = nil;
if (![supermarket.managedObjectContext save:&error]) {
NSLog(#"Unable to save managed object context.");
NSLog(#"%#, %#", error, error.localizedDescription);
}
//And similarly inverse relationship.
Fetching
also now since you have related information about product in Supermarket table and vice versa, the fetching will be smoothing and as discussed before, will not prefetch all data upfront which it would have done otherwise. As there is no need to create further relationship by coreData fetch.
Please follow for more details: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdRelationships.html#//apple_ref/doc/uid/TP40001857-SW10
Also Please follow this:
Core Data sectionNameKeyPath with Relationship Attribute Performance Issue

Does Core Data autoupdate a many to many relationship on saving

I have 2 database tables in Sqlite and using Core Data / NSManagedObjects:
Users
user_id
name
belongsToGroups (many groups)
Groups
group_id
title
groupMembers (many users)
By using core data to save a Group and add users (and add him to a group) like so:
Groups *coreData = nil;
coreData = [NSEntityDescription insertNewObjectForEntityForName:#"Groups" inManagedObjectContext:context];
request = [NSFetchRequest fetchRequestWithEntityName:#"Users"];
request.predicate = [NSPredicate predicateWithFormat:#"user_id = %#", user_id];
results = [context executeFetchRequest:request error:&error];
coreData.group_id = group_id
coreData.title = title
[coreData addGroupMembersObject:[results lastObject]];
Is it correct to say that core data manages the Users to many groups and groups with many users with the above code?
Yes, if the relationships are set-up as inverse relationships of each other.
Modifying one relationship automatically updates the inverse relationship.
Remark: I would call the entities "User" and "Group", because each object of the
entity represents a single user or group. I would also (but that might be a matter
of taste) call the relationships just "groups" and "users".

NSPredicate to get children's children's

I want to fetch all the treatments, that are children of this List object.
I am not sure if it can be done.
I have a TableView showing all the patients of a list. Each patient can have many treatments.
I have a reference to the current list, and want to use that to somehow find the childrens children.
Thank you
Use a fetch request for the "Treatment" entity with the predicate
[NSPredicate predicateWithFormat:#"ofPatient.ofList = %#", currentList]
using the inverse relationships from "Treatment" -> "Patient" -> "List".

How to fetch the NSSet of all attributes of a one-to-many relationship B of all objects A

Suppose I have an entity A with thousands of objects. Each of these objects have a one-to-many relationship to another entity B. One attribute of entity B is "section".
I want the NSSet of all distinct sections of B amongst all objects of entity A.
Were I to only want the distinct sections of B for ONE object of A, I would use:
[A valueForKeyPath:#"B.section"];
But is there an efficient way to get this for ALL objects of A? It seems to me that fetching all objects of A and then iterating over this array, performing the above is 'too much work.' Sure it can be done without much trouble, but I am hoping that my core data naivety simply means that I don't know the trick to accomplish this in one line.
How can this be done?
Thanks!
Edit for requested clarification:
Suppose a Person Entity has a to-many relationship with a Vehicles Entity.
Suppose the Vehicles entity has an attribute 'color'.
I want an NSSet of all colors for all vehicles belonging to all people.
If I'm understanding correctly, and assuming that the A-->B relationship has an inverse B-->A relationship named a, your question could be phrased as: All distinct values of B.section where the relationship B.a is not nil. That would get each distinct value of B.section for every B that's associated with some A.
In that case you want something like:
// Fetch B
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"B"];
// Fetch B that's related to an A
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"a != nil"];
[request setPredicate:predicate];
// Fetch distinct values of "section", return values as NSDictionary
[request setResultType:NSDictionaryResultType];
[request setReturnsDistinctValues:YES];
[request setPropertiesToFetch:#["section"]];
Execute that and you get an array of NSDictionary. The dictionaries have a key named section which correspond to B.section. Only distinct values are included.
Assuming you have the following managed object class:
#interface A: NSManagedObject
#property (nonatomic, retain) NSSet *BSet;
#end
If you have an instance of A called aObj and you want to grab all the objects in BSet with unique sections, you would do the following:
NSArray *uniqueBSections =
[aObj valueForKeyPath:#"#distinctUnionOfObjects.BSet.section"];

Core Data with to Many relationship - Creating a NSPredicate with ALL in SUBQUERY

My object graph looks like this
SnapShot -->> Pane --> ManagedImage
I'm trying to find a SnapShot that has the exact ManagedImages contained with in a set.
The code I've got now returns an Array of SnapShots that have one or more of the ManagedImages that are in the set. I then search through the Array to find the correct SnapShot but I'm guessing it would be much faster to filter in the Subquery
With an NSPredicate how can I get the unique SnapShot that has ALL of the ManagedImages that are in the set?
Here's my code
mySet = ... // A unique set of (usually 3) managedImages that I'm trying to find a snapShot for
NSFetchRequest *request = ...
request.entity = [NSEntityDescription entityForName:#"SnapShot" inManagedObjectContext:[self managedObjectContext]];
// Want this to work but sends an exception
//request.predicate = [NSPredicate predicateWithFormat:#"SUBQUERY(self.panes, $pane, ALL $pane.managedImage IN %#).#count != 0", mySet];
// Using this
request.predicate = [NSPredicate predicateWithFormat:#"SUBQUERY(self.panes, $pane, $pane.managedImage IN %#).#count != 0", mySet];
A good rule of thumb is that if you already have managed objects in hand, you don't fetch but instead walk the relationships from the managed objects you have to the managed objects you want.
So, your relationship graph probably actually looks like this:
SnapShot <-->> Pane <--> ManagedImage
or maybe:
SnapShot <<-->> Pane <<--> ManagedImage
Since you have a set of ManagedImage objects all you have to do is walk the keypath of pane.snapShot or panes.snapShots to find the SnapShot objects associated with each ManagedImage object. Then you just extract the unique SnapShot objects.
In the first case, the matter is trivial because of the one-to-one relationship path of
ManagedImage-->Pane-->SnapShot
In the second case, you will need to first get all the unique SnapShot objects:
NSSet *shots=[aMangedImageObj valueForKeyPath:#"distinctUnionOfSets.panes.snapShots"];
... for each ManagedImage instances and then merge all the sets with setByAddingObjectsFromSet: or a similar method to produce a single set of unique objects.
Fetches should be used to find the first objects in graph that you need but once you have the objects, you don't fetch but walk the relationships. Otherwise, there is not much point to having relationships in the first place.

Resources