Today I've faced a very strange behavior of NSFetchedResultsController which I use to fill UITableView with Core Data records.
I have two entities in Core Data model: Parent and Child. Each entity has Int16 attribute typeNumber and string attribute name. Parent has to-many relationship to Child called children.
NSFetchRequest for NSFetchedResultsController selects Child entities sorted by name with predicate [NSPredicate predicateWithFormat:#"parent.typeNumber == 1"]. Looks very simple.
Before NSFetchedResultsController performs fetch there is a following structure in CoreData: one Parent with typeNumber = 1 named parent1 with two children: child1 (name) with typeNumber = 1 and child2 with typeNumber = 2. So right after performFetch it shows those two child1 and `child2. So far so good.
But then following action is triggered: new Parent object is added to the same NSManagedObjectContext with typeNumber = 1 named parent2 and with two children: child3 with typeNumber = 1 and child4 with typeNumber = 2. This fires controller: didChangeObject: atIndexPath: forChangeType: newIndexPath: selector of NSFetchedResultsControllerDelegate protocol two times. Now I see all 4 children in UITableView. Ok.
Then I change parent1.typeNumber to 2 with following code: parent1.typeNumber = [NSNumber numberWithInt:2];. But no NSFetchedResultsControllerDelegateselectors are fired! How's that? Now child1 and child2 won't match NSFetchedResultsController fetchRequest, right? Why NSFetchedResultsController did not react on that? I thought it somehow knows when properties of NSManagedObject subclasses that used by CoreData are changed.
You can only use the access form e.g.
parent1.typeNumber = [NSNumber numberWithInt:2];
... if you have created a custom Parent subclass of NSManagedObject and instantiated the parent1 object as that subclass. Otherwise, the parent1 is just a generic NSManagedObject and you have to make a change with key-value coding:
[parent1 setValue:[NSNumber numberWithInt:2] forKey:#"typeNumber];
If you don't use the proper access form, the changes are actually never made so the controller never sends its delegate the change messages.
Related
I am using UIManagedDocument with Parent Child context.
In my child context I do the following
Code 1
NSSet *results = [self.event.memberships filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
return ([[evaluatedObject deleted] boolValue] == NO);
}]];
Above code returns the expected results (only Not deleted members for the event).
Code 2
But this code does not. It fetches all records.
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"deleted == NO"];
NSSet *results = [self.event.memberships filteredSetUsingPredicate:predicate];
It seems confusing. Both should return same results, but predicateWithBlock returns correct results where as predicateWithFormat returns all records.
What are the pros and cons of using predicateWithBlock instead of predicateWithFormat?
The problem is that you have defined an attribute deleted for your entity. That conflicts with the isDeleted method of NSManagedObject, so you should rename that attribute.
The following "experiment" shows that strange things happen if you call your attribute "deleted" (c is a managed object with a custom deleted attribute):
// Set custom "deleted" property to YES:
c.deleted = #YES;
// Use the property, as your Code 1
NSLog(#"%#", [c deleted]);
// Output: 1
// Use Key-Value Coding, as your Code 2
NSLog(#"%#", [c valueForKey:#"deleted"]);
// Output: 0
// Now really delete the object and try again:
[context deleteObject:c];
NSLog(#"%#", [c valueForKey:#"deleted"]);
// Output: 1
Your "Code 1" refers to the property, therefore it returns the expected result. "Code 2" uses Key-Value Coding, and [c valueForKey:#"deleted"] returns YES if the object
actually has been deleted from the context!
So renaming that attribute should solve your problem. Unfortunately the compiler does not
emit warnings if an attribute name conflicts with a built-in method.
Use the formatting placeholder to replace the bool value:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"%K == %#",
#"deleted", #(NO)];
Your use of the key path is probably ok, but the right-hand side probably doesn't look like "NO" to the parser.
In this test Core Data project, I have a one to many relationship from "Customer" to "Products" and this relationship is named 'products'. Customer's attribute is 'name' and Product's attribute is 'item'. I've subclassed the entities and Xcode has produced the following for Customer:
#interface Customer : NSManagedObject
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSSet *products;
#end
#interface Customer (CoreDataGeneratedAccessors)
- (void)addProductsObject:(Products *)value;
- (void)removeProductsObject:(Products *)value;
- (void)addProducts:(NSSet *)values;
- (void)removeProducts:(NSSet *)values;
#end
To add, let's say, one customer (John Doe) and two items (Widget 1 & Widget 2), I can use the accessor method addProductsObject as follows:
...
// Add (1) customer object
Customer *custObj = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[custObj setValue:#"John Doe" forKey:#"name"];
// Add (2) products for John Doe
for (int foo=0; foo<2; foo++) {
self.product = [NSEntityDescription insertNewObjectForEntityForName:#"Products" inManagedObjectContext:context];
NSString *objString = [NSString stringWithFormat:#"Widget %d", foo];
self.product.item = objString;
[custObj addProductsObject:self.product];
}
...
This works fine but, if possible, I'd like to make use of the addProducts accessor.
I'm assuming that the generated accessor method addProducts is there to facilitate the insertion of a 'set' of objects with something like:
...
NSSet *itemSet = [[NSSet alloc] initWithObjects:#"Widget 1", #"Widget 2", nil];
[custObj addProducts:itemSet];
...
but this fails. In this try, I understand a product object hasn't been explicitly created and, as such, an explicit product assignment hasn't taken place but I thought, perhaps, the accessor would take care of that.
What, therefore, is the correct usage of addProducts, and for that matter, removeProducts?
Thanks.
The set you are passing to addProducts contains NSString, not Products.
NSMutableSet* products = [NSMutableSet set];
Products* product = [NSEntityDescription insertNewObjectForEntityForName: #"Products" inManagedObjectContext: context];
product.item = #"Widget 1";
[products addObject: product];
product = [NSEntityDescription insertNewObjectForEntityForName: #"Products" inManagedObjectContext: context];
product.item = #"Widget 2";
[products addObject: product];
[customer addProducts: products];
Per se, the accessor have no way to know what the strings you gave it in first place were for. You have to provide a set containing the right kind of entities.
That said, you can define your own accessor, taking a set of strings and inserting properly initialized Products in your relationship : addProductsWithStrings: per example.
I'm trying to keep traveling route in core data and here is my data model. I have entity Coordinate keeping latitude and longitude in double, then I created Locatable entity and make relation between these two entities.
Coordinate <-> Locatable
then I have POI entity inherited Locatable these POI representing my route.
finally I have Destination entity inherited POI using as my destination point
Coordinate <-> Locatable
|
V
POI
|
V
Destination
My Route class have points as one-to-many relation with these POI and destination as one-to-one with Destination.
Problem occur when I try to get my POI collection for create route with MKPolylineView, when I call route.points I also get my destination point with those POI. I know core data create big Locatable for both POI and Destination, but this behavior isn't logically right my destination point shouldn't show up with points. Is this right behavior or I missing something.
Quite complicated to explain if I miss some important information please tell me.
Updated for more clarify
I have Route entity with one-to-one with Destination as destination and one-to-many with POI as points(which I using as my traveling path)
When I add destination
route.destination = destination_obj
it also show up in
route.points
which doesn't right because these two property serve two different purpose (one for making traveling path, another for pre calculate some data). Is this behavior have any document or explanation ?
Added small data model and code sample so every one can reproduce this
Make 3 entity
Family
- int generation
- parents as to-may relation to Parent and family as inverse
- child as to-one relation to Child and family as inverse
Parent
- String name
- family to-one relation to Family
Child : Parent
Here is my code
Family *fam;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Family" inManagedObjectContext:self.managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entity];
NSArray *meters = [self.managedObjectContext executeFetchRequest:fetchRequest error:nil];
if ([meters count] > 0) {
NSLog(#"found");
fam = [meters lastObject];
fam.generation = [NSNumber numberWithInt:[fam.generation intValue] + 1];
} else {
NSLog(#"new");
fam = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:self.managedObjectContext];
fam.generation = [NSNumber numberWithInt:1];
[self saveContext];
};
NSLog(#"There are %d paren", [fam.parents count]);
for (Parent *p in fam.parents) {
NSLog(#"name : %#", p.name);
}
Child *child;
if (!fam.child) {
child = [NSEntityDescription insertNewObjectForEntityForName:
[[NSEntityDescription entityForName:#"Child" inManagedObjectContext:self.managedObjectContext] name]
inManagedObjectContext:self.managedObjectContext];
fam.child = child;
}
fam.child.name = [NSString stringWithFormat:#"child number %d", [fam.generation intValue]];
NSLog(#"There are %d parent after adding one child", [fam.parents count]);
Parent *parent = [NSEntityDescription insertNewObjectForEntityForName:
[[NSEntityDescription entityForName:#"Parent" inManagedObjectContext:self.managedObjectContext] name]
inManagedObjectContext:self.managedObjectContext];
parent.name = [NSString stringWithFormat:#"parent number %d", [fam.generation intValue]];
[fam addParentsObject:parent];
NSLog(#"There are %d parent after add parent", [fam.parents count]);
for (Parent *p in fam.parents) {
NSLog(#"name : %#", p.name);
}
[self saveContext];
in short I create family and add one child and one parent to this family and print out some output
in the first run I got this result
2011-08-27 19:06:28.271 child[2015:207] new
2011-08-27 19:06:28.276 child[2015:207] There are 0 paren
2011-08-27 19:06:28.278 child[2015:207] There are 0 parent after adding one child
2011-08-27 19:06:28.279 child[2015:207] There are 1 parent after add parent
2011-08-27 19:06:28.280 child[2015:207] name : parent number 1
which is what I expected, then I rerun the app again and this what the weird thing occur
2011-08-27 19:08:12.383 child[2035:207] found
2011-08-27 19:08:12.386 child[2035:207] There are 2 paren
2011-08-27 19:08:12.387 child[2035:207] name : parent number 1
2011-08-27 19:08:12.388 child[2035:207] name : child number 1
2011-08-27 19:08:12.389 child[2035:207] There are 2 parent after adding one child
2011-08-27 19:08:12.390 child[2035:207] There are 3 parent after add parent
2011-08-27 19:08:12.390 child[2035:207] name : parent number 1
2011-08-27 19:08:12.391 child[2035:207] name : parent number 2
2011-08-27 19:08:12.391 child[2035:207] name : child number 2
child entity is included in parents property. This is some kind of my misconception or this is a bug on SDK ?
I think your setup is too complicated. Also, you set up your relationships a bit like sub-classes. But relationships and sub-classes are completely different concepts, and I think you might be mixing them up.
Here is what I propose:
Have an entity Location, with attributes latitude, longitude and name. Set latitude and longitude as non-optional, but name as optional. Now you have all the basic building blocks you need.
In your code you can define objects such as *POI of type Location, *destination of type Location and *startPoint of type Location. You can use the coordinates to calculate the shortest route for example. By setting the name you can make any Location into a POI. If you want names for non-POIs, create a boolean attribute isPOI.
Now, if you want to store specific travel routes, you can introduce an entity Route, with attributes like name, date etc. and a one-to-many relationship to Location. Note that you can not put the many Locations in any particular order with this scheme. However, what you could do is include two more one-to-one relationships to Location, one called startLocation and one destination.
If you really need a more complex description of itineraries in your core data model, you probably need another entity. You could call it TravelPoints with a many-to-one relationship to Route, a one-to-one relationship to Location and attributes like arrivalDate, timeStayed or departureDate, sequentialNumber (to indicated the order in Route), etc.
With this setup you should be able to go far with your project.
I am trying to parse a lot of text files and organize their contents as managed objects. There are a lot of duplicates in the text files, so one of the "collateral" tasks is to get rid of them.
What i am trying to do in this respect is to check whether an entity with the given content exists, and if it doesn't, i create one. However, i have different entities with different attributes and relationships. What i want is a kind of function that would take a number of attributes as an input and return a new NSManagedObject instance, and i wouldn't have to worry if it was inserted into the data store or fetched from it.
Is there one?
I must also say that i am a noob at core data.
Some more detail, if you want:
I am trying to write a sort of dictionary. I have words (Word{NSString *word, <<-> Rule rule}), rules (Rule{NSString name, <->>Word word, <<->PartOfSpeech partOfSpeech, <<-> Ending endings}), parts of speech (PartOfSpeech{NSString name, <<-> Rule rule}) (i hope the notation is clear).
Two words are equal, if they have the same word property, and "linked" to the same rule. Two rules are the same, if they have the same endings and part of speech.
So far i've written a method that takes NSPredicate, NSManagedObjectContext and NSEntityDescription as an input, and first queries the datastore and returns an entity if it finds one, or creates a new one, inserts it into the datastore and returns it. However, in this case I cannot populate the new entity with the necessary data (within that method), so i have to either pass an NSDictionary with the names of attributes and their values and insert them, or return by reference a flag as to whether i created a new object or returned an old one, so that i could populate it with the data outside.
But it looks kind of ugly. I'm sure there must be something more elegant than that, i just couldn't find it. Please, help me if you can.
Your basically on the right path. Core Data is an object graph. There not a lot of dynamic built in. There's also no "upsert". like you surmise, you have to fetch and if it doesn't exist, you insert one.
Here is what I have just started using to handle a fetch-or-create scenario. I am using a top level managed object which contains a few to-many relationships to subordinate objects. I have a class that houses a few arrays of data (those are not shown here). This class is responsible for saving and retrieving to and from core data. When the class is created, I do a fetch-or-create to access my top level NSManagedObject.
#implementation MyDataManagerClass
...
#synthesize MyRootDataMO;
- (MyDataManagerClass *) init {
// Init managed object
NSManagedObjectContext *managedObjectContext = [(MyAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
// Fetch or Create root user data managed object
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"MyRootDataMO" inManagedObjectContext:managedObjectContext];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:entityDescription];
NSError *error = nil;
NSArray *result = [managedObjectContext executeFetchRequest:request error:&error];
if (result == nil) {
NSLog(#"fetch result = nil");
// Handle the error here
} else {
if([result count] > 0) {
NSLog(#"fetch saved MO");
MyRootDataMO = (MyRootDataMO *)[result objectAtIndex:0];
} else {
NSLog(#"create new MO");
MyRootDataMO = (MyRootDataMO *)[NSEntityDescription insertNewObjectForEntityForName:#"MyRootDataMO" inManagedObjectContext:managedObjectContext];
}
}
return self;
}
...
say i have NSManagedObject A, that has a many-to-many relationship to NSManagedObject B.
I have one saved instance of A and B. (not yet related)
Now I want A to save the instance of B twice in its relationship, which is of course a set.
Though, since its a set it stores only one reference of B.
see: (not syntax checked)
NSArray *tmpArray = [NSArray arrayWithObjects: B1, B1, nil];
[A setB: [NSSet setWithArray: tmpArray]];
-> only one B is stored in that relationship..
Is it possible to keep track of both B's ?
You can't do this. Core Data is not maintaining an array but an object-graph.
An object-graph store the relationships between objects. Since each object is unique, it makes no sense to have a duplicate relationship because that conveys no information. Suppose you have an object Person instance Jane that has a brothers relationship which contains three objects, Steve,John and Mike. It would be logically nonsensical to have two relationships to Steve because the real Steve that the object models isn't Jane's brother twice. Even if Jane did have two brothers named Steve, they would still be seperate individuals requiring their own objects to represent them in the object-graph.
If you find yourself thinking you require duplicate relationships, then you've probably misunderstood something about how the object-graph works.
I had a similar issue and searched but could not find anything. Indeed I was thinking about the relationship incorrectly.
The app is arranging a bunch of Items on a Board in sequence with repeats.
EX. Do A, Do B, Do C, Do A again, Do D
I created a good old-fashioned join table with the Item, Board and board position called BoardItemPositon.
From the Board entity you can hide all of the join table messiness and get and set an array.
#implementation Board
#dynamic boardItems;
- (void)setItems:(NSArray *)items{
//the ordered set of links
NSMutableOrderedSet *boardItemSet = [NSMutableOrderedSet new];
int i = 1;
for (Item *item in items) {
BoardItemPosition *boardItemPosition = (BoardItemPosition *)[NSEntityDescription insertNewObjectForEntityForName:#"BoardItemPosition"
inManagedObjectContext:[self managedObjectContext]];
NSNumber *num = [NSNumber numberWithInt:i++];
[boardItemPosition setItemPosition:num];
[boardItemPosition setItem:item];
[boardItemSet addObject:boardItemPosition];
}
//delete the old links
for (BoardItemPosition *boardItemPosition in [self boardItems]) {
[self.managedObjectContext deleteObject:[self.managedObjectContext objectWithID:boardItemPosition.objectID]];
}
//set the new links
[self setBoardItems:boardItemSet];
}
- (NSArray *)items{
//pull out item and add to array
NSMutableArray *itemArray = [NSMutableArray new];
for (BoardItemPosition *boardItemPosition in [self boardItems]) {
[itemArray addObject:[boardItemPosition item]];
}
return itemArray;
}