3

I am quite new into saving into coreData and using iOS dev.

What I am trying to achieve:

I want to be able to have a user in my db that has a unique identifier / is pulled with idFB and that user can create and retrieve their work out routines.

How far have I gone?

I managed (I think) to create a method that properly retriev the routineName from the Routine entity that is associated with the right User. See the fetch method.

My problem:

I think I am not saving with the right entities relationship association User (usersExercise) <--->> Routine (userID). In order words I think my save method is not right... as I am saving the whole user to userID and it just doesnt feel right? Mainly because when it spits out the Routine.userID it pulls the whole associated user instead of a specific ID? i dont really know what to expect

Could anyone please help me build these method properly? I am very confused with the whole process of coreData saving and making the right relationships.

enter image description here

- (void) save {
    Routine *newRoutine = [NSEntityDescription insertNewObjectForEntityForName:@"Routine" inManagedObjectContext:context];

    newRoutine.users = [self getCurrentUser];
    newRoutine.routineName = @"myRoutine Test Name";
    NSError* error;
    [context save:&error ];

    NSLog(@"Saved now try to fetch");
    [self fetch];

}
-(void) fetch {
    NSFetchRequest *fetchRequestItems = [[NSFetchRequest alloc] init];
    NSEntityDescription *entityItem = [NSEntityDescription entityForName:@"Routine" inManagedObjectContext:context];
    [fetchRequestItems setEntity:entityItem];
    User* user = [self getCurrentUser];
    // if i try [[self getCurrentUser] usersRoutine] it shows an error

    [fetchRequestItems setPredicate:[NSPredicate predicateWithFormat:@"users == %@",user]];

    //Sort by last edit ordered
    NSArray *sortDescriptors = [NSArray arrayWithObjects:nil];
    [fetchRequestItems setSortDescriptors:sortDescriptors];
    NSError *error = nil;
    NSArray* Routines = [context executeFetchRequest:fetchRequestItems error:&error];
    NSLog(@"result %@", [(Routine *)Routines[0] users]  );


}
-(User *)getCurrentUser {
    NSEntityDescription *entityDesc = [NSEntityDescription entityForName:@"User" inManagedObjectContext:context];
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    [request setEntity:entityDesc];

    if (_appDelegate.isFB)
    { 
        request.predicate = [NSPredicate predicateWithFormat:@"idFB LIKE %@",_appDelegate.fdID];
        NSError *error = nil;
        NSArray *matches = [[context executeFetchRequest:request error:&error] mutableCopy];

        return (User *)matches[0];
    } else
    {
        NSLog(@"CreateRoutinePOPUP NON FB TO BE TESTED");
        request.predicate = [NSPredicate predicateWithFormat:@"email LIKE %@",_appDelegate.currentUser];
        NSError *error = nil;
        NSArray *matches = [[context executeFetchRequest:request error:&error] mutableCopy];

        return (User *)matches[0];
    }

This is what the NSLog in fetch is printing:

2013-04-28 22:33:26.555 iGym[7916:c07] result <User: 0xa480580> (entity: User; id: 0xa495a00 <x-coredata://D87CEBB4-016C-4A1B-802C-2D1117BB3E51/User/p1> ; data: {
    dob = "1986-12-26 00:00:00 +0000";
    email = ".com";
    firstTime = nil;
    gender = male;
    height = nil;
    idFB =3333;
    idUserExternal = 0;
    idUserInternal = 0;
    isPT = nil;
    language = "en_US";
    location = "London, United Kingdom";
    metricSystem = nil;
    name = Joan;
    nickname = nil;
    password = nil;
    surname = Thurft;
    usersExercise = "<relationship fault: 0xa4824a0 'usersExercise'>";
    usersRoutine =     (
        "0xa495f00 <x-coredata://D87CEBB4-016C-4A1B-802C-2D1117BB3E51/Routine/p6>",
        "0xa4877e0 <x-coredata://D87CEBB4-016C-4A1B-802C-2D1117BB3E51/Routine/p1>",
        "0xa4877f0 <x-coredata://D87CEBB4-016C-4A1B-802C-2D1117BB3E51/Routine/p2>",
        "0xa487800 <x-coredata://D87CEBB4-016C-4A1B-802C-2D1117BB3E51/Routine/p3>",
        "0xa487810 <x-coredata://D87CEBB4-016C-4A1B-802C-2D1117BB3E51/Routine/p4>",
        "0xa487820 <x-coredata://D87CEBB4-016C-4A1B-802C-2D1117BB3E51/Routine/p5>"
    );
    weight = nil;
})

also when i add NSLog(@"get current result %@", [(User *)matches[0] usersRoutine] ); to the getCurrentUser method I get the whole user's data and the relationship says

usersExercise = "<relationship fault: 0xa464730 'usersExercise'>";
Jonathan Thurft
  • 4,087
  • 7
  • 47
  • 78

2 Answers2

7

Core Data is not exactly like working with a standard database where you assign some foreign key like userID to another table where you want a relationship to the User object and then use that foreign ID to find the relationship like exercise.where('user_id = ?', userID). Instead, you define actual relationships and let Core Data handle everything behind the scenes for setting up any join tables or foreign keys.

Instead of how you have it set up, you'd just have in the User entity two relationships for exercises and routines that are mapped to the Exercise and Routine entities and then you'd have an inverse relationship on the Exercise and Routine called users if it's a has-and-belongs-to-many relationship. So now, you need to replace usersExercise with exercises, usersRoutine with routines and then userID with users for the Exercise and Routine entities.

Even if you don't actually need that inverse relationship, you still need it since Core Data uses it for data integrity purposes and Xcode will give you a warning if you leave it unpopulated.

