iPhone - Core Data Questions? - iphone

Above is my data model diagram.
Everytime i create a Car whcih contains a model and make, it adds a car object to core data.
The problem is that it also adds 1 make and 1 model to the core data, so i get duplicates.
for example, in the following code, it saves 1 car object 2 models, and 2 makes,
so I get a duplicate Make in my table (2 "Nissan").
How can i avoid this? is it possible to create primary keys?
In the following example i want to assume that make and models already exists, and a car is only referencing to them, so how can I avoid inserting into make, and model, and only insert into car?
- (void)MyCode
{
[self AddCar:#"Nissan" #"Rogue"];
[self AddCar:#"Nissan" #"Murano"];
}
- (void)AddCar :(NSString*)_make :(NSString*)_model
{
Car *car = [[Car alloc] initWithEntity:[NSEntityDescription entityForName:#"Car"
inManagedObjectContext:self.managedObjectContext]
insertIntoManagedObjectContext:self.managedObjectContext];
Make *make = [[Make alloc] initWithEntity:[NSEntityDescription entityForName:#"Make"
inManagedObjectContext:self.managedObjectContext]
insertIntoManagedObjectContext:self.managedObjectContext];
make.name = _make;
Model *model = [[Model alloc] initWithEntity:[NSEntityDescription entityForName:#"Model"
inManagedObjectContext:self.managedObjectContext]
insertIntoManagedObjectContext:self.managedObjectContext];
model.name = _model;
car.make = make;
car.model = model;
[self saveContext];
}

There are a few issues here.
The data model shows that the Make and Model entities each have a to-one relationship with Car. Therefore, there can be only one Car for each Make and only one Car for each Model. In other words, given that data model, Nissan can only make one Car. You probably want a to-many cars relationship in Model so that Nissan can make more than one Car. Same thing with Make so that there can be more than one Car with a given Make.
The way NSManagedObjects are inserted into the managed object context is incorrect. It should be done like this:
Car *car = [NSEntityDescription
insertNewObjectForEntityForName:#"Car"
inManagedObjectContext:self.managedObjectContext];
Finally, your code shows that a new Make and Model entity are created for each Car. If you are starting with the name of a Make, you probably want to search the managed object context for a Make that has a matching name. If found, just set the make relationship of the new Car to the Make entity that is already in the context. If not found, create a new Make entity and set up a relationship to that.

don't create new makes and models but use those stored already in core data.
Write a controller that searches for a make and model with NSPredicate. something like this:
- (id)modelNamed:(NSString *)searchName addIfMissing:(BOOL)add {
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity = [NSEntityDescription entityForName:self.entityName inManagedObjectContext:self.managedObjectContext];
[request setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name == %#", searchName];
[request setPredicate:predicate];
NSError *error;
NSArray *results = [self.managedObjectContext executeFetchRequest:request error:&error];
if (!results) {
LogError(#"Error [%s], %#, %#", _cmd, error, [error localizedDescription]);
return nil;
}
if ([results count] == 0) {
if (add) {
// add model with new name...
id newModel = ...
return newModel;
}
else
return nil;
}
return [results objectAtIndex:0];
}

Related

how to properly save into coredata one to many relationship

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.
- (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'>";
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.
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]);
}
}
}

CoreData Edit one attribute in One to Many relationship

