Inside my user object I have the following code to generate a new 'session' or continue the existing session if one exists.
Strangely it will keep other properties but just loses the 'user' property... user is in a one to many relationship with session, 1 user can have many sessions. (or will do, for the following test I am simply checking for any previous session and using it if it exists)
-(void)setupSessionStuff
{
// Create new Core Data request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Session" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
// Create Sort Descriptors for request
NSSortDescriptor *startTimeSort = [[NSSortDescriptor alloc] initWithKey:#"startTime" ascending:NO selector:nil];
[request setSortDescriptors:[NSArray arrayWithObjects:startTimeSort, nil]];
[startTimeSort release];
[request setFetchLimit:1]; // Only get the most recent session
// Execute request
NSError *error = nil;
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
if (results == nil) {
// Something went horribly wrong...
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1);
}
[request release];
Session *theSession = nil;
if ([results count] == 1) {
NSLog(#"existing session");
// Use existing Session
theSession = [results objectAtIndex:0];
NSLog(#"session.user: %#", [theSession valueForKey:#"user"]); // this is always null!
} else {
NSLog(#"new session");
// Create new Sesson
theSession = (Session *)[NSEntityDescription insertNewObjectForEntityForName:#"Session" inManagedObjectContext:[self managedObjectContext]];
// Add the Session to the User
NSLog(#"before: session.user: %#", theSession.user); // null
theSession.user = self;
NSLog(#"after: session.user: %#", theSession.user); // looks good
}
...
NSLog(#"before.save: session.user: %#", theSession.user); // good
// Save everything
error = nil;
if (![[self managedObjectContext] save:&error]) {
// Something went horribly wrong...
NSLog(#"Unresolved error: %#, %#, %#", error, [error userInfo],[error localizedDescription]);
exit(-1);
}
NSLog(#"after.save: session.user: %#", theSession.user); // still there..
}
Additionally I have opened up the Core Data sqlite file and examined with SQLite Manager. It looks like the relationship has been correctly saved, as I can see the userID stored in the session table.
-
Just added this at the start of my method as another test.
NSSet *set = self.session;
for(Session *sess in set) {
NSLog(#"startTime %#", sess.startTime);
NSLog(#"user %#", sess.user);
}
Strangely enough the user is set in this case!? So set here then not set a few lines later when I do the fetch request... ?
-
In response to feedback below
Have added this code after assigning session.user = self and both return the expected output. So it does look like the problem is with the subsequent fetch.
NSLog(#"self.session: %#", self.session);
NSLog(#"self.session: %#", [self valueForKey:#"session"]);
Also I agree that accessing my session's through self.session will let me work around my issue, but it doesn't solve what is going on here.
In other places I surely won't be able to walk from one entity to the other so need to confidence the fetch is going to pull everything in correctly.
Firstly, I would check that the relationship is set from the other side by logging self.session. If that shows as null then you have no reciprocal relationship set in the model.
I think that your fetch is poorly constructed. You are relying on the sort descriptor to provide the last used session but you only have a fetch limit of 1. You seem to think that the fetch will find all existing Session objects, sort them and then return the first one. However, sorts execute last so the fetch will find a single session object (because of the fetch limit) and will then apply the sort to that single object. This makes it likely that you will be dealing with a more or less random Session object.
You probably don't need a fetch at all because you are only interested in the Session objects in a relationship with the User object which is self. If you already have the object in one side of a relationship, you don't need to fetch, you just need to walk the relationship. All you really need to do is:
if ([self.session count]==0){
//...create new session and set relationship
}else{
//... find latest session
}
I think your problem with this particular chunk of code is in the reporting. When you get a return value, you use the self.user notation but where you get a null return you use valueForKey:.
While in theory both return the same value, they don't have to because they don't use the same mechanism to return the value. The self-dot notation calls an accessor method while valueForKey: may or may not depending on the specifics of a class' implementation. I would test if the self.session returns a value where valueForKey: does not.
Well I found the problem and solved my issue...
After examining the memory address of my session entity I noticing that it was changing between runs. Investigating further I discovered that where I had been testing some code earlier in another class, creating a new session entity, but not saving it, well, it was being saved after all - when I issued the save in my code above!
Related
So I am building in a hide function into my application. In my settings menu I have a UISwitch that should allow the user to hide themselves. I have created the UISwitch's IBAction like so:
-(IBAction)hideUserToggle:(id)sender {
AppDelegate *newAppDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [newAppDelegate managedObjectContext];
NSManagedObject *newOwner;
NSEntityDescription *entityDesc = [NSEntityDescription entityForName:#"LoggedInUser" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDesc];
NSManagedObject *matches = nil;
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
newOwner = [NSEntityDescription insertNewObjectForEntityForName:#"LoggedInUser" inManagedObjectContext:context];
if (_hideUser.on) {
if ([objects count] == 0) {
NSLog(#"%#",[error localizedDescription]);
} else {
matches = objects[0];
[newOwner setValue:#"userHidden" forKeyPath:#"isHidden"];
NSLog(#"%#",[matches valueForKeyPath:#"isHidden"]);
}
} else {
if([objects count] == 0) {
NSLog(#"%#",[error localizedDescription]);
} else {
matches = objects[0];
[newOwner setValue:#"userNotHidden" forKeyPath:#"isHidden"];
NSLog(#"%#",[matches valueForKeyPath:#"isHidden"]);
}
}
}
This should set the value of the Core Data String that I use to determine whether a person is hidden or not, which I use later in my code as a conditional for loading data. However when I test this feature it doesn't seem to update the persistent data store (Core Data) when the user has flipped the switch. I have looked around everywhere and I found a reference to there being a delay in updating Core Data here -> Why does IOS delay when saving core data via a UIManagedDocument, however it doesn't seem to provide the answer to my problem.
I want to be able flip the switch and save that value so that when the user swipes over to another view controller it is immediately aware that the user has gone into "hiding" or offline so it does not show certain information.
A NSManagedObjectContext is a scratchpad. Changes you make within the context exist only within the context unless or until you save them to the context's parent (either the persistent store itself or another context).
You're not saving them. I'd assume you're therefore not seeing the change elsewhere because you're using different contexts. Meanwhile the change eventually migrates because somebody else happens to save.
See -save: for details on saving.
(aside: the key-value coding [newOwner setValue:#"userHidden" forKeyPath:#"isHidden"]-style mechanism is both uglier and less efficient than using an editor-generated managed object subclass; hopefully it's just there while you're debugging?)
I am using Core Data to store some information for my app.
I have a .xcdatamodeld file containing 8 entities, and I extract them on different views.
In one of the viewControllers, I call three of them. Like this:
AppDelegate *appDelegate = (AppDelegate *) [[UIApplication sharedApplication]delegate];
managedObjectContext = appDelegate.managedObjectContext;
NSManagedObjectContext *moc = [self managedObjectContext];
NSEntityDescription *entiAll = [NSEntityDescription entityForName:#"AllWeapons" inManagedObjectContext:moc];
NSFetchRequest *frAll = [[NSFetchRequest alloc] init];
[frAll setEntity:entiAll];
NSError *error = nil;
arrAll = [moc executeFetchRequest:frAll error:&error];
displayArray = [[NSMutableArray alloc]initWithArray:arrAll];
NSEntityDescription *entiRange = [NSEntityDescription entityForName:#"WeaponsRanged" inManagedObjectContext:moc];
NSFetchRequest *frRanged = [[NSFetchRequest alloc] init];
[frRanged setEntity:entiRange];
NSError *errorRanged = nil;
arrRange = [moc executeFetchRequest:frRanged error:&errorRanged];
NSLog(#"%i, %i", [arrRange count], [[moc executeFetchRequest:frRanged error:&errorRanged] count]);
NSEntityDescription *entiMelee = [NSEntityDescription entityForName:#"WeaponsMelee" inManagedObjectContext:moc];
NSFetchRequest *frMelee = [[NSFetchRequest alloc] init];
[frMelee setEntity:entiMelee];
NSError *errorMelee = nil;
arrMelee = [moc executeFetchRequest:frMelee error:&errorMelee];
NSLog(#"%i, %i", [arrMelee count], [[moc executeFetchRequest:frMelee error:&errorMelee] count]);
The problem is that the middle one (the one filling the arrRange-array) doesn't work..
arrAll logs out with all correct data, arrMelee logs out with all the correct data (x4 for some reason, don't know if this is related :S), but arrRange logs out as an empty array.
[arrRange count]; gives me 0, even though I know there is lots of data there.
I ran this code on the simulator, and found the .sqlite file, opened it in Firefox's SQLite Manager, and saw the correct data, 40 rows.
I went into the appDelegate, where I fill the CoreData when necessary, and saw that the method which downloads the data in JSON-format successfully sends it to the sqlite aswell.
Here I fill the CoreData with data from the json:
[self deleteAllObjects:#"WeaponsRanged"];
NSManagedObjectContext *context = [self managedObjectContext];
for(NSDictionary *item in jsonWeaponRanged)
{
WeaponsRanged *wr = [NSEntityDescription insertNewObjectForEntityForName:#"WeaponsRanged"
inManagedObjectContext:context];
///***///
wr.recoil = [item objectForKey:#"Recoil"];
///***///
NSError *error;
if(![context save:&error])
NSLog(#"%#", [error localizedDescription]);
}
And if I here do NSLog(#"%# - %#", wr.recoil, [item objectForKey:#"Recoil"]); I get the correct data. (Same data on both)
So. The correct data is obviously in the core. But my NSFetchRequest or something is failing. I am pretty noob at Objective-C, so it might be my bad code-grammar striking again. I realize I should use things again etc, not creating new objects all the time.. But cmon, this is my first app.. And if that is actually the problem, I might learn. But I'm stuck.
SOMETIMES I get data, sometimes I don't. It's weird. I re-launched the app, and got data from it, and now I don't.. I haven't found a pattern yet..
Anyone?
Or is there another way to request data from the entity?
I have some suggestions, too big for a comment.
1) after you create the WeaponsRanged, try reading them back:
for(NSDictionary *item in jsonWeaponRanged)
{
WeaponsRanged *wr = [NSEntityDescription insertNewObjectForEntityForName:#"WeaponsRanged"
inManagedObjectContext:context];
NSLog(#"IS WR Realized? %#", wr ? #"YES" : #"NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO WR");
///***///
wr.recoil = [item objectForKey:#"Recoil"];
///***///
NSError *error;
if(![context save:&error])
NSLog(#"%#", [error localizedDescription]);
}
// Now lets see if we can retrieve them:
{
NSEntityDescription *entiRange = [NSEntityDescription entityForName:#"WeaponsRanged" inManagedObjectContext:context];
NSFetchRequest *frRanged = [[NSFetchRequest alloc] init];
[frRanged setEntity:entiRange];
NSError *errorRanged = nil;
arrRange = [context executeFetchRequest:frRanged error:&errorRanged];
NSLog(#"Wrote %i items, read back %i items", [jsonWeaponRanged count], [arrRange count] );
}
2) In the viewController reading WeaponsRanged, add an assert before the fetch on mod:
NSLog(#"IS moc set? %#", moc ? #"YES" : #"NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO MOC");
EDIT:
3) Spread statements everywhere you access the MOC:
assert([NSThread isMainThread]);
[If you haven't used asserts before google and read up on the topic. These are a powerful tool for developers to find out about potential problems way before they manifest themselves in the gui or elsewhere. They are normally compiled out for release/distribution builds.]
This will force an exception if the thread is not the main thread, and then let you track down the reason by following the stack trace.
Nevermind! It was my own damn fault (yet again..).
The problem occured before the code I presented, and it turns out the data was never in the .sqlite-file when the problem was present.
This is what I had:
I collected data from the internet through json-request. I have told the app to check the "version" of the data through the internet, and if the data is outdated, then re-download it.
First, I download all data, then I add them to their own entity in Core Data. After downloading, I clear the current Core Data entity of the downloaded data. So on the top of each add-method it said i.e [self deleteAllObjectsOfEntity:#"WeaponsRanged"];, My whole problem was that in the addMelee-method, it ALSO said [self deleteAllObjectsOfEntity:#"WeaponsRanged"]; instead of #"WeaponsMelee", thus deleting all ranged weapons, and later adding melee to the melee entity. And that also proves that the other problem I mentioned of arrMelee logging out four times as much data as it should was caused by this.
The reason it sometimes worked was that the downloading is not happening in any ordered mode. So the addRanged was sometimes called before the addMelee. If ranged comes first, it clears the arrRanged, and fills it up with correct data, and THEN melee comes, and clears it out. When melee was called first, it cleared arrRanged and filled additional data to arrMelee, and THEN ranged comes and tries to clear an empty entity, and then fills it up with correct data.
The solution was obviously to change the entity deleted when adding it, as it was the wrong one.
Sorry.... :)
Cheers,
I'm experiencing a problem with core data, I guess I'm just looking in the wrong direction again.
My managedObjectContext will return an empty NSSet if I call registeredObjects on it. If I execute a fetch beforehand however, it will return the same objects that as the fetch did just a moment ago.
There's no multithreading going on.
Here's what I do:
[self setupContext]; // This will set up managedObjectContext, which is a property of this class
// Fetching...
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *foo = [NSEntityDescription entityForName:#"Foo" inManagedObjectContext:managedObjectContext];
[request setEntity:foo];
NSError *fetchError = nil;
NSArray *fetchResults = [managedObjectContext executeFetchRequest:request error:&ftchError];
NSLog(#"Fetch returned %i objects.", [fetchResults count]);
[request release];
// Done fetching...
NSArray *allObjects = [[managedObjectContext registeredObjects] allObjects];
NSLog(#"Context contains %i objects...", [allObjects count]);
The store contains 30 objects. If I run the code above, both NSLogs will report five objects. If I remove the fetch part between the two comments, it will report zero objects for the whole context.
Note that I am at no point commiting or otherwise changing the contexts contents.
Do I need to force the context into refreshing itself first? I've never done this before though and I don't recall registeredObjects failing on me like this on other occasions in the first place.
Any suggestions appreciated!
Toastor
You may be confused about what registeredObjects means. This is the set of objects that are currently in the NSManagedObjectContext. This is not the set of objects in the store, just the ones in the context. If you haven't fetched or otherwise registered the objects in the context, then they won't be in registeredObjects.
Is it possible to make a Core Data attribute unique, i.e. no two MyEntity objects can have the same myAttribute?
I know how to enforce this programatically, but I'm hoping there's a way to do it using the graphical Data Model editor in xcode.
I'm using the iPhone 3.1.2 SDK.
Every time i create on object I perform a class method that makes a new Entity only when another one does not exist.
+ (TZUser *)userWithUniqueUserId:(NSString *)uniqueUserId inManagedObjectContext:(NSManagedObjectContext *)context
{
TZUser *user = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"TZUser" inManagedObjectContext:context];
request.predicate = [NSPredicate predicateWithFormat:#"objectId = %#", uniqueUserId];
NSError *executeFetchError = nil;
user = [[context executeFetchRequest:request error:&executeFetchError] lastObject];
if (executeFetchError) {
NSLog(#"[%#, %#] error looking up user with id: %i with error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [uniqueUserId intValue], [executeFetchError localizedDescription]);
} else if (!user) {
user = [NSEntityDescription insertNewObjectForEntityForName:#"TZUser"
inManagedObjectContext:context];
}
return user;
}
From IOS 9 there is a new way to handle unique constraints.
You define the unique attributes in the data model.
You need to set a managed context merge policy "Merge policy singleton objects that define standard ways to handle conflicts during a save operation" NSErrorMergePolicy is the default,This policy causes a save to fail if there are any merge conflicts.
- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
[_managedObjectContext setMergePolicy:NSOverwriteMergePolicy];
return _managedObjectContext;
}
The various option are discussed at Apple Ducumentation Merge Policy
It is answered nicely here
Zachary Orr's Answer
and he has kindly also created a blogpost and sample code.
Sample Code
Blog Post
The most challenging part is to get the data Model attributes editable.The Secret is to left click and then right click, after you have clicked the + sign to add a constraint.
I've decided to use the validate<key>:error: method to check if there is already a Managed Object with the specific value of <key>. An error is raised if this is the case.
For example:
- (BOOL)validateMyAttribute:(id *)value error:(NSError **)error {
// Return NO if there is already an object with a myAtribute of value
}
Thanks to Martin Cote for his input.
You could override the setMyAttribute method (using categories) and ensure uniqueness right there, although this may be expensive:
- (void)setMyAttribute:(id)value
{
NSArray *objects = [self fetchObjectsWithMyValueEqualTo:value];
if( [objects count] > 0 ) // ... throw some exception
[self setValue:value forKey:#"myAttribute"];
}
If you want to make sure that every MyEntity instance has a distinct myAttribute value, you can use the objectID of the NSManagedObject objects for that matter.
I really liked #DoozMen approach!!
I think it's the easiest way to do what i needed to do.
This is the way i fitted it into my project:
The following code cycles while drawing a quite long tableView, saving to DB an object for each table row, and setting various object attributes for each one, like UISwitch states and other things: if the object for the row with a certain tag is not present inside the DB, it creates it.
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Obiettivo" inManagedObjectContext:self.managedObjectContext];
request.predicate = [NSPredicate predicateWithFormat:#"obiettivoID = %d", obTag];
NSError *executeFetchError = nil;
results = [[self.managedObjectContext executeFetchRequest:request error:&executeFetchError] lastObject];
if (executeFetchError) {
NSLog(#"[%#, %#] error looking up for tag: %i with error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), obTag, [executeFetchError localizedDescription]);
} else if (!results) {
if (obbCD == nil) {
NSEntityDescription *ent = [NSEntityDescription entityForName:#"Obiettivo" inManagedObjectContext:self.managedObjectContext];
obbCD = [[Obiettivo alloc] initWithEntity:ent insertIntoManagedObjectContext:self.managedObjectContext];
}
//set the property that has to be unique..
obbCD.obiettivoID = [NSNumber numberWithUnsignedInt:obTag];
[self.managedObjectContext insertObject:obbCD];
NSError *saveError = nil;
[self.managedObjectContext save:&saveError];
NSLog(#"added with ID: %#", obbCD.obiettivoID);
obbCD = nil;
}
results = nil;
Take a look at the Apple documentation for inter-property validation. It describes how you can validate a particular insert or update operation while being able to consult the entire database.
You just have to check for an existing one :/
I just see nothing that core data really offers that helps with this. The constraints feature, as well as being broken, doesn't really do the job. In all real-world circumstances you simply need to, of course, check if one is there already and if so use that one (say, as the relation field of another item, of course). I just can't see any other approach.
To save anyone typing...
// you've download 100 new guys from the endpoint, and unwrapped the json
for guy in guys {
// guy.id uniquely identifies
let g = guy.id
let r = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Guy")
r.predicate = NSPredicate(format: "id == %d", g)
var found: [CD_Guy] = []
do {
let f = try core.container.viewContext.fetch(r) as! [CD_Guy]
if f.count > 0 { continue } // that's it. it exists already
}
catch {
print("basic db error. example, you had = instead of == in the pred above")
continue
}
CD_Guy.make(from: guy) // just populate the CD_Guy
save here: core.saveContext()
}
or save here: core.saveContext()
core is just your singleton, whatever holding your context and other stuff.
Note that in the example you can saveContext either each time there's a new one added, or, all at once afterwards.
(I find tables/collections draw so fast, in conjunction with CD, it's really irrelevant.)
(Don't forget about .privateQueueConcurrencyType )
Do note that this example DOES NOT show that you, basically, create the entity and write on another context, and you must use .privateQueueConcurrencyType You can't use the same context as your tables/collections .. the .viewContext .
let pmoc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
pmoc.parent = core.container.viewContext
do { try pmoc.save() } catch { fatalError("doh \(error)")}
I've looked through all the class documentation for Core Data and I can't find away to programmatically update values in a core data entity. For example, I have a structure similar to this:
id | title
============
1 | Foo
2 | Bar
3 | FooFoo
Say that I want to update Bar to BarBar, I can't find any way to do this in any of the documentation.
In Core Data, an object is an object is an object - the database isn't a thing you throw commands at.
To update something that is persisted, you recreate it as an object, update it, and save it.
NSError *error = nil;
//This is your NSManagedObject subclass
Books * aBook = nil;
//Set up to get the thing you want to update
NSFetchRequest * request = [[NSFetchRequest alloc] init];
[request setEntity:[NSEntityDescription entityForName:#"MyLibrary" inManagedObjectContext:context]];
[request setPredicate:[NSPredicate predicateWithFormat:#"Title=%#",#"Bar"]];
//Ask for it
aBook = [[context executeFetchRequest:request error:&error] lastObject];
[request release];
if (error) {
//Handle any errors
}
if (!aBook) {
//Nothing there to update
}
//Update the object
aBook.Title = #"BarBar";
//Save it
error = nil;
if (![context save:&error]) {
//Handle any error with the saving of the context
}
The Apple documentation on using managed objects in Core Data likely has your answer. In short, though, you should be able to do something like this:
NSError *saveError;
[bookTwo setTitle:#"BarBar"];
if (![managedObjectContext save:&saveError]) {
NSLog(#"Saving changes to book book two failed: %#", saveError);
} else {
// The changes to bookTwo have been persisted.
}
(Note: bookTwo must be a managed object that is associated with managedObjectContext for this example to work.)
Sounds like you're thinking in terms of an underlying relational database. Core Data's API is built around model objects, not relational databases.
An entity is a Cocoa object—an instance of NSManagedObject or some subclass of that. The entity's attributes are properties of the object. You use key-value coding or, if you implement a subclass, dot syntax or accessor methods to set those properties.
Evan DiBiase's answer shows one correct way to set the property—specifically, an accessor message. Here's dot syntax:
bookTwo.title = #"BarBar";
And KVC (which you can use with plain old NSManagedObject):
[bookTwo setValue:#"BarBar" forKey:#"title"];
If I'm understanding your question correctly, I think that all you need to keep in mind is managed objects are really no different than any other Cocoa class. Attributes have accessors and mutators you can use in code, through key value coding or through bindings, only in this case they're generated by Core Data. The only trick is you need to manually declare the generated accessors in your class file (if you have one) for your entity if you want to avoid having to use setValue:ForKey:. The documentation describes this in more detail, but the short answer is that you can select your attributes in the data model designer, and choose Copy Obj-C 2.0 Method Declarations from the Design menu.
NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSArray *temp = [[NSArray alloc]initWithObjects:#"NewWord", nil];
[object setValue:[temp objectAtIndex:0] forKey:#"title"];
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
I think this piece of code will give you the idea ;)