CoreData relationship save issue - ios

i have two tables in core data, say table TA and table TB, TA has one-to-many relationships with TB.
There is one record A1 in TA, and i'd like to have several records(B1,B2,..) in TB mapping to A1 in the for loop.
In first iteration, i query db and get A1, invoke addTBObject:B1 and didn't save the context.
In next iteration, query again to get A1, but it return nil. (So strange...)
If i save the context in the first interation, then it's ok to get A1 again in the next iteration. But i don't think this is a better practice, which would result many save actions in one for loop.
Can anybody help on this? Appreciate on that!
code extract:
NSString* objBId = #"xxx";
for (int i=0; i< [dataArray count]; i++) {
ObjA obj = [dataArray objectAtIndex:i];
NSManagedObject* moObjA = [self getManagedObjAById:obj.objId inContext:context];
if(moObjA)
{
NSManagedObject* moObjB = [self getManagedObjBById:objBId inContext:context];
if (moObjB != nil)
{
[moObjB addAObject:moObjA];
[self saveDB:context]; //if don't save here, moObjB will be nil in the next iteration...
}
}
}

Seems found the clue to this problem.
In the implementation of getManagedObjBById, the NSFetchRequest is initiated each time, if there is no commit in each iteration, the fetch request will return nil in next iteration.
After I changed the code to use [NSFetchRequest fetchRequestWithEntityName:#"TabelB"], it's all set! The for loop can always get the managed object B in each iteration without save any more.
Looking further, does this mean the NSFetchRequest for certain entity can only be initialized once before commit (one transaction)??
origin code:
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"xxx" inManagedObjectContext:context]];
Now:
NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:#"xxx"];

Related

Managed object data one-to-many write data