I am new to CoreData and been looking to all the books and examples but none of them really tell me how to do this, so any help is greatly appreciated.
Basically, I have 2 Entities in one to Many relation. [other relationships are not important in this case]
The relationship and entities:
Now I can get All the MedicalCondition Entity based on given Profile Entity using NSFetchRequest
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"MedicalCondition" inManagedObjectContext:delegate.managedObjectContext];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"condition" ascending:YES]];
request.predicate = [NSPredicate predicateWithFormat:#"MedicalToProfile = %#", myProfile];
//request.fetchBatchSize = 20;
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:delegate.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
NSError *error;
BOOL success = [frc performFetch:&error];
NSArray *fetchedObjectsFromCore;
[request release];
if (success) {
fetchedObjectsFromCore = [frc fetchedObjects];
}
This is ok. Now the problem comes in when I want to update one particular entry. I am not sure how to do it. I can add more MedicalCondition object just fine. But when it comes to edit, I am not sure.
The only way I cant think of is to have "ID" attribute in entity. I think there must be a better solution than this. Please help ! Thankz so much.
If you have a Core Data object from a valid context, then editing it is very easy. Modify the object data, then save its context. Let's say you have a MedicalCondition object that you got hands on somehow.
MedicalCondition *condition;
// modify a field
condition.date = [NSDate date];
// save
NSError *error;
[managedObjectContext save:&error];
Also if you have a given Profile object, you can get all associated MedicalCondition objects directly without having to perform a fetch as long as you do not care about order.
Profile *someonesProfile = ...;
someonesProfile.conditions
// and access a profile from a given MedicalCondition since
// it seems to be a bi-directional relationship.
MedicalCondition *someCondition = ...;
someCondition.profile.dateofbirth;
You should give more meaningful names to the relationships instead of MedicalToProfile, ProfileToMedication, etc. For example, instead of ProfileToMedical, maybe use:
medicalConditions
which is semantically nicer, and reads better in code:
someonesProfile.medicalConditions
Thankz again Anurag.
I kindna got it working.
this is how I did it
NSSet *newMedical = myProfile.ProfileToMedical;
NSMutableArray *arrayMedical = [NSMutableArray arrayWithArray:[newMedical allObjects]];
MedicalCondition *c = [arrayMedical objectAtIndex:1];
c.condition = #"Amazing";
And It update the right place :)
But now when I call again
NSSet *details = myProfile.ProfileToMedical;
NSMutableArray *arrayDetail = [NSMutableArray arrayWithArray:[details allObjects]];
The return NSSet is show the updated condition to be at other index. I understand that is because myProfile.ProfileToMedical is unsorted? so I must always sort the array first before I view/edit attribute to ensure the consistency?
Thankz again

Core-Data: How to set up relationship from one object to specific one in many objects with same name?

I have a question in core data:
there are 2 Entities in the project, Books and Pages;
there are 3 Objects books user created in Entity Books;
there are several Objects pages user created in Entity Pages;
relationship inbetween is one page belongs to one book, one book has many pages.
and my question: there are 3 book with the same object name "book",and each has unique attribute .bookName : #"metal" ;#"plastic" ;#"glass". how to set page to the book with .bookName = #"glass" ?
//In BookViewController
Books *book = (Books *)[NSEntityDescription insertNewObjectForEntityForName:#"Books" inManagedObjectContext:managedObjectContext];
//textView.text is user input text
book.bookName = textView.text;
//In PageViewController
Pages *page = (Pages *)[NSEntityDescription insertNewObjectForEntityForName:#"Pages" inManagedObjectContext:managedObjectContext];
page.itsbook = WHAT?
Thank you for reading, I stuck here like: a day, really appreciate your help, love you!
If you need to find a specific book then you need to use a NSFetchRequest to ask Core data for it.
Quite some code is needed, so you probably add a convinience method to your Bookclass that looks something like this:
+(Book*)bookWithName:(NSString*)name
{
// 0. I assume you have something like this to get the context…
NSManagedObjectContext* context = [NSManagedObjectContext threadLocalContext];
// 1. Create an empty request
NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];
// 2. Set the entity description for the object to fetch
NSEntityDescription* entity = [NSEntityDescription entityForName:#"Book"
inManagedObjectContext:context];
[request setEntity:entity];
// 3. Set a predicate asking for objects with a mathing bookName property
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"%K LIKE[cd] %#",
#"bookName",
name];
[request setPredicate:predicate];
// 4. Execute the request on the managed object context.
NSArray* objects = [context executeFetchRequest:request error:NULL];
// 5. Result is an array, maybe handle empty array and many objects?
return [objects lastObject];
}
I'm not sure what you mean. If you want to insert a page as a subset of the book you want this line of code:
[book addPageToBookObject:page]; // the method will be the relationship name
If you want to get the book from the page object you'll need an inverse relationship from the pages to the book.

iPhone SDK: Core Data and uniquing?

