Entity relationship between NSManagedObjects issue - ios

I'm running into a CoreData problem related to one-to-many relationships.
Entity 1 - Authors has a one-to-many relationship with Entity 2 - Books. I think Books has a one-to-one relationship with Authors.
Since there are multiple books per author in the author object I have
#property (nonatomic, retain) NSSet *book;
The corresponding property in the book object is:
#property (nonatomic, retain) Authors *author;
When the app downloads books from the server through an API, after saving the book, I am trying to also save the author and associate the book with the author with the following code:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Books" inManagedObjectContext:self.managedObjectContext];
NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
record setValue:bookname forKey:#"bookname"];
if ([self.managedObjectContext save:&error]) {
Authors *newAuthor = [NSEntityDescription insertNewObjectForEntityForName:#"Authors" inManagedObjectContext:self.managedObjectContext];
newAuthor.aid = authorid;
newAuthor.book = record;
}
This code has worked for me for one to one relationships, but in this case, is throwing following exception error:
'NSInvalidArgumentException', reason: 'Unacceptable type of value for
to-many relationship: property = "book"; desired type = NSSet; given
type = Books;
Can anyone suggest how to fix this?
Update:
I also tried switching it around to read:
record.author = newAuthor;
But this gives error
"property 'author' not found on object of type NSManagedObject"
although there is such a property defined in the Books object (as shown above).

Since the book property is a set, CoreData should have created an Authors method such as addBookObject:. When you created your custom entity classes, there should be a file with a name similar to "Authors+CoreDataProperties.h". Look in there for defined methods.
Your second option should also work if you use Books instead of NSManagedObject.
i.e.:
Books *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];

