Extremely Massive and Continuous Import to Core Data Efficiently - objective-c

Questions:
How do i release the memory used by the NSManagedObjectContext(i guess) when the number of records to be inserted to Core Data are unforeseeable, such that the memory can be efficiently used?
Here is my case:
I have a bluetooth device that will continuously send twelve sets of integer to the iOS Device on every 0.00125 seconds(the minimum interval, maximum case will be 0.002 seconds) , i should then store those integer into CoreData with the timestamp.
Data Objects and Association:
When the process start, a header record(NSManagedObject) is created as the key to retrieve the batch of received data from the bluetooth device. The object is retained as strong property during the whole period of data receiving and will remove from the property( probably set nil) when the process is ended.
Design of the NSManagedObjectContext:
All of the three ManagedObjectContext are a singleton object stored in AppDelegate
The code for creating the ManagedObjectContext are listed below:
- (NSManagedObjectContext *)masterManagedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_masterManagedObjectContext != nil) {
return _masterManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_masterManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_masterManagedObjectContext setUndoManager:nil];
return _masterManagedObjectContext;
}
-(NSManagedObjectContext*) backgroundManagedObjectContext{
if(_backgroundManagedObjectContext != nil){
return _backgroundManagedObjectContext;
}
_backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[_backgroundManagedObjectContext setUndoManager:nil];
[_backgroundManagedObjectContext setParentContext:[self masterManagedObjectContext]];
return _backgroundManagedObjectContext;
}
-(NSManagedObjectContext*) mainManagedObjectContext{
if(_mainManagedObjectContext !=nil){
return _mainManagedObjectContext;
}
_mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainManagedObjectContext setUndoManager:nil];
[_mainManagedObjectContext setParentContext:[self masterManagedObjectContext]];
return _mainManagedObjectContext;
}
The import is processed in the backgroundManagedObjectContext.
The Header is created and stored by using the below code:
_header = [NSEntityDescription insertNewObjectForEntityForName:#"Header" inManagedObjectContext:_backgroundManagedObjectContext];
_header.startTime = [NSDate date];
NSError *error;
BOOL success = [_backgroundManagedObjectContext save:&error];
The Received Data is created and stored by using the below code when the bluetooth devices fired the method:
#autoreleasepool {
ReceivedData* data = [NSEntityDescription insertNewObjectForEntityForName:#"ReceivedData" inManagedObjectContext:_backgroundManagedObjectContext];
//Data is set here
[_header addFk_header_many_dataObject:data];
currentCount ++;
if(currentCount >=1000){
currentCount = 0;
NSError *error;
BOOL success = [_backgroundManagedObjectContext save:&error];
}
}
the received data will be stored into the managedObjectContext per 1000 data is received.
Once if i stop the process, the memory consumed is doubled, and last until i completely terminate the app.
The code to handle the end of process is listed below:
_header.endTime = [NSDate date];
_header = nil;
NSError *error;
BOOL success = [_backgroundManagedObjectContext save:&error];
[_masterManagedObjectContext performBlock:^{
NSError* mastererror;
BOOL mastersuccess = [_masterManagedObjectContext save:&mastererror];
}];
Issue:
As mentioned by Core Data Performance by Apple, using reset method of NSManagedObjectContext will remove all managed objects associated with a context and "start over" as if you'd just created it.
In my understanding, that means i can only call this method at the end of the whole process. I have tried to add reset function just after _backgroundManagedObjectContext and _masterManagedObjectContext is saved. However, the memory remain unchanged.
Illustration of the memory usage
For the case of data is received on every 0.002 seconds, 0.5MB memory increased per 1000 records is saved to backgroundManagedObjectContext. Therefore, the app will consume around 150 MB for 8 mins process time and memory will increase to 320MB when the process terminated at that time, and will retain the memory usage around 220MB.
Questions:
How do i release the memory used by the NSManagedObjectContext(i guess) when the number of records to be inserted to Core Data are unforeseeable, such that the memory can be efficiently used?
Sorry for some idiot mistakes as i am quite new in iOS. I have tried my best to search around before posting the question.
Your help is appreciated.
Thank you very much.
Remarks
I have tried the above mentioned case in no more than 10 mins process time. However, the implementation should have extended to the case for more than 1 hour process time. I still have no idea on the way of handling such case.
EDIT 1 modified the code for showing the relationship of ReceivedData and Header
EDIT 2 updated the code for the standard mentioned by #flexaddicted

Just my advice. Maybe someone could have a different approach.
In this case I would eliminate the BackgroundManagedObjectContext and I would leave only the MasterManagedObjectContext (as the parent of the main one). Since, you need a low memory profile, you should switch to mechanism that allows you to control the memory footprint of your application. So, I would created a sort of buffer that starts to collect the receive data. When the buffer has reached its limit, I would move the receive data to the MasterManagedObjectContext in order to save them into the persistent store. Here the limit of the buffer (a vector of structs or array of objects in my mind) should be tuned on the application performance. In this way you have a direct control of the objects created. So, you can throw them away whenever you have finished a bunch of imported data (where the bunch is the limit of that vector/array).
Otherwise, you can try the following approach.
#autoreleasepool {
NSMutableArray *temporary = [NSMutableArray array];
ReceivedData* data = [NSEntityDescription insertNewObjectForEntityForName:#"ReceivedData" inManagedObjectContext:_backgroundManagedObjectContext];
// Data is set here
// Let temporary to hold a reference of the data object
[temporary addObject:data];
currentCount ++;
if(currentCount >=1000){
currentCount = 0;
NSError *error;
BOOL success = [_backgroundManagedObjectContext save:&error];
for(NSManagedObject *object in temporary) {
[_backgroundManagedObjectContext refreshObject:object mergeChanges:NO];
}
[temporary removeAllObjects];
}
}
Update 1
Can you also show where you set the relationship between ReceiveData and Header? I'm asking this because you can change the time where you set the relationship between this two entity.
Based on your modified code.
#autoreleasepool {
receivedData* data = [NSEntityDescription insertNewObjectForEntityForName:#"ReceivedData" inManagedObjectContext:_backgroundManagedObjectContext];
//Data is set here
[_header addFk_header_many_dataObject:data];
currentCount ++;
if(currentCount >=1000){
currentCount = 0;
NSError *error;
BOOL success = [_backgroundManagedObjectContext save:&error];
}
}
If you are able to posticipate this association on the master queue (I guess you need to set the attribute as an optional one), you can do like the following:
#autoreleasepool {
ReceivedData* data = [NSEntityDescription insertNewObjectForEntityForName:#"ReceivedData" inManagedObjectContext:_backgroundManagedObjectContext];
// Data is set here
// Move it later
//[_header addFk_header_many_dataObject:data];
currentCount ++;
if(currentCount >=1000){
currentCount = 0;
NSError *error;
BOOL success = [_backgroundManagedObjectContext save:&error];
[_backgroundManagedObjectContext reset];
}
}
P.S. receiveData *data = ... should be ReceiveData *data = .... In other words, classes should start with a capital letter.

Related

Coredata: Performance issue when saving a lot of data

I want to fill my app's database with some data from a CSV file during initial startup (50'000 entries). I however run into performance issues...it is way too slow right now and in the simulator takes like 2-3 minutes. My context hierarchy is as follows:
parentContext (separate thread)
context (main thread)
importContext (separate thread)
The code:
for (NSString *row in rows){
columns = [row componentsSeparatedByString:#";"];
//Step 1
Airport *newAirport = [Airport addAirportWithIcaoCode: [columns[0] stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceCharacterSet]]
name:columns[1]
latitude:[columns[2] doubleValue]
longitude:[columns[3] doubleValue]
elevation:[columns[4] doubleValue]
continent:columns[5]
country:columns[6]
andIataCode:columns[7]
inManagedObjectContext:self.cdh.importContext];
rowCounter++;
//Increment progress
dispatch_async(dispatch_get_main_queue(), ^{
[progress setProgress:rowCounter/totalRows animated:YES];
[progress setNeedsDisplay];
});
// STEP 2: Save new objects to the parent context (context).
//if(fmodf(rowCounter, batchSize)== 0) {
NSError *error;
if (![self.cdh.importContext save:&error]) {
NSLog(#"error saving %#", error);
}
//}
// STEP 3: Turn objects into faults to save memory
[self.cdh.importContext refreshObject:newAirport mergeChanges:NO];
}
If I activate the if with modulo for batch size 2000 in step 2, then of course only every 2000nd entry gets saved and then the performance is fast. But like this it is super slow and I wonder why? I have my import context in a separate thread and still it lags very much...
Normally, it is enough to issue a single save after processing all your entries. You don't need to save this often. Just do it after the for loop. I see that you try to save memory by turning the objects into faults each time, so this might require a save (did not try this before).
I suggest you use #autoreleasepool instead and let the system decide where to save memory:
for (NSString *row in rows) {
#autoreleasepool {
// Just STEP 1 here
...
}
}
NSError *error;
if (![self.cdh.importContext save:&error]) {
NSLog(#"error saving %#", error);
}

Core Data save and Concurrency problems in nested loops + CloudKit

I'm using CloudKit to download an array of records (contained in myArray) The myArray enumeration is within the completion handler of the CloudKit block. There are a few nested CloudKit queries and array enumerations (example below). From there, I'm creating managed objects in a loop, and saving them, which will run only on first launch and then I'd expect Core Data to have them available persistently, so the app is designed to retrieve them without the need of re-creating them.
The problem is that my objects do not appear to save, as on the apps second launch the views are empty (or that the app saves some, or it crashes), and will only fill if I re-run the code to create the objects.
I think the issue may to do with concurrency issues / threads + Core Data - which seems to be the case after adding the compiler flag suggested. Consequently, I edited my code to make use of private queues for the NSManagedObjectContext, but still have crashes. I've edited the example below to show what my code looks like now.
I've simplified the code below for clarify and the purpose of the example, but it is more or less what I have:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//Download records from CloudKit, the following enumeration is within the CloudKit completion handler
[myArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
MyManagedObj *managedObjToInsert = [NSEntityDescription
insertNewObjectForEntityForName:#"entityName"
inManagedObjectContext:self.managedObjectContext];
managedObjToInsert.property = obj[#"property"];
//Get some new records from CloudKit with predicate based on this object which is related to the new records, this next block enumeration is in the completion handler of the new query
[myNextArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
MyManagedObj *nextManagedObjToInsert = [NSEntityDescription
insertNewObjectForEntityForName:#"entityName"
inManagedObjectContext:self.managedObjectContext];
nextManagedObjToInsert.property = obj[#"property"];
nextManagedObjToInsert.relatedObj = managedObjToInsert; //relational
}];
}];
NSError *error;
if (![self.managedObjectContext save:&error])
{
NSLog(#"Problem saving: %#", [error localizedDescription]);
}
}
I've added the flag suggested in the answers below, and it seems like my managedobjectcontext is being passed outside the main thread, giving unpredictable results. Where do or how do I use the private queue blocks - assuming that is the solution to the problem?
Turn on concurrency debugging if you are worried about the threads. Add this as a command line parameter to the start up
-com.apple.CoreData.ConcurrencyDebug 1
see an example here; http://oleb.net/blog/2014/06/core-data-concurrency-debugging/
I think you are correct in worrying about threads because you can't be sure what thread your CloudKit completion block is being run. Try wrapping your object creation loop (and the save) within a [self.managedObjectContext performBlockAndWait] section.
Solved this issue using private queues with the help of the following documentation (in addition to the helpful comments/answers shared before this answer):
Correct implementation of parent/child NSManagedObjectContext
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Concurrency.html
http://benedictcohen.co.uk/blog/archives/308
The problem was that I was trying to save to the NSManagedObjectContext on the main thread whilst the code being executed by the cloudkit query to the database was occuring on another thread, resulting in crashes and inconsistent saves to the persistent store. The solution was to use the NSPrivateQueueConcurrencyType by declaring a new child context (this is only supported in iOS 5+).
Working code now looks like*:
//create a child context
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[privateContext setParentContext:[self managedObjectContext]];
//Download records from CloudKit by performing a query
[publicDatabase performQuery:myQuery inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * resultsArray, NSError * error) {
[resultsArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
__block NSManagedObjectID *myObjID;
//Async and not in main thread so requires a private queue
[privateContext performBlockAndWait:^{
MyManagedObj *managedObjToInsert = [NSEntityDescription insertNewObjectForEntityForName:#"entityOne" inManagedObjectContext:privateContext];
managedObjToInsert.property = obj[#"property"];
myObjID = managedObjToInsert.objectID;
NSError *error;
if (![privateContext save:&error]) //propergates to the parent context
{
NSLog(#"Problem saving: %#", [error localizedDescription]);
}
}];
//Get some new records from CloudKit with predicate based on this object which is related to the new records, this next block enumeration is in the completion handler of the new query
[publicDatabase performQuery:mySecondQuery inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * secondResultsArray, NSError * error) {
[secondResultsArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * stop) {
[privateContext performBlockAndWait:^{
MyManagedObj *nextManagedObjToInsert = [NSEntityDescription insertNewObjectForEntityForName:#"entityTwo" inManagedObjectContext:privateContext];
nextManagedObjToInsert.property = obj[#"property"];
NSManagedObject *relatedObject = [privateContext objectWithID:myObjID];
nextManagedObjToInsert.relatedObj = relatedObject; //relational
}];
}];
}];
NSError *childError = nil;
if ([privateContext save:&childError]) { //propagates to the parent context
[self.managedObjectContext performBlock:^{
NSError *parentError = nil;
if (![self.managedObjectContext save:&parentError]) { //saves to the persistent store
NSLog(#"Error saving parent %#", parentError);
}
}];
} else {
NSLog(#"Error saving child %#", childError);
}
}];
}];
*please note that this is just an example to show how I solved the problem - there may be certain variable declarations missing, but the gist of the solution is there.

Objective C - Firebase - How to add completion handler to FDataSnapshot

I'm experimenting with Firebase's FDataSnapshot to pull in data and I would like it to write its data to my core data using MagicalRecord.
According to Firebases "best practice" blog I need to keep a reference to the "handle" so it can be cleaned up later on. Further, they mention to put the FDSnapshot code in viewWillAppear.
I am wanting a callback so that when its finished doing its thing to update core data.
But I'm really note sure how to do that; its doing two things and giving a return at the same time.
// In viewWillAppear:
__block NSManagedObjectContext *context = [NSManagedObjectContext MR_context];
self.handle = [self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
if (snapshot.value == [NSNull null])
{
NSLog(#"Cannot find any data");
}
else
{
NSArray *snapshotArray = [snapshot value];
// cleanup to prevent duplicates
[FCFighter MR_truncateAllInContext:context];
for (NSDictionary *dict in snapshotArray)
{
FCFighter *fighter = [FCFighter insertInManagedObjectContext:context];
fighter.name = dict[#"name"];
[context MR_saveToPersistentStoreWithCompletion:^(BOOL contextDidSave, NSError *error){
if (error)
{
NSLog(#"Error - %#", error.localizedDescription);
}
}];
}
}
}];
NSFetchRequest *fr = [[NSFetchRequest alloc] initWithEntityName:[FCFighter entityName]];
fr.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.fighterList = (NSArray *) [context executeFetchRequest:fr error:nil];
[self.tableView reloadData];
In the above code, the core data reading does not wait for the firebase to complete.
Thus, my query -- how would I best combine a completion handler so that when it is complete to update core data, and reload the tableview.
Many thanks
This is a common issue when working with Asynchronous data.
The bottom line is that all processing of data returned from an async call (in this case, the snapshot) needs to be done inside the block.
Anything done outside the block may happen before the data is returned.
So some sudo code
observeEvent withBlock { snapshot
//it is here where snapshot is valid. Process it.
NSLog(#"%#", snapshot.value)
}
Oh, and a side note. You really only need to track the handle reference when you are going to do something else with it later. Other than that, you can ignore the handles.
So this is perfectly valid:
[self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
//load your array of tableView data from snapshot
// and/or store it in CoreData
//reload your tableview
}

NSSortDescriptor of NSFetchRequest not working after context save

I'm doing operations in a GCD dispatch queue on a NSManagedObjectContext defined like this:
- (NSManagedObjectContext *)backgroundContext
{
if (backgroundContext == nil) {
self.backgroundContext = [NSManagedObjectContext MR_contextThatNotifiesDefaultContextOnMainThread];
}
return backgroundContext;
}
MR_contextThatNotifiesDefaultContextOnMainThread is a method from MagicalRecord:
NSManagedObjectContext *context = [[self alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:[NSManagedObjectContext MR_defaultContext]];
return context;
After fetching my objects and giving them the correct queue position i log them and the order is correct. However, the second log seems to be completely random, the sort descriptor clearly isn't working.
I have narrowed down the Problem to [self.backgroundContext save:&error]. After saving the background context sort descriptors are broken.
dispatch_group_async(backgroundGroup, backgroundQueue, ^{
// ...
for (FooObject *obj in fetchedObjects) {
// ...
obj.queuePosition = [NSNumber numberWithInteger:newQueuePosition++];
}
NSFetchRequest *f = [NSFetchRequest fetchRequestWithEntityName:[FooObject entityName]];
f.predicate = [NSPredicate predicateWithFormat:#"queuePosition > 0"];
f.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"queuePosition" ascending:YES]];
NSArray *queuedObjects = [self.backgroundContext executeFetchRequest:f error:nil];
for (FooObject *obj in queuedObjects) {
DLog(#"%# %#", obj.queuePosition, obj.title);
}
if ([self.backgroundContext hasChanges]) {
DLog(#"Changes");
NSError *error = nil;
if ([self.backgroundContext save:&error] == NO) {
DLog(#"Error: %#", error);
}
}
queuedObjects = [self.backgroundContext executeFetchRequest:f error:nil];
for (FooObject *obj in queuedObjects) {
DLog(#"%# %#", obj.queuePosition, obj.title);
}
});
I've got no idea why the sort descriptor isn't working, any Core Data experts want to help out?
Update:
The problem does not occur on iOS 4. I think the reason is somewhere in the difference between thread isolation and private queue modes. MagicalRecord automatically uses the new concurrency pattern which seems to behave differently.
Update 2:
The problem has been solved by adding a save of the background context:
if ([[NSManagedObjectContext MR_contextForCurrentThread] hasChanges]) {
DLog(#"Changes");
NSError *error = nil;
if ([[NSManagedObjectContext MR_contextForCurrentThread] save:&error] == NO) {
DLog(#"Error: %#", error);
} else {
NSManagedObjectContext *parent = [NSManagedObjectContext MR_contextForCurrentThread].parentContext;
[parent performBlockAndWait:^{
NSError *error = nil;
if ([parent save:&error] == NO) {
DLog(#"Error saving parent context: %#", error);
}
}];
}
}
Update 3:
MagicalRecord offers a method to recursively save a context, now my code looks like this:
if ([[NSManagedObjectContext MR_contextForCurrentThread] hasChanges]) {
DLog(#"Changes");
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveWithErrorHandler:^(NSError *error) {
DLog(#"Error saving context: %#", error);
}];
}
Shame on me for not using it in the first place...
However, I don't know why this helps and would love to get an explanation.
I'll try to comment, since I wrote MagicalRecord.
So, on iOS5, MagicalRecord is set up to try to use the new Private Queue method of multiple managed object contexts. This means that a save in the child context only pushes saves up to the parent. Only when a parent with no more parents saves, does the save persist to its store. This is probably what was happening in your version of MagicalRecord.
MagicalRecord has tried to handle this for you in the later versions. That is, it would try to pick between private queue mode and thread isolation mode. As you found out, that doesn't work too well. The only truly compatible way to write code (without complex preprocessor rules, etc) for iOS4 AND iOS5 is to use the classic thread isolation mode. MagicalRecord from the 1.8.3 tag supports that, and should work for both. From 2.0, it'll be only private queues from here on in.
And, if you look in the MR_save method, you'll see that it's also performing the hasChanges check for you (which may also be unneeded since the Core Data internals can handle that too). Anyhow, less code you should have to write and maintain...
The actual underlying reason why your original setup didn't work is an Apple bug when fetching from a child context with sort descriptors when the parent context is not yet saved to store:
NSSortdescriptor ineffective on fetch result from NSManagedContext
If there is any way you can avoid nested contexts, do avoid them as they are still extremely buggy and you will likely be disappointed with the supposed performance gains, cf. also:
http://wbyoung.tumblr.com/post/27851725562/core-data-growing-pains
Since CoreData isnot a safe-thread framework and for each thread(operation queue), core data uses difference contexts. Please refer the following excellent writing
http://www.duckrowing.com/2010/03/11/using-core-data-on-multiple-threads/

Why is Core Data losing one of my values?

Inside my user object I have the following code to generate a new 'session' or continue the existing session if one exists.
Strangely it will keep other properties but just loses the 'user' property... user is in a one to many relationship with session, 1 user can have many sessions. (or will do, for the following test I am simply checking for any previous session and using it if it exists)
-(void)setupSessionStuff
{
// Create new Core Data request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Session" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
// Create Sort Descriptors for request
NSSortDescriptor *startTimeSort = [[NSSortDescriptor alloc] initWithKey:#"startTime" ascending:NO selector:nil];
[request setSortDescriptors:[NSArray arrayWithObjects:startTimeSort, nil]];
[startTimeSort release];
[request setFetchLimit:1]; // Only get the most recent session
// Execute request
NSError *error = nil;
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
if (results == nil) {
// Something went horribly wrong...
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1);
}
[request release];
Session *theSession = nil;
if ([results count] == 1) {
NSLog(#"existing session");
// Use existing Session
theSession = [results objectAtIndex:0];
NSLog(#"session.user: %#", [theSession valueForKey:#"user"]); // this is always null!
} else {
NSLog(#"new session");
// Create new Sesson
theSession = (Session *)[NSEntityDescription insertNewObjectForEntityForName:#"Session" inManagedObjectContext:[self managedObjectContext]];
// Add the Session to the User
NSLog(#"before: session.user: %#", theSession.user); // null
theSession.user = self;
NSLog(#"after: session.user: %#", theSession.user); // looks good
}
...
NSLog(#"before.save: session.user: %#", theSession.user); // good
// Save everything
error = nil;
if (![[self managedObjectContext] save:&error]) {
// Something went horribly wrong...
NSLog(#"Unresolved error: %#, %#, %#", error, [error userInfo],[error localizedDescription]);
exit(-1);
}
NSLog(#"after.save: session.user: %#", theSession.user); // still there..
}
Additionally I have opened up the Core Data sqlite file and examined with SQLite Manager. It looks like the relationship has been correctly saved, as I can see the userID stored in the session table.
-
Just added this at the start of my method as another test.
NSSet *set = self.session;
for(Session *sess in set) {
NSLog(#"startTime %#", sess.startTime);
NSLog(#"user %#", sess.user);
}
Strangely enough the user is set in this case!? So set here then not set a few lines later when I do the fetch request... ?
-
In response to feedback below
Have added this code after assigning session.user = self and both return the expected output. So it does look like the problem is with the subsequent fetch.
NSLog(#"self.session: %#", self.session);
NSLog(#"self.session: %#", [self valueForKey:#"session"]);
Also I agree that accessing my session's through self.session will let me work around my issue, but it doesn't solve what is going on here.
In other places I surely won't be able to walk from one entity to the other so need to confidence the fetch is going to pull everything in correctly.
Firstly, I would check that the relationship is set from the other side by logging self.session. If that shows as null then you have no reciprocal relationship set in the model.
I think that your fetch is poorly constructed. You are relying on the sort descriptor to provide the last used session but you only have a fetch limit of 1. You seem to think that the fetch will find all existing Session objects, sort them and then return the first one. However, sorts execute last so the fetch will find a single session object (because of the fetch limit) and will then apply the sort to that single object. This makes it likely that you will be dealing with a more or less random Session object.
You probably don't need a fetch at all because you are only interested in the Session objects in a relationship with the User object which is self. If you already have the object in one side of a relationship, you don't need to fetch, you just need to walk the relationship. All you really need to do is:
if ([self.session count]==0){
//...create new session and set relationship
}else{
//... find latest session
}
I think your problem with this particular chunk of code is in the reporting. When you get a return value, you use the self.user notation but where you get a null return you use valueForKey:.
While in theory both return the same value, they don't have to because they don't use the same mechanism to return the value. The self-dot notation calls an accessor method while valueForKey: may or may not depending on the specifics of a class' implementation. I would test if the self.session returns a value where valueForKey: does not.
Well I found the problem and solved my issue...
After examining the memory address of my session entity I noticing that it was changing between runs. Investigating further I discovered that where I had been testing some code earlier in another class, creating a new session entity, but not saving it, well, it was being saved after all - when I issued the save in my code above!