I have a data model in my small application with two small tables: Test and subTest, where Test can have many subTests.
Into Test table we can add as many as we want items and for each item in Test table there is a bunch of items in subTest table.
I can easily request a data from the subTest table associated with Test1, Test2 etc by using CoreData predicates and then using valueForKeyPath. However, I have the problem with saving data into the subTest table. I cannot managed, how to write data in a way, that later on I can fetch this data by passing the test name (e.g. Test1) and then using valueForKeyPath.
Here is an example to clarify what I did mean.
In table Test is a Test1 associated with the the bunch of subtests(st1, st2). Now the subtest st3 must be added to the subTest table. Later we want to get bunch of subtests associated with Test1:
Test test = call here the method which returns NSManagedObject;
NSMutableSet setOfSubTests = [test valueForKeyPath testTosubtests.toTest]; /* Returns st1 st2 and st3 */
Is it only about to write data to the subTest table using old school indexes? (Test1 has index 1 and in subTest table all subtests associated with it have Test1 index)? Is the right thing to do with a CoreData in iOS? Can I apply the same principles as with common SQL DB?
All those things(CoreData and NSManagedObject) are quite new to me. And I want to understand them better.
You need to stop thinking about core data as database tables. Core data was designed to manage collections of related objects. It's underlying implementation may be an SQL database, or a flat binary file, or some custom store implementation. Do not think of it as a database with tables and such.
Now, as for your example...
I have a data model in my small application with two small tables:
Test and subTest, where Test can have many subTests.
You would have a Test entity, and a Subtest entity. The Test entity would have a to-many relationship to Subtest, since one test can "hold" many Subtest entities.
The Subtest entity would have a to-one relationship to Test since a Subtest can only belong to one Test.
I understand things better in code, so the model may look like this when described in code.
- (NSManagedObjectModel*)modelForTestsAndSubtests {
NSEntityDescription *testEntity = [[NSEntityDescription alloc] init];
testEntity.name = #"Test";
NSAttributeDescription *testName = [[NSAttributeDescription alloc] init];
testName.name = #"name";
testName.attributeType = NSStringAttributeType;
NSEntityDescription *subtestEntity = [[NSEntityDescription alloc] init];
subtestEntity.name = #"Subtest";
NSAttributeDescription *subtestName = [[NSAttributeDescription alloc] init];
subtestName.name = #"name";
subtestName.attributeType = NSStringAttributeType;
// A Test can have many Subtest objects in its relationship
NSRelationshipDescription *testToSubtests = [[NSRelationshipDescription alloc] init];
testToSubtests.optional = YES;
testToSubtests.name = #"subtests";
testToSubtests.destinationEntity = subtestEntity;
testToSubtests.deleteRule = NSCascadeDeleteRule;
testToSubtests.minCount = testToSubtests.maxCount = 0;
testToSubtests.ordered = NO;
// A Subtest can (and must) reference exactly one Test
NSRelationshipDescription *subtestToTest = [[NSRelationshipDescription alloc] init];
subtestToTest.optional = NO;
subtestToTest.name = #"test";
subtestToTest.destinationEntity = testEntity;
subtestToTest.inverseRelationship = testToSubtests;
subtestToTest.deleteRule = NSNullifyDeleteRule;
subtestToTest.minCount = subtestToTest.maxCount = 1;
testToSubtests.inverseRelationship = subtestToTest;
testEntity.properties = #[testName, testToSubtests];
subtestEntity.properties = #[subtestName, subtestToTest];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init];
model.entities = #[testEntity, subtestEntity];
return model;
}
However, I have the problem with saving data into the subTest table.
Again, don't think of it as saving data into a table. Think of it as "I'm going to add subtest "st1" to "Test1"
You would do that like this...
NSManagedObject *subtest = [NSEntityDescription
insertNewObjectForEntityForName:#"Subtest"
inManagedObjectContext:test.managedObjectContext];
[subtest setValue:test forKey:#"test"];
Note that when you assign the to-one relationship from the Subtest entity to the Test entity, core data will automatically setup the inverse relationship, so you don't have to add the subtest to the to-many relationship in the Test entity.
Now, let's say you insert 10 subtests (this is just for testing)...
for (int i = 0; i < 10; ++i) {
NSManagedObject *subtest = [NSEntityDescription
insertNewObjectForEntityForName:#"Subtest"
inManagedObjectContext:moc];
[subtest setValue:[NSString stringWithFormat:#"st%02d", i] forKey:#"name"];
[subtest setValue:test forKey:#"test"];
}
[moc save:&error]; // Handle failure and error appropriately...
I cannot managed, how to write data in a way, that later on I can
fetch this data by passing the test name (e.g. Test1) and then using
valueForKeyPath.
So, if you want to get the test with name "Test1" you could write something like this...
- (NSManagedObject*)existingTestWithName:(NSString*)name
inMOC:(NSManagedObjectContext*)moc
error:(NSError**)error {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Test"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"name = %#", name];
fetchRequest.fetchLimit = 1;
NSManagedObject *result = nil;
NSArray *fetched = [moc executeFetchRequest:fetchRequest error:error];
if (fetched) {
if (error) *error = nil;
result = [fetched firstObject];
}
return result;
}
Then, you could grab 'Test1' like so...
NSManagedObject *test = [self existingTestWithName:#"Test1" inMOC:moc error:&error];
if (test) {
// Do something with the Test entity that has name "Test1"
}
And then, once you have the test object, you can get access to all the Subtest objects for this test instance via its "subtests" relationship.
NSSet *subtests = [test valueForKey:#"subtests"];
Thus, you can find a specific subtest by simply searching the collection...
- (NSManagedObject*)findExistingSubtestWithName:(NSString*)name
forTest:(NSManagedObject*)test {
// This is "simple" but could yield less than optimal performance
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name = %#", name];
NSSet *subtests = [test valueForKey:#"subtests"];
return [[subtests filteredSetUsingPredicate:predicate] anyObject];
}
However, this causes all subtests to be loaded into memory to perform an iterative search. Tis is fine if the objects are relatively small, and there are a small number of them.
Or, you can actually perform a fetch...
- (NSManagedObject*)fetchExistingSubtestWithName:(NSString*)name
forTest:(NSManagedObject*)test
error:(NSError**)error {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Subtest"];
fetchRequest.predicate = [NSPredicate
predicateWithFormat:#"test = %# AND name = %#", test, name];
fetchRequest.fetchLimit = 1;
NSManagedObject *result = nil;
NSArray *fetched = [test.managedObjectContext executeFetchRequest:fetchRequest
error:error];
if (fetched) {
if (error) *error = nil;
result = [fetched firstObject];
}
return result;
}
Since you seem to be familiar with SQL, here is the SQL that core data generates for the fetch above.
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZTEST
FROM ZSUBTEST t0 JOIN ZTEST t1 ON t0.ZTEST = t1.Z_PK
WHERE ( t0.ZNAME = ? AND t1.ZNAME = ?) LIMIT 1
However, that is just to show you how the fetch happens. You should still think of everything as interconnected objects, and don't worry about the implementation of the store being SQL until you need to be concerned about performance issues.
Note
You can generate subclass code in Xcode and third party tools like mogenerator that can greatly enhance your experience using managed objects. However, it's still good to know how it all works.

Adding values to a particular index in core data

// deleted all datas for entity before adding
for (int i=0; i<[tempArray count]; i+=3)
{
userData=(UserData *)[NSEntityDescription insertNewObjectForEntityForName:#"UserData" inManagedObjectContext:managedObjectContext];
[userData setUserIcon:tempArray[i]];
[userData setUserID:tempArray[i+1]];
[userData setUserName:tempArray[i+2]];
NSLog(#"loop values%#",[userData userName]);
}
The logged values give names in correct order. But the values stored in core data is not in proper order. E.g. value inside loop shows (apple,ball,cat,dog) but core data stored value shows (apple,cat,ball,dog). I checked values in core data by :
NSFetchRequest *fetchRequest1 = [NSFetchRequest fetchRequestWithEntityName:#"UserData"];
fetchRequest1.resultType = NSDictionaryResultType;
[fetchRequest1 setPropertiesToFetch:[NSArray arrayWithObjects:#"userName", nil]];
fetchRequest1.returnsDistinctResults = YES;
NSArray *dictionaries1 = [self.managedObjectContext executeFetchRequest:fetchRequest1 error:nil];
NSLog (#"names after: %#",dictionaries1);
Any idea why order is mismatching ???
Also alternatively, can I add the values into core data for a particular index so that I can order the table myself?
Or can i use sorting, but i should sort the entire row as the attribute in a row are dependent to each other.
Let me know if you need more information.
I agree with previous answer - your design based on insert order has no practical sense because such order can not be guaranteed and hard to maintain.
The best practice is to apply sort predicates.
For instance you may go to this SO question

iOS CoreData parsing from CSV cannot save error 1570 nil object

I'm quite new to objective c and iOS, would really appreciate some help here.
I have been banging my head against the wall trying to get this working, but keep getting the error:
Failed to save to data store: The operation couldn’t be completed. (Cocoa error 1570.)
What I am doing is importing a CSV file.
From what I can tell it is partially working. CSV is correctly being parsed, I'm getting all the correct values, and I have debugged through and from what I can tell the KA objects are all getting the right values.
But there seems to be something wrong with how I am doing the relationships - the 1570 issue is coming from a nil newTrainingDay's required date field (which seems like it is filled from when I've debugged). I have a feeling something else is wrong but I am not sure what..
Can anyone tell me what I am doing wrong here?
I think the problem stems from creating and inserting new objects into the database, before then searching to see if there is an existing object that meets your needs. Take this code, for example:
// look for existing day
KATrainingDay *newTrainingDay = [NSEntityDescription insertNewObjectForEntityForName:#"KATrainingDay" inManagedObjectContext:self.managedObjectContext];
// Fetching
fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"KATrainingDay"];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"date == %#", date]];
// Execute Fetch Request
fetchError = nil;
NSArray *trainingDays = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
// if you found it
if (!fetchError) {
if ([trainingDays count]) {
for (KATrainingDay *day in trainingDays) {
newTrainingDay = day;
}
} else {
// set name and date for training day
newTrainingDay.date = date;
newTrainingDay.name = trainingDayNameString;
[newTrainingDay addRoutinesObject:newTrainingRoutine];
[newTrainingRoutine addTrainingDaysObject:newTrainingDay];
}
You start by creating and inserting a KATrainingDay into your context, and then do a fetch to look for a KATrainingDay with the right date. If you find it, you assign your pointer (newTrainingDay) to the object you just fetched. But the object you just inserted is still in the context (though you no longer have a pointer to it) and will have nil values for all its attributes and relationships. Hence when you save, that object fails the required day constraint.
To fix this, insert new objects only if you don't find a match with your fetch:
// look for existing day
KATrainingDay *newTrainingDay;
// Fetching
fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"KATrainingDay"];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"date == %#", date]];
// Execute Fetch Request
fetchError = nil;
NSArray *trainingDays = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
// if you found it
if (!fetchError) {
if ([trainingDays count]) {
for (KATrainingDay *day in trainingDays) {
newTrainingDay = day;
}
} else {
// set name and date for training day
newTrainingDay = [NSEntityDescription insertNewObjectForEntityForName:#"KATrainingDay" inManagedObjectContext:self.managedObjectContext]
newTrainingDay.date = date;
newTrainingDay.name = trainingDayNameString;
}
[newTrainingDay addRoutinesObject:newTrainingRoutine];
(and likewise for the other entities). In the above I have also moved the addRoutinesObject: call outside that else block, so it will execute whether you find existing or create new (which I assume you want). And note you don't need to set both sides of the relationship - if you set one side, CoreData does the other for you automatically (assuming they are defined as inverses). One other tip, you don't need your for loop to get the last item in an array, you can just use:
newTrainingDay = [trainingDays lastObject];

(Core Data) How to make a random sorting for NSFetchedResultsController?

I need to get objects from database in random order.
First, I generate random "id"s into NSArray and set them as predicate:
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"(self.id IN %#)", randomIds];
// The example of randomIds contents (always different):
// <__NSArrayM 0xd1cc4e0> (185,51,69,25,33,135,136,97,157,112,145,132,56,15,159,70,88,6,72,82)
But to this time my results are still sorted because NSFetchedResultsController insists on setting the "sortDescriptors" for its FetchRequest:
NSSortDescriptor *sortingDescriptor = [[NSSortDescriptor alloc] initWithKey:#"id" ascending:YES];
fetchRequest.sortDescriptors = #[sortingDescriptor];
fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
But I need the fetch results in random order: the same as in "randomIds" array.
I haven't found any reasonable solution for this problem yet. How would you solve it? Is there a chance someone already has come to any solution?
I want to suggest creating an NSSortDescriptor that uses a comparator block, and have that block use a random number generator to decide which item goes first for each comparison.
However, I have a vague memory that you can't use comparator-based sort descriptors in fetch requests to Core Data when the internal database is SQLite.
Why don't you simply convert your fetched results array into a mutable array and then scramble them:
- (NSMutableArray *) scrambledArray: (NSArray *) theArray
{
NSMutableArray *result = [theArray mutableCopy];
NSUInteger count = [result count];
for (NSUInteger index = 0; index < count; index++)
{
NSUInteger random = arc4random_uniform(count);
id itemAtIndex = result[index];
result[index] = result[random];
result[random] = result;
}
return result;
}
The code above is very fast, taking O(n) time to complete.
If you never need the sort descriptor, just delete it from the fetched results controller.
If you need it only sometimes with the same fetched results controller, you have to reinitialize it. Just keep a flag around and reload the fetched results controller.
if (!useRandom) {
fetchRequest.sortDescriptor = ...;
}
else {
// add the predicate
}
To reload without this sort descriptor, just do this:
useRandom = YES;
self.fetchedResultsController = nil;
[self.tableView reload Data];

Fecth request causes the foreign key of parent manage object to be null

I have two entities with relationship. One parent and child(to many). The first time I fetch it is ok. The second time a fetch request is called the reference key to the parent gets removed/null which orphans the child record.
- (void)prepareGallery
{
self.events = [[NSMutableArray alloc] init];
self.photos = [[NSMutableArray alloc] init];
NSArray *tempArr = [self fetchEntity:#"PEvent" predicate:nil];
for (Event *pEvent in tempArr) {
NSSet *photoSet = [pEvent photos];
NSArray *photosArray = [photoSet allObjects];
if ([photosArray count] > 0) {
//only add events with photos
[self.events addObject:pEvent];
[self.photos addObject:photosArray];
}
}
if ([self.events count] > 0) {
[collectionView reloadData];
}else{
NSLog(#"Events empty");
}
}
-(NSArray*)fetchEntity:(NSString*) entityName predicate:(NSPredicate*) predicate
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
request.resultType = NSManagedObjectResultType;
if (predicate != nil) {
request.predicate = predicate;
}
NSError *error;
NSArray *result = [_managedObjectContext executeFetchRequest:request error:&error];
return result;
}
Model Relationship
Entity: Photo
Destination: PEvent
Inverse: photos
Delete Rule: no action
Type: To one
Entity: PEvent
Destination: Photo
Inverse: pEvent
Delete Rule: cascade
Type: To many
Several issues. Your setup with the arrays is not optimal. By creating two separate arrays with events and photos you are breaking the relationship.
The key here is to completely do away with these artefacts. You can fetch all events and keep them in an array, if you want, but of course it would be much more robust to use a NSFetchedResultsController to give you the objects.
If you use an array, all you need is the event array. If for your collection view datasource you need the number of photos for an event, it is as easy as event.photos.count.
You are doing a to of completely unnecessary operations in your setup routine:
E.g. you fetch all events, but then filter them in a loop into a new array. You could just use a predicate in your fetch request.
Also, you transform your photos NSSet into an array (which will have random order), which is also unnecessary. If you need you photos in order, something like a timestamp seems most appropriate.

Resources