U can generate NSManagedObject subclass, after just create few objects, set relations and save context once, someting like:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Book" inManagedObjectContext:self.context];
Book *newBook = [[Book alloc] initWithEntity:entity insertIntoManagedObjectContext:self.context];
newBook.bookName = #"My new book";
NSEntityDescription *entity2 = [NSEntityDescription entityForName:#"Record" inManagedObjectContext:self.context];
Record *newRecord = [[Record alloc] initWithEntity:entity2 insertIntoManagedObjectContext:self.context];
newRecord.name = #"New record";
newBook.record = newRecord;
[newRecord addBook:[NSSet setWithObject:newBook]];
[self.context save:nil];
This is sample for db like below:
To autogenerate classes - select Entities in *.xcdatamodel file and press File->New->File select CoreData section and NSManaged Object subclass, go by wizard steps.
You will get something like
And even more:
Good tutorial u can also found here

Related

Save to more than one entity

For a detail view I would like to let the user leave notes for each item. The app is for a data-driven website. In the web version, the web app stores notes in a separate table with a field for the itemid.
In Core Data I have an entity of items and another entity of notes. The notes entity has an attribute called itemid. When user creates a note the first time, it stores the itemid in the note record.
My question is when you pull up the item for editing how can you simultaneously pull up the right note based on the note having a certain itemid?
In a database situation you could do a join, or from a web page you could make two separate requests to the two tables but I am somewhat flummoxed by how to do this with Core Data.
Do you have to put a relationship to the note and therefore have the noteid in the item row?
If so would you be able to access the note attribute using the item object?
Thanks in advance for any suggestions.
This is what I am using to save information. I just don't know how to make sure I'm saving it for right note.
self.managedObjectContext = [IDModel sharedInstance].managedObjectContext;
NSString *noteText = _notesView.text;
NSNumber *itemId = self.item.itemid;
// Populate Record
[self.note setValue:noteText forKey:#"note"];
[self.note setValue:itemId forKey:#"itemid"];
Model (simplified):
Item:
name NSString
itemid: Integer 64
Note:
note NSString
noteid: Integer 64
itemid: Integer 64
Edit:
Code to try to link note and item while creating both...
//in save method
// Create Entity
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Notes" inManagedObjectContext:self.managedObjectContext];
// Initialize New Record ie newNote
NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
// Populate Record
[record setValue:note forKey:#"note"];
[record setValue:localid forKey:#"localnid"];
// Save Record
NSError *error = nil;
if ([self.managedObjectContext save:&error]) {
// If note saved, save new item…
if (itemlength>1) {
Items *newItem = [NSEntityDescription insertNewObjectForEntityForName:#“Item” inManagedObjectContext:self.managedObjectContext];
newItem.item = item;
newItem.note = self.note
//This is null as note does not seem to pick up newly created note.
if (![self.managedObjectContext save:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
}
}
Yes, you should use a relationship between the Item and Note entities. To create a relationship just Ctrl-drag from one entity to the other in the data model editor. Note that Xcode automatically adds an inverse relationship:
I've renamed the relationships for clarity - you can tailor the details of the relationship (name, one-one v one-many, delete rule, etc) in the panel on the right. In the example above, the Item entity has three properties: 2 attributes and 1 relationship. Given an Item object, say myItem, the values for these properties can be accessed using the Key Value Coding methods: valueForKey: and setValue:forKey:. For example, if attribute is defined as a string:
NSString *myStringValue = [myItem valueForKey:#"attribute"];
[myItem setValue:#"new value for attribute" forKey:#"attribute"];
That's very long-winded. So to make life easier, use the "Create NSManagedObject subclass..." option. Xcode will configure each entity to be a subclass of NSManagedObject and will create new class files (.h/.m or .swift) with details of the properties. For the example Item:
#property (nullable, nonatomic, retain) NSString *attribute;
#property (nullable, nonatomic, retain) NSString *attribute1;
#property (nullable, nonatomic, retain) Note *note;
The thing to realise is that the note relationship is of class Note. It's not a foreign key, or a noteid, that you have to use to lookup the corresponding Note object. It is the Note object. Under the hood, CoreData is adding primary keys and foreign keys to the underlying tables, but all that aggravation is abstracted away.
Having created the subclasses, you can use the dot-accessors for the object properties:
NSString *myStringValue = myItem.attribute;
myItem.attribute = #"new value for attribute";
For the relationship, if you have an Item object called myItem and a Note object called myNote, you can set the relationship value with:
myItem.note = myNote;
or equivalently:
myNote.item = myItem;
(Note: use one or the other, not both; CoreData automatically sets inverse relationships for you).
Now, you have the added complication of a web server from which the Item objects and Note objects are downloaded. And on your web server, your Notes table has a field for the itemid, which is used to link Items and Notes. At some point, you want to link up Note objects and Item objects using the itemid. The usual approach would be to do it once (as soon as the CoreData objects are synchronised from the server), set the relationship accordingly, and thenceforth use the relationship rather than the itemid to get the note for a given item. For example, if you are creating a new Note object, and the itemid from the server is "1234", you might do this:
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Item"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"itemid == %#", #"1234"];
NSError *error;
NSArray *results = [context executeFetchRequest:fetchRequest error:&error];
// should check for nil results/error
if (results.count > 0) {
// found (at least) one Item with itemid == #"1234"
// use the first to set the relationship
newNote.item = results[0];
}
Then whenever you have a particular Item object, you can access the corresponding note using
Note *myNote = myItem.note;
Furthermore, you can cascade the dot-notation, so get the value of attribute for the Note for myItem, use:
NSString *noteText = myItem.note.attribute;
EDIT
Your save method is very close: either set self.note = record before you save, or use newItem.note = record:
//in save method
// Create Entity
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Notes" inManagedObjectContext:self.managedObjectContext];
// Initialize New Record ie newNote
NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
// Populate Record
[record setValue:note forKey:#"note"];
[record setValue:localid forKey:#"localnid"];
// Save Record
NSError *error = nil;
if ([self.managedObjectContext save:&error]) {
// If note saved, save new item…
if (itemlength>1) {
Items *newItem = [NSEntityDescription insertNewObjectForEntityForName:#“Item” inManagedObjectContext:self.managedObjectContext];
newItem.item = item;
newItem.note = record;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
}
}
}
To reach your goal you need to create a NSFetchRequest passing it the right NSPredicate.
The fetch request will be run against your Note entity. The predicate will allow you to specify that the Note object you want to retrieve is the one for that specific noteid.
So, if you have a 1-to-1 relationship between Item and Note, the NSPredicate should like the following:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"relationshipToItem.propertyForItemId == %#", yourItemId];
[request setPredicate:predicate];
Here I suppose you've a created a relationship between the two entities, otherwise you need to do it manually. Can you provide how your model looks like?

how to fetch data from two tables in core data objective-c

I'm new in use Core Data. I have a two entities CDContact and CDAddress. I have problem to fetch a second entity (CDAddress).
self.fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"CDContact"];
[self.fetchRequest setSortDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"firstName" ascending:YES]]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:self.fetchRequest managedObjectContext:[CoreDataManager sharedInstance].managedObjectContext sectionNameKeyPath:nil cacheName:nil];
[self.fetchedResultsController setDelegate:self];
[self fetchResults];
It works but I get only data from CDContact.
I try something like this (I found this on documentation):
NSManagedObjectContext *context = [CoreDataManager sharedInstance].managedObjectContext;
NSEntityDescription *contactEntity = [NSEntityDescription entityForName:#"CDContact" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = contactEntity;
request.relationshipKeyPathsForPrefetching = [NSArray arrayWithObject:#"CDAddress"];
but in this version both entities doesn't work.
I'll be glad for any examples which help me resolve this problem.
As per comments, a fetch will only return results from a single entity. To access the second entity, you either need to run a second fetch, or to use the relationship on your CDContact objects. For example if the relationship is named address,
myCDContact.address
will give the CDAddress for the specified CDContact.
To expand on this, suppose your CDContact entity has attributes firstName and lastName, and a one-one relationship to CDAddress entitled address. Likewise suppose your CDAddress entity has attributes street, town, and zipCode, and the inverse relationship to CDContact entitled contact.
Then to create a new contact named "John Smith" living at "1 Main Street, AnyTown, 90210", you would have code something like this:
CDContact *myCDContact = (CDContact *)[NSEntityDescription insertNewObjectForEntityForName:#"CDContact" inManagedObjectContext:context];
myCDContact.firstName = #"John";
myCDContact.lastName = #"Smith";
CDAddress *myCDAddress = (CDAddress *)[NSEntityDescription insertNewObjectForEntityForName:#"CDAddress" inManagedObjectContext:context];
myCDAddress.street = #"1 Main Street";
myCDAddress.town = #"AnyTown";
myCDAddress.zipCode = #"90210";
// and lastly, to set the relationship between them...
// EITHER
myCDContact.address = myCDAddress;
// OR
myCDAddress.contact = myCDContact;
(Note the either/or: you only have to set the relationship "one-way", CoreData will set the inverse automatically). If you then save the context, and subsequently fetch the contacts, with something like this:
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:#"CDContact"];
[fetch setSortDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"firstName" ascending:YES]]];
NSError *error;
NSArray *results = [context executeFetchRequest:fetch error:&error];
then (assuming the fetch is successful: you should test for nil/error) the array will contain your contacts. Taking the first item in the array, you can then access its properties including the related CDAddress like this:
CDContact *myContact = (CDContact *)[results objectAtIndex:0];
NSLog(#"The contact is %# %#", myContact.firstName, myContact.lastName);
NSLog(#"who lives at %#, %#, %#", myContact.address.street, myContact.address.town, myContact.address.zipCode);
(All the above assumes you have created NSManagedObject subclasses for your entities. If you haven't, you should.)
The answer provided by pbasdf is perfect and very detailed. However, I would like to add this from the apple documentation.
To retrieve data using a managed object context, you create a fetch request. A fetch request is an object that specifies what data you want, for example, “all Employees,” or “all Employees in the Marketing department ordered by salary, highest to lowest.” A fetch request has three parts. Minimally it must specify the name of an entity (by implication, you can only fetch one type of entity at a time). It may also contain a predicate object that specifies conditions that objects must match and an array of sort descriptor objects that specifies the order in which the objects should appear...
Assuming the relationship between the entities CDContact and CDAddress is one-to-one, your CDContact interface file should look something like this:
#interface CDContact : NSManagedObject
#property (nonatomic,strong) NSString *firstName;
#property (nonatomic,strong) NSString *lastName;
#property (nonatomic,strong) CDAddress *address;
#end
This way, once you fetch a given CDContact object, you can access the related CDAddress object using the property "address". But if you need to access all addresses and all contacts, you need to perform two separate fetches.

iOS: core data to many relationship

I have a doubt how to manage a "to many relationship" in core data.
In my example I have the main identity 'Struct' that have a "to many relationship" with another identity called 'Id_loc'
Then, I have this object in a JSON file that is a Struct identity:
{"id":"s1",
"n":"Name Struct",
"id_loc":["l1","l2"]} //id_loc can contain many element
when I parse this object I have id_loc as an array.
Inside Struct class I have two methods:
- (void)addLocObject:(Id_loc *)value;
- (void)addLoc:(NSSet *)values;
then I do this to store id_loc array inside:
Struct *struct = [NSEntityDescription insertNewObjectForEntityForName:#"Struct" inManagedObjectContext:context];
NSArray *array_loc = [element objectForKey:#"id_loc"];
NSSet *set = [NSSet setWithArray:array_loc];
[struct addLoc:set];
Is it a right way?
Is it not necessary to call this?
Id_loc *loc = [NSEntityDescription insertNewObjectForEntityForName:#"Id_loc" inManagedObjectContext:context];
EDIT
Is it the right answer?
Struct *struct = [NSEntityDescription insertNewObjectForEntityForName:#"Struct" inManagedObjectContext:context];
NSArray *array_loc = [element objectForKey:#"id_loc"];
for (id loc in array_loc){
Id_loc *loc = [NSEntityDescription insertNewObjectForEntityForName:#"Id_loc" inManagedObjectContext:context];
loc.ident = loc;
[struct addLocObject:loc];
}
You cannot really save an NSSet into the CoreData just like that. NSSet contains other CoreData entities only which are related to your main object.
To save an array with data you need to use NSData property and use NSKeyedArchiver to archive your NSArray with NSStrings.
However even it's simplest solution there are some limitations. For example you won't be able to use and predicates on those properties. Therefore I would recommend to make another entity which is "Location" and create a location objects based on those "l1", "l2" values.
Yes you use addLocObject: or if you want to add multiple ones you use - (void)addLoc:(NSSet *)values; But you have to create those objects - create them in core data withing your context and than add it to main object.
Probably if you have ID you want also to select first existing locations and create them only if the don't exist.

Trying to dynamically create an NSManagedObject subclass based on NSString value in iOS

I am building an application that utilizes Core Data, which has multiple entities in the Data Model. What I would like to do is create a method which is able to create the appropriate subclass of NSManagedObject based on the name that the method it receives, which would be an NSString.
My method looks like this:
- (NSManagedObject *) addEntity:(NSString *)name {
NSManagedObjectContext *context = [self managedObjectContext];
NSError *error;
//need to add a line here that casts EntityType to of type "name" which is passed into the method.
EntityType *testEntity = [NSEntityDescription insertNewObjectForEntityForName:name inManagedObjectContext:context];
[context save:&error];
return testEntity;
}
Where "EntityType" is of type "name", such that if I pass the name "Manager" into the method, I will create an object of type "Manager". Therefore the above line:
EntityType *testEntity = [NSEntityDescription insertNewObjectForEntityForName:name inManagedObjectContext:context];
would read:
Manager *testEntity = [NSEntityDescription insertNewObjectForEntityForName:name inManagedObjectContext:context];
What do I need to do in order to dynamically create Entities based on the type that I pass into the method? Please note that I am doing this because I have over twenty entities in my application and would like to have just one method that I can use for any of them.
If your issue is the type of testEntity just use a generic NSManagedObject
Anyway a nice solution would be to add a category over NSManagedObject. Here's an implementation which basically replicates the same functionality provided by MagicalRecord.
NSManagedObject+Utilities.h
#interface NSManagedObject (Utilities)
+ (instancetype)my_createAndSave:(NSError **)error;
#end
NSManagedObject+Utilities.m
#implementation NSManagedObject (Utilities)
+ (instancetype)my_createAndSave:(NSError **)error {
NSString *entityName = NSStringFromClass(self);
if ([self respondsToSelector:#selector(entityName)]) {
entityName = [self performSelector:#selector(entityName)];
}
NSManagedObject *entity = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:self.managedObjectContext];
[self.managedObjectContext save:error];
return entity;
}
#end
A few remarks:
Naming:
being a category is always preferable to add a custom prefix, not to clash with Apple's API. I used my_ here, but you can add whatever you like.
starting the method name with new should be reserved to method returning a non autoreleased object. Since entity is autoreleased, ARC will add an extra retain before returning the object (balanced later by a release) (reference). While this is not an issue, it breaks the standard naming conventions and should be avoided.
Error handling
you should either yield the error or handle it properly. I chose to pass the error back to the caller in my implementation

Storing Related Entities in Core Data

I new to core data, it would great if someone can give feedback/hint me with given requirement.
Entity A
{
property 1;
property 2;
property 3;
}
Entity B
{
property 1;
property 2;
property 3;
}
Entity C
{
EntityB ObjB; // pointing to Entity B
EntityA ObjA; // pointing to Entity A
property 1;
property 2;
property 3;
}
Entity D
{
EntityB ObjB; // pointing to Entity B
EntityA ObjA; // pointing to Entity A
property x;
property y;
property z;
}
Newly Inserted objected needs to be store into Entity C,Entity D with few more properties.
for isolation of insertion to main context below mentioned approach is used
NSEntityDescription *entity = [NSEntityDescription entityForName:#"EntityB" inManagedObjectContext:self.managedObjectContext];
objB = [[EntityB alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
As I have store objB & objA into EntityC, EntityD below method will be use.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"EntityC" inManagedObjectContext:self.managedObjectContext];
objC = [[EntityC alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
objC.objB = objC;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"EntityD" inManagedObjectContext:self.managedObjectContext];
objD = [[EntityD alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
objD.objB = objB;
then finally I will save all objects into main context.
[self.managedObjectContext insertObject:objB];
[self.managedObjectContext insertObject:objC];
[self.managedObjectContext insertObject:objD];
[self.managedObjectContext save:&error];
Since I am not have much experience on core data I would like to know they way I am storing information is good or there is another approach to store data in mentioned scenarios.
Thanks for your valuable time.
This:
objC.objB = objC;
does not make sense and is probably a typo and you meant
objC.objB = objB;
This:
objD.objB = objB;
is correct and establishes the relationship between objD and objB.
These calls:
[self.managedObjectContext insertObject:objB];
[self.managedObjectContext insertObject:objC];
[self.managedObjectContext insertObject:objD];
are not needed, because initWithEntity:insertIntoManagedObjectContext: already
inserts the objects into the context.
Note that you should also define inverse relationships from EntityA and EntityB
to EntityC and EntityD, I cannot see from your question if you already did that.

Resources