SFnx
SFnx

Reputation: 203

Fetching a one-to-many core data relationship returns correct objects the first time but empty set all other times

I have an object, Workout, that has a one-to-many relationship with an object, Exercise.

Diagram of models: https://i.sstatic.net/K1gHk.png

When I create a Workout object, I add three Exercise objects to it by looping over

[self addExercisesObject:exercise]

and then save my managed object context. Then, from my controller for displaying a workout, I can successfully fetch the workout and its exercises (with a fetch request), as shown by the output in my debugger:

Printing description of self->_savedWorkout:
<Workout: 0x6c5a990> (entity: Workout; id: 0x6e46e00 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Workout/p1> ; data: {
bodyweight = nil;
date = "2012-05-09 16:59:43 +0000";
exercises =     (
    "0x6e3c870 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p3>",
    "0x6e3eaf0 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p2>",
    "0x6e36820 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p1>"
);
isCompleted = 0;
workoutId = 1;
workoutPlan = "0x6e6c980 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/WorkoutPlan/p1>";
})

So far so good. However, if I close my app in my simulator and start it up again and perform the same fetch request in same view, the workout looks like this:

Printing description of self->_savedWorkout:
<Workout: 0x6ea8ff0> (entity: Workout; id: 0x6e8f9e0 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Workout/p1> ; data: {
bodyweight = nil;
date = "2012-05-09 16:59:43 +0000";
exercises =     (
);
isCompleted = 0;
workoutId = 1;
workoutPlan = "0x6c8a130 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/WorkoutPlan/p1>";
})

It appears that it fetches the same workout object, but now exercises is an empty set. Actually, exercises first looks like this after the fetch request:

exercises = "<relationship fault: 0x8a93100 'exercises'>";

but once I do:

for (Exercise *exercise in self.savedWorkout.exercises)

self.savedWorkout.exercises resolves to an empty set. I do not edit the workout in anyway in any part of my app.

My fetch request is made by this method in my Workout class:

- (Workout *)getLatestWorkout
{
  self.model = [[self.managedObjectContext persistentStoreCoordinator] managedObjectModel];

  NSFetchRequest *fetchRequest = [self.model fetchRequestTemplateForName:@"getLatestWorkout"];

  NSError *error = nil;
  NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  if ([results count] == 1) {
    return [results objectAtIndex:0];
  }
  return nil;
}

I made the fetch request template with Xcode's GUI tool. It fetches all Workout objects where isCompleted == 0. You can see that it fetches the same object each time because the workout's x-coredata path is the same in both debugger outputs.

Update: I checked my SQLite database. There is one workout in the workout table and three exercises in the exercises table.

Any ideas what's going on?

EDIT: Code that creates objects posted below

- (void)storeUserSettings
{
  // get the file path if it exists
  NSString *path = [[NSBundle mainBundle] pathForResource:@"userSettings" ofType:@"plist"];

  // create it if it doesn't
  if (path == nil) {
    path = [NSString stringWithFormat:@"%@%@", 
            [[NSBundle mainBundle] bundlePath], @"/userSettings.plist"];
  }

  // and write the new settings to file
  [self.userSettings writeToFile:path atomically:YES];

  // load managed object context
  [self loadMOC];

  WorkoutPlan *currentPlan = [[WorkoutPlan alloc] getActiveWorkoutPlan];

  [currentPlan setManagedObjectContext:self.managedObjectContext];

  // if user has no plan or is changing plans, create new plan and first workout
  if (currentPlan == nil || 
      ([self.userSettings valueForKey:@"plan"] != currentPlan.planId)) {

    // create a workoutPlan object
    WorkoutPlan *workoutPlan = [[WorkoutPlan alloc] initWithEntity:
                                [NSEntityDescription entityForName:@"WorkoutPlan" 
                                            inManagedObjectContext:self.managedObjectContext] 
                                    insertIntoManagedObjectContext:self.managedObjectContext];

    // set attributes to values from userSettings and save object
    [workoutPlan createWorkoutPlanWithId:[self.userSettings valueForKey:@"plan"] 
                                schedule:[self.userSettings valueForKey:@"schedule"]
                             dateStarted:[self.userSettings valueForKey:@"nextDate"]];
  }
  // if user is just changing schedule, update schedule of current plan
  else if (![currentPlan.schedule isEqualToString:[self.userSettings valueForKey:@"schedule"]]) {
    [currentPlan setSchedule:[self.userSettings valueForKey:@"schedule"]];

    [currentPlan saveMOC];
  }
}

- (void)loadMOC
{
  AppDelegate *delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
  self.managedObjectContext = delegate.managedObjectContext;
  self.model = [[self.managedObjectContext persistentStoreCoordinator] managedObjectModel];
}