Either I'm not understanding what the term "uniquing" means in Core Data, or I'm not fetching my data properly. I have a pretty simple data model. Three entities: Community, Asset, and Category. Each Community has a relationship to multiple categories. Each category has a relationship to multiple assets. Each asset that is created must have one and only one category.
In the code I've posted, I'd like to output all the categories that a specific community has into the console. I thought that because of Core Data's uniquing capability, only one category of the same name could exist at a time (name is the only attribute for a category). However, when I print to the console, I'm getting duplicate category names.
// Fetch Community instances in the database, and add them to an NSMutableArray
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *community = [NSEntityDescription entityForName:#"Community" inManagedObjectContext:managedObjectContext];
[request setEntity:community];
// Only return the community instances that have the cityName of the cell tapped in the CommunitiesNonEditableTableViewController
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(cityName like %#)", cellCityName];
[request setPredicate:predicate];
NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
// Handle the error.
}
// Set communitiesArray with mutableFetchResults
[self setCommunitiesArray:mutableFetchResults];
[mutableFetchResults release];
[request release];
// Creates a community instance using the community stored in the array at index 0. This is the only community in the array.
Community *communityInstance;
communityInstance = [communitiesArray objectAtIndex:0];
// Retrieves existing categories of assets in the community, and adds them to an NSSet
NSSet *communityCategoriesSet = communityInstance.categories;
// Converts NSSet to an NSArray with each category as an index
NSArray *communityCategoriesArray = [communityCategoriesSet allObjects];
// For loop that iterates through the array full of categories, retrieves the names of each category, and adds it to an NSMutableArray
categoryNames = [[NSMutableArray alloc] init];
int i;
for (i = 0; i < [communityCategoriesArray count]; i++) {
Category *categoryInstance;
categoryInstance = [communityCategoriesArray objectAtIndex:i];
[categoryNames addObject:categoryInstance.name];
}
// Prints array full of category names to console
NSLog(#"%#", categoryNames);
When I execute this, I get duplicate names in the console. Why?
Uniquing means that each object in the object graph is itself unique. It does not mean that the attributes of any two objects are not identical. Uniquing is about relationship not attributes. No two objects can occupy the exact same position in the object graph.
As to why you get multiple categories in the output: The simplest explanation is that communityInstance.categories is a to-many relationship. (Since it has a plural name and you assign it to set.) In a to-many relationship, the context does not force a single object on the other end of the relationship.

Setting up a one to many relationship in Core Data on the iPhone

I am new to iPhone development trying to figure out how to setup a one to many relationship Core Data. I have two Entities setup One Leagues which has a one to many relationship to an entity Teams. So lot's of teams in a league. Think all the teams that play baseball and are in the MLB.
I am pre-filling the data when the user first enter's their username and password, so I have the leagues populate first, which I have no problem with. Then when I begin adding the teams I do a search looking for league. Code is below.
- (NSMutableSet *)checkItem:(int *)identifier inTable:(NSString *)table inContext:(NSString *)context {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:table inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:context,identifier];
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray *items = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
[fetchRequest release];
if ([items count] >= 1) {
return [items objectAtIndex:0];
} else {
return nil;
}
}
By calling this and then set up the team entry and save ('league' is my relationship):
NSMutableSet *leagueObjectSet = [self checkItem:lid inTable:#"Leagues" inContext:#"id=%i"];
NSManagedObject *teamObject = [NSEntityDescription insertNewObjectForEntityForName:#"Teams" inManagedObjectContext:managedObjectContext];
[teamObject setValue:[NSNumber numberWithInt:tid] forKey:#"id"];
[teamObject setValue:[dict valueForKey:#"name"] forKey:#"teamName"];
[teamObject setValue:leagueObjectSet forKey:#"league"];
[self saveAction];
When I run this I get this error message in the console.
"the entity Teams is not key value coding-compliant for the key "league"."
Am I doing this right? With everything I have read it seems like I am. I come from a MySQL background so be gentle!
From your description, I believe that you have a wrong setup for your one to many relationship. Assuming you have two entities named League and Team, and that you have correctly setup the relationship, when you want to relate a team to a league, your league object must not be a NSMutableSet as shown in your code snippet: it must be a NSManagedObject too. A League object, being on the to many side of the relationship will have a property named teams or similar which will be a NSMutableSet. A Team object, on the one side of the relationship will have instead a property named league or similar which is a League object.
Then, to setup the relationship you can do as you did using key value coding, but for performance reasons it is best to do
teamObject.id = yourId;
teamObject.teamName = #"your team name";
teamObject.league = yourLeagueObject;
This is much faster and suggested by Apple.