I have a Core Data model representing a TV guide on iOS 4+, with 3 classes:
Channel
(BBC 1)Program
(Top Gear)Broadcast
(Top Gear on BBC 1 on Monday at 8pm)I have about 40 channels, 8000 programs and 6000 broadcasts, and I would like to fine-tune the import process so that it doesn't take up to a minute to run.
Importing the channels and programs is easy because these are independent objects. A broadcast however has a relationship to a channel and to a program (1-to-many), and both channels and programs have inverse relationships to the broadcasts (many-to-1). To speed things up I have an in-memory dictionary of fault channels and programs that have only their Web Service identifier prefetched: I create a broadcast and look through both dictionaries to get the corresponding channel and program without a round-trip to the database.
But when I assign a program or a channel to a broadcast, the channel and program's inverse relationships access trigger a fault of both objects right away, causing a massive slowdown (6000 * 2 requests) and consequent memory pressure as shown in the Core Data Faults Instruments report. I tried pre-fetching the broadcasts
relationship on both channels and programs, but the relationship still gets faulted.
Do you have any idea why the inverse relationships get accessed and fault their parents? How do I avoid reading from the database when saving a relationship?
UPDATE: Sample code, my assign / update method for a Broadcast
instance. The dictionary
variable comes from the Web Service and channels
and programs
contain the fault channels and programs objects indexed by Web Service identifier. Faulting occurs on the self.program = program
and self.channel = channel
lines.
- (BOOL)assignWithDictionary:(NSDictionary *)dictionary channels:(NSDictionary *)channels programs:(NSDictionary *)programs {
// Add channel relationship
NSNumber *channelIdentifier = [dictionary objectForKey:@"channel_id"];
if (self.channel == nil || ![self.channel.identifier isEqualToNumber:channelIdentifier]) {
Channel *channel = [channels objectForKey:channelIdentifier];
if (channel == nil) {
NSLog(@"Broadcast %@ has invalid channel: %@", identifier, channelIdentifier);
return NO;
}
self.channel = channel;
}
// Same to add a program relationship
// ...
}
And my fetch request to get the channels or the programs list:
- (NSDictionary *)itemsForEntity:(NSEntityDescription *)entity {
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSError *error = nil;
NSArray *itemsArray = nil;
request.entity = entity;
request.relationshipKeyPathsForPrefetching = [NSArray arrayWithObject:@"broadcasts", nil];
request.propertiesToFetch = [NSArray arrayWithObjects:@"identifier", @"version", nil];
itemsArray = [self.context executeFetchRequest:request error:&error];
NSAssert1(error == nil, @"Could not fetch the items from the database: %@", error);
{
NSMutableDictionary *items = [NSMutableDictionary dictionaryWithCapacity:itemsArray.count];
for (NSManagedObject *item in itemsArray) {
[items setObject:item forKey:[item valueForKey:@"identifier"]];
}
return [NSDictionary dictionaryWithDictionary:items];
}
}
Faulting allows Core Data to put boundaries on the object graph. Because a fault is not realized, a managed object fault consumes less memory, and managed objects related to a fault are not required to be represented in memory at all.
Inverse relationships enable Core Data to propagate change in both directions when an instance of either the source or destination type changes. Every relationship must have an inverse. When creating relationships in the Graph editor, you add inverse relationships between entities in a single step.
Fetched Properties in Core Data are properties that return an array value from a predicate. A fetched property predicate is a Core Data query that evaluates to an array of results.
Not exactly sure what you are trying to do here but...
The first thing is that you can't alter properties using just faults. Faults are just placeholders to allow you to measure/count the object graph and walk relationships. If you actually alter a relationship it will fire the fault causing the related objects to load.
If you are trying to set relationships between specific Channel
, Program
and Broadcast
objects using just faults, that won't work.
Your itemsForEntity:
method I don't understand. It will fetch every existing managed object of the entity passed and then it will return those objects in a dictionary. That will cause a massive memory overhead especially in the case of the Program
objects of which there are 8,000.
You can't use propertiesToFetch
unless you set the fetch return to dictionary, which you don't. You can't use a dictionary return type anyway if you need to set relationships. You use both these when all you want is the data held in certain attributes. It's not a tool for manipulating the object graph's relationships.
Setting the relationshipKeyPathsForPrefetching
only speeds things up if you know you will be accessing an existing relationship. It doesn't help when you are setting the relationships up in the first place e.g. if there is no existing objects in the broadcasts
relationships or you are adding or removing Broadcast
objects, prefetching the broadcasts
keypath does nothing for you.
I'm not sure I understand your data model well enough but I think you are going about this the wrong way. It looks to me like your trying to use the identifier
like a primary key in a SQL database and that is counterproductive. In Core Data, a relationship links to objects together, not a shared attribute and value.
As a rule, if you have two or more objects with the same attribute name with the same value, then you have a poorly designed data model in most cases.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With