- (void)createWorkoutPlanWithId:(NSNumber *)planId schedule:(NSString *)schedule 
                    dateStarted:(NSDate *)dateStarted
{ 
  [self deactivateCurrentPlan];

  // set workout plan attributes
  [self setPlanId:planId];
  [self setIsActive:[NSNumber numberWithBool:YES]];
  [self setSchedule:schedule];
  [self setDateStarted:dateStarted]; 

  // create first workout and add to workout plan

  Workout *firstWorkout = [NSEntityDescription
                          insertNewObjectForEntityForName:@"Workout"
                          inManagedObjectContext:self.managedObjectContext];

  [firstWorkout setManagedObjectContext:self.managedObjectContext];

  [firstWorkout createFirstWorkoutForPlan:self onDate:dateStarted];

  [self addWorkoutsObject:firstWorkout];

  [self saveMOC];
}

- (void)createFirstWorkoutForPlan:(WorkoutPlan *)plan onDate:(NSDate *)startDate
{
  // set workout attributes
  [self setDate:startDate];
  [self setIsCompleted:[NSNumber numberWithBool:NO]];
  [self setWorkoutId:[NSNumber numberWithInt:1]];

  NSArray *exerciseList = [self getExercisesForWorkout:self inPlan:plan];

  // iterate over exercises in spec and create them
  for (NSDictionary *exerciseSpec in exerciseList) 
  {
    // create a exercise MO
    Exercise *exercise = [NSEntityDescription
                          insertNewObjectForEntityForName:@"Exercise"
                          inManagedObjectContext:[plan managedObjectContext]];

    [exercise setManagedObjectContext:[plan managedObjectContext]];
    [exercise createExerciseForWorkout:self withSpec:exerciseSpec];

    // add exercise to workout object
    [self addExercisesObject:exercise];
  }

}

- (void)createExerciseForWorkout:(Workout *)workout withSpec:exerciseSpec
{
  // set exercise attributes
  self.exerciseId = [exerciseSpec valueForKey:@"id"];
  self.isPersonalRecord = [NSNumber numberWithBool:NO];

  NSArray *sets = [exerciseSpec valueForKey:@"sets"];
  int i = 1;
  for (NSNumber *setReps in sets)
  {
    // create a set MO
    Set *set = [NSEntityDescription
                          insertNewObjectForEntityForName:@"Set"
                          inManagedObjectContext:[workout managedObjectContext]];

    [set setManagedObjectContext:[workout managedObjectContext]];

    // set set attributes
    set.order = [NSNumber numberWithInt:i];
    set.repetitions = setReps;
    set.weight = [exerciseSpec valueForKey:@"default_weight"]; 

    // add set to exercise object
    [self addSetsObject:set];
    i++;
  }
}

Upvotes: 1

Views: 1535

Answers (3)

dawid
dawid

Reputation: 374

I had a similar problem. The parent-child relationship worked when the app was running but after re-start only the latest child record was retrieved.

I was adding the children like this:

  • create the child record
  • set the child's parent attribute, set the child's other attributes
  • add the child to the parent using the parent's add method

I found that it was fixed if I did it like this:

  • create the child record
  • add the child to the parent using the parent's add method
  • set the child's parent attribute, set the child's other attributes

Upvotes: 1

bmcwhirt
bmcwhirt

Reputation: 1

I'm having this exact same problem but my model is pretty complex. My app creates the entities and relationships on startup if they don't already exist. If they are created and I don't exit the app, I'm able to fetch an entity with a to-many relationship and see the correct count of related objects. If I exit my app and restart it (it now knows it doesn't have to create a default set of data) then the relationships are returning a null set. I can't figure it out.

EDIT: I figured out that my problem relates to an Ordered set relation. I had to use a Category to create a work around (found on stack overflow) to insert new entries into an ordered set. So I'm guessing that has something to do with it.

Upvotes: 0

Jody Hagins
Jody Hagins

Reputation: 28409

Core Data is complex. There could be dozens of things to check, any one thing which could be causing issues.

How many MOCs are you using? How are you saving? Many more questions...

I would suggest turning on the SQL debugging flag (-com.apple.CoreData.SQLDebug 1) in the EditScheme for arguments when starting the application.

Run your code, and see what is actually going on.

Relationships resolve to a fault when fetched, unless you override it in the fetch request.

If you are using more than one MOC in a parent/child relationship, the save from the child to the parent just puts data into the parent, it does not really save it. If using UIManagedDocument, it's a whole different set of issues...

I hope this does not sound harsh. Be prepared to provide a whole lot of information for a Core Data question, other than "this is not saving and here is some debugging output."

Basically, how CoreData works depends on how the stack is created, whether using UIManagedDocument or not, multiple threads, how creating objects, how saving them, options on fetch requests, and a whole lot more things.

It's actually not that complex, but there are lots of customizations and special cases depending on how it is used.

EDIT

Post the code that creates the objects/relationships.

Also, try the fetch with a manual fetch request instead of the template. When you look at the data in the database, do you see the foreign keys for the relationships set appropriately?

Run it all again with debugging enabled to see exactly what the SQL is doing. That is more valuable that your own debugging output.

Upvotes: 0

Related Questions