Managed object data one-to-many write data - ios

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.

Related

iOS / Core Data Entity Relationships with Pre-Defined Data

I have two Objective-C Core Data entities - say Person and Nationality. Person would have a To-One relationship with Nationality, whilst Nationality would have a To-Many relationship with Person. Also, Person class can have any number of objects / rows, whereas Nationality would have a pre-defined list of 200 odd instances. So Person should not be able to assign a nationality to itself other than those 200 objects.
Can someone advise please how would we code this out or in case there is a sample code available? Afraid I dont seem to be able to get a start on how to leverage setValue: forKey: here...
Much appreciated!
Let's assume that your Nationality entity has a "name" attribute that uniquely identifies that nationality. You can provide any manner of UI to get this from the user. It can be typing in a string or fetching all nationalities and putting them in a table or some kind of picker.
If you want all nationalities, that's easy.
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Nationality"];
NSError *error;
NSArray *nationalities = [moc executeFetchRequest:fetchRequest error:&error];
if (nationalities == nil) {
// handle error
} else {
// You now have an array of all Nationality entities that can be used
// in some UI element to allow a specific one to be picked
}
If you want to look it up based on a string for the name...
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Nationality"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"name = %#", nationalityName];
fetchRequest.fetchLimit = 1;
NSArray *nationalities = [moc executeFetchRequest:fetchRequest error:&error];
if (nationalities == nil) {
// handle error
} else {
NSManagedObject *nationality = [nationalities firstObject];
// If non-nil, it will be the nationality object you desire
}
Creating the person and assigning its nationality is also straight forward...
if (nationality) {
NSManagedObject *person = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:moc];
// Set any attributes of the Person entity
[person setValue:#"Fred Flintstone" forKey:#"name"];
// Assign its nationality, and as long as the relationship is setup with
// inverse relationships, the inverse will be automatically assigned
[person setValue:nationality forKey:#"nationality"];
}

Core-data many-to-many relationship

I have an Android app, and now I'm making an iOS version, but I'm having some problem with the joins in CoreData.
I have the following tables:
Cidade
-cid_codigo integer primary key
-cid_nome text
-cid_nome_normalizado text
Anunciante
-anu_codigo integer primary key
-anu_nome text
-some other values
AnuncianteCidade
-cid_codigo integer
-anu_codigo integer
To get the all data from table Cidade I use the following method:
+(NSMutableArray *)getAllCidades{
NSMutableArray *retorno = [[NSMutableArray alloc] init];
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSEntityDescription *entityDesc = [NSEntityDescription entityForName:#"Cidade" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDesc];
//WHERE CLAUSE
NSPredicate *pred = [NSPredicate predicateWithFormat:#"1 = 1"];
[request setPredicate:pred];
NSError *error;
NSArray *cidades = [context executeFetchRequest:request error:&error];
if([cidades count] == 0){
NSLog(#"Nenhuma cidade encontrada");
}else{
for(NSManagedObject *cidade in cidades){
Cidade *c = [[Cidade alloc] init];
[c initWithCodigo:[[cidade valueForKey:#"cid_codigo"] integerValue] nome:[cidade valueForKey:#"cid_nome"] nomeNormalizado:[cidade valueForKey:#"cid_nome_normalizado"]];
[retorno addObject:c];
}
}
return retorno;
}
But now, given a name from Cidade, I want to get all the data from Anunciante associated with this Cidade.
How can I do that?
Core Data is not a database. Core Data is an object graph that happens to persist to disk and one of the formats that Core Data can persist to is a database structure. This is an important distinction that will help you to work with it moving forward.
First, you cannot call just -init on a NSManagedObject. That will not work as it is not the designated initializer of NSManagedObject. I would suggest you read up on Core Data and learn how to stand up the Core Data stack.
Having said that, your Cidade objects should have a reference to Anunciante. The join table is internal to Core Data and you don't have access to it nor should you. To get all of the Anunciante objects for a Cidade object is to simply request the objects:
Given an NSArray of Cidade objects:
NSArray *objects = ...;
for (NSManagedObject *object in objects) {
NSSet *anunciantes = [object valueForKey:#"anunciante"];
}
This is assuming you have a many to many relationship defined in the Core Data model editor between the Cidade and the Anunciante entities.
In addition to Marcus' answer, I would add that a predicate "1 = 1" could be simply left out.
To insert a managed object into the context you use a NSEntityDescription class method:
Cidade *cidade = [NSEntityDescription insertNewObjectForEntityForName:#"Cidade"
inManagedObjectContext:context];
All "anunciantes" of a cidade will be conveniently available to you as a NSSet:
cidade.anunciantes
is all you need.

Core Data - Select distinct

I know there have been several discussions about this but none of them resolved my simple problem.
I have an Entity called Character and inside there are 4 columns:
character_id, episode_id, title, desc
there can be several same character_ids values but with different episode_id.
When I perform fetch\select I do it for whole table and wishes to get it distinctly by character_id. so this is what I do:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:moc];
[fetchRequest setEntity:entity];
// Add a sort descriptor. Mandatory.
if(sortDescriptors != nil) {
[fetchRequest setSortDescriptors:sortDescriptors];
}
fetchRequest.predicate = predicate;
// Required! Unless you set the resultType to NSDictionaryResultType, distinct can't work.
// All objects in the backing store are implicitly distinct, but two dictionaries can be duplicates.
// Since you only want distinct names, only ask for the 'name' property.
fetchRequest.resultType = NSDictionaryResultType;
fetchRequest.propertiesToFetch = [NSArray arrayWithObject:[[entity propertiesByName] objectForKey:#"title"]];
fetchRequest.returnsDistinctResults = YES;
NSArray *fetchResults = [moc executeFetchRequest:fetchRequest error:&error];
The 'fetchResults' array contains 3 out of 10 rows which is the right result!
The problem: None of the object within the array is accessible.
If I try the following:
NSDictionary item1 = [fetchResults objectAtIndex:0];
NSString *title = [item1 objectForKey:#title"];
I get an exception!
What am I doing wrong?? how can I translate back the dictionary into NSManagedObjects??
Thank you!
First, when using Core Data you should not use foreign keys. Rather, it is preferable to use Core Data's relationships and let the framework deal with the ids in an opaque manner. Maybe you are synching with a web service and need to keep track of the ids but your program logic actually should not depend on it.
Second, if you need an object, it is really better to use the NSManagedObjectResultType rather than the NSDictionaryResultType. You can still obtain distinct results. If you are not experiencing performance issues, this is the preferred pattern. The code is also much more readable.
Your data structure would be this, with a many-to-many relationship:
Character <<--->> Episode
All characters of an episode or all episodes with a certain character is simple. These will be "distinct" results dictated by the logic of the data model:
NSArray *allCharactersInEpisode = episode.characters;
NSArray *allEpisodesWithCharacter = character.episodes;
To select all characters of all episodes you just select all characters. Much simpler than a "distinct" query.

CoreData relationship save issue

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"];

Updating a particular attribute of core data

I am developing an application where i used core data framework for the purpose of maintaining a database. My entity contains three attributes called: name, start time and end time of a list of applications. I am getting the correct values for name and start time attribute.
Now my problem is my end time attribute should contain the value of the next entries start time value. If anybody having any idea about this please let me know.
Thanks
You can leave the endTime attribute blank until you create the next entity. In the +Create category on the entity, get the last/first object (assuming you are using ordered entities) and update the endTime with the same value used for the new startTime.
If your objects are not ordered it could be a bit tricky since all the entities are in a set. But if ordered, you are good since NSOrderedSet responds to lastObject (and firstObject).
Enjoy,
Damien
EDIT: Here is an example factory method that either 1) returns the existing stock entity for a stock symbol or 2) creates a new entity for that symbol. Pretty easily modified to get entities and select the first/last depending on your sort order. Again see the Core Data classes from Prof. Hegarty.
+ (Stock *)stockForSymbol:(NSString *)symbol inManagedObjectContext:(NSManagedObjectContext *)context {
Stock *stock = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Stock"];
request.predicate = [NSPredicate predicateWithFormat:#"symbol = %#",symbol];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"symbol" ascending:YES];
request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSError *error = nil;
NSArray *matches = [context executeFetchRequest:request error:&error];
if (!matches || [matches count] > 1) {
// handle error
} else if ([matches count] == 0) {
stock = [NSEntityDescription insertNewObjectForEntityForName:#"Stock" inManagedObjectContext:context];
stock.symbol = symbol;
stock.strategyPosition = [NSNumber numberWithInt:StrategyPositionFlat];
stock.userPosition = stock.strategyPosition;
stock.userOwns = [NSNumber numberWithBool:NO];
} else {
stock = [matches lastObject];
}
return stock;
}

Resources