When you set up those relationships, then you would call the routines or exercises like user.exercises which will return the associated set of exercises for that user. As you noticed, Core Data will return what they call a fault for a relationship that will get fired and the data returned when you actually need the contents of that relationship. Faults are there so that you are only returned exactly what info you need instead of running unnecessary queries on the data set.

Another thing to note is that Core Data doesn't reference unique id's like userID as you are doing. Instead, each object within Core Data has a unique ID found by [objectName objectID] (which is only permanent after it's been saved to the data store). You really shouldn't need to setup a unique ID as an attribute on an entity except for special cases.

Also, you really shouldn't need to use those unique objectID's unless you're passing objects around like in a multi-threaded application for background processing in which case NSManagedObjectID is thread-safe and you can use it to find the object again on a background thread/managed object context.

I'd really recommend reading a good intro to Core Data such as http://www.raywenderlich.com/934/core-data-on-ios-5-tutorial-getting-started

It can be a little strange at first converting to Core Data if you're used to normal database setup/architecture, but once you get used to it, it's actually a lot faster and handles all of the hard work behind the scenes for you.


Update from the comments:

You're misunderstanding the concept of relationships in Core Data. In Core Data, a relationship does not return an associated ID like a typical database join relationship would. Instead, it returns a fault which gets fired when you need the data from that relationship. So it's not returning the entire User object, but a fault to the associated User object which gets fired and queried when you do something like exercise.user.name

Your code is working exactly like it should be when you're saving, you are just under the incorrect assumption that it's not.

iwasrobbed
  • 46,496
  • 21
  • 150
  • 195
  • Thank you for the explanation! :)... I think I have setted the relationship identities right because CD doesnt throw any error or warnings. So if i am correct you are saying that the associations that I've made in the CD graphic are wrong? What do you think could be the solution for me issue? Thanks – Jonathan Thurft Apr 28 '13 at 22:14
  • Yes, they are how you would setup a typical database join relationship, but not how you setup relationships on Core Data. Instead, you need to replace `usersExercise` with `exercises`, `usersRoutine` with `routines` and then `userID` with `users` for the `Exercise` and `Routine` entities. – iwasrobbed Apr 28 '13 at 22:18
  • I'm assuming that it is a `has-and-belongs-to-many` relationship so make sure those options are selected in the Core Data menus for those relationships. – iwasrobbed Apr 28 '13 at 22:19
  • Thats an arbitrary change of name of the relationship with no effect on the actual functionality. I updated my question with a picture of my datamodel for you to see. Then if my relationships are correct. what i am doing wrong in my code? – Jonathan Thurft Apr 28 '13 at 22:23
  • 1
    You're misunderstanding the concept of relationships in Core Data. In Core Data, a relationship does not return an associated ID like a typical database join relationship would. Instead, it returns a `fault` which gets fired when you need the data from that relationship. So it's not returning the entire `User` object, but a `fault` to the associated `User` object which gets fired and queried when you do something like `exercise.user.name` – iwasrobbed Apr 28 '13 at 22:28
  • Could you provide me an example of how to query and / or store in the right way please? – Jonathan Thurft Apr 28 '13 at 22:30
  • @JonathanThurft I already have in the link in the answer: http://www.raywenderlich.com/934/core-data-on-ios-5-tutorial-getting-started – iwasrobbed Apr 28 '13 at 22:31
  • By your edit to your answer that you said that "I think my code is not working properly but it is" then you are saying that all my fetch and save code is correct or only that the save function is correct??? – Jonathan Thurft Apr 28 '13 at 22:32
  • I'm saying that your code is working. You are under the assumption that you're storing an entire `user` in the Routine, but you're just storing a relationship. I would however, do the opposite of how you have it: `[user.addRoutinesObject:routine];` and then Core Data will automatically add the `user` to the associated `Routine` object without you having to worry about it (thanks to that inverse relationship you setup) – iwasrobbed Apr 28 '13 at 22:38
  • can you please help me with this https://stackoverflow.com/questions/50055953/core-data-store-data-faster-and-search-using-predicate-and-short-descriptor – Iraniya Naynesh Aug 21 '18 at 07:42
0

You need to use the provided method to add a "many object" in the one to many object. In your case it is called addRoutineObject:

Try this new save method:

- (void) save {

    Routine *newRoutine = [NSEntityDescription insertNewObjectForEntityForName:@"Routine" inManagedObjectContext:context];
    newRoutine.routineName = @"myRoutine Test Name";

    NSEntityDescription *entityDesc = [NSEntityDescription entityForName:@"User" inManagedObjectContext:context];
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    [request setEntity:entityDesc];


    NSArray *matches;
    NSError *error = nil;
    if (_appDelegate.isFB)
    { 
        request.predicate = [NSPredicate predicateWithFormat:@"idFB LIKE %@",_appDelegate.fdID];
        matches = [[context executeFetchRequest:request error:&error] mutableCopy];

    } else
    {
        NSLog(@"CreateRoutinePOPUP NON FB TO BE TESTED");
        request.predicate = [NSPredicate predicateWithFormat:@"email LIKE %@",_appDelegate.currentUser];
        matches = [[context executeFetchRequest:request error:&error] mutableCopy];
    }

    if (matches.count == 0)
    {
        NSLog(@"no user matched");
    }
    else
    {
        User *aUser = [matches objectAtIndex:0];
        [aUser addRoutineObject:newRoutine];
        if (![context save:&error])
        {
            NSLog(@"couldn't save: %@", [error localizedDescription]);
        }
    }
   }
user523234
  • 14,323
  • 10
  • 62
  • 102