I have around 10000 objects of entity 'Message'. When I add a new 'Message' i want to first see whether it exists - and if it does just update it's data, but if it doesn't to create it.
Right now the "find-or-create" algorithm works with by saving all of the Message objects 'objectID' in one array and then filtering through them and getting the messages with existingObjectWithID:error:
This works fine but in my case when I fetch an 'Message' using existingObjectWithID: and then try to set and save a property by setting the property of the 'Message' object and calling save: on it's context it doesn't saves it properly. Has anyone come across a problem like this?
Is there a more efficient way to make find-or-create algorithm?
First, Message is a "bad" name for a CoreData entity as apple use it internally and it cause problems later in development.
You can read a little more about it HERE
I've noticed that all suggested solutions here use an array or a fetch request.
You might want to consider a dictionary based solution ...
In a single threaded/context application this is accomplished without too much of a burden by adding to cache (dictionary) the newly inserted objects (of type Message) and pre-populating the cache with existing object ids and keys mapping.
Consider this interface:
#interface UniquenessEnforcer : NSObject
#property (readonly,nonatomic,strong) NSPersistentStoreCoordinator* coordinator;
#property (readonly,nonatomic,strong) NSEntityDescription* entity;
#property (readonly,nonatomic,strong) NSString* keyProperty;
#property (nonatomic,readonly,strong) NSError* error;
- (instancetype) initWithEntity:(NSEntityDescription *)entity
keyProperty:(NSString*)keyProperty
coordinator:(NSPersistentStoreCoordinator*)coordinator;
- (NSArray*) existingObjectIDsForKeys:(NSArray*)keys;
- (void) unregisterKeys:(NSArray*)keys;
- (void) registerObjects:(NSArray*)objects;//objects must have permanent objectIDs
- (NSArray*) findOrCreate:(NSArray*)keys
context:(NSManagedObjectContext*)context
error:(NSError* __autoreleasing*)error;
#end
flow:
1) on application start, allocate a "uniqueness enforcer" and populate your cache:
//private method of uniqueness enforcer
- (void) populateCache
{
NSManagedObjectContext* context = [[NSManagedObjectContext alloc] init];
context.persistentStoreCoordinator = self.coordinator;
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:self.entity.name];
[r setResultType:NSDictionaryResultType];
NSExpressionDescription* objectIdDesc = [NSExpressionDescription new];
objectIdDesc.name = #"objectID";
objectIdDesc.expression = [NSExpression expressionForEvaluatedObject];
objectIdDesc.expressionResultType = NSObjectIDAttributeType;
r.propertiesToFetch = #[self.keyProperty,objectIdDesc];
NSError* error = nil;
NSArray* results = [context executeFetchRequest:r error:&error];
self.error = error;
if (results) {
for (NSDictionary* dict in results) {
_cache[dict[self.keyProperty]] = dict[#"objectID"];
}
} else {
_cache = nil;
}
}
2) when you need to test existence simply use:
- (NSArray*) existingObjectIDsForKeys:(NSArray *)keys
{
return [_cache objectsForKeys:keys notFoundMarker:[NSNull null]];
}
3) when you like to actually get objects and create missing ones:
- (NSArray*) findOrCreate:(NSArray*)keys
context:(NSManagedObjectContext*)context
error:(NSError* __autoreleasing*)error
{
NSMutableArray* fullList = [[NSMutableArray alloc] initWithCapacity:[keys count]];
NSMutableArray* needFetch = [[NSMutableArray alloc] initWithCapacity:[keys count]];
NSManagedObject* object = nil;
for (id<NSCopying> key in keys) {
NSManagedObjectID* oID = _cache[key];
if (oID) {
object = [context objectWithID:oID];
if ([object isFault]) {
[needFetch addObject:oID];
}
} else {
object = [NSEntityDescription insertNewObjectForEntityForName:self.entity.name
inManagedObjectContext:context];
[object setValue:key forKey:self.keyProperty];
}
[fullList addObject:object];
}
if ([needFetch count]) {
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:self.entity.name];
r.predicate = [NSPredicate predicateWithFormat:#"SELF IN %#",needFetch];
if([context executeFetchRequest:r error:error] == nil) {//load the missing faults from store
fullList = nil;
}
}
return fullList;
}
In this implementation you need to keep track of objects deletion/creation yourself.
You can use the register/unregister methods (trivial implementation) for this after a successful save.
You could make this a bit more automatic by hooking into the context "save" notification and updating the cache with relevant changes.
The multi-threaded case is much more complex (same interface but different implementation altogether when taking performance into account).
For instance, you must make your enforcer save new items (to the store) before returning them to the requesting context as they don't have permanent IDs otherwise, and even if you call "obtain permanent IDs" the requesting context might not save eventually.
you will also need to use a dispatch queue of some sort (parallel or serial) to access your cache dictionary.
Some math:
Given:
10K (10*1024) unique key objects
average key length of 256[byte]
objectID length of 128[byte]
we are looking at:
10K*(256+128) =~ 4[MB] of memory
This might be a high estimate, but you should take this into account ...
Ok, many things can go wrong here this is how to:
Create NSManagedObjectContext -> MOC
Create NSFetchRequest with the right entity
Create the NSPredicate and attache it to the fetch request
execute fetch request on newly created context
fetch request will return an array of objects matching the predicate
(you should have only one object in that array if your ids are distinct)
cast first element of an array to NSManagedObject
change its property
save context
The most important thing of all is that you use the same context for fetching and saving, and u must do it in the same thread cause MOC is not thread safe and that is the most common error that people do
Currently you say you maintain an array of `objectID's. When you need to you:
filter through them and get the messages with existingObjectWithID:error:
and after this you need to check if the message you got back:
exists
matches the one you want
This is very inefficient. It is inefficient because you are always fetching objects back from the data store into memory. You are also doing it individually (not batching). This is basically the slowest way you could possibly do it.
Why changes to that object aren't saved properly isn't clear. You should get an error of some kind. But, you should really change your search approach:
Instead of looping and loading, use a single fetch request with a predicate:
NSFetchRequest *request = ...;
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:#"XXX == %#", YYY];
[request setPredicate:filterPredicate];
[request setFetchLimit:1];
where XXX is the name of the attribute in the message to test, and YYY is the value to test it against.
When you execute this fetch on the MOC you should get one or zero responses. If you get zero, create + insert a new message and save the MOC. If you get one, update it and save the MOC.
Related
I have 2 NSSets with NSManagedObjects, the objects for each set are fetched in different threads, meaning some have a matching objectID, but the objects themselves are different. Now I want to remove managedObjects in one set from the other.
NSSet* oldObjects;
NSMutableSet* currentObjects;
// I want to remove the managedObjects in oldObjects from currentObjects, all objects in oldObjects are also in currentObjects
// This doesn't work, since the objects don't match
[currentObjects removeObjectsInArray:[oldObjects allObjects]];
// But strangely enough, this doesn't add any objects to currentObjects, but if the objects don't match, shouldn't it?
//[currentObjects addObjectsFromArray:[oldObjects allObjects]];
// This does work for me but this code is running on the main thread and I can see this becoming rather slow for large data sets
NSArray* oldObjectIDs = [[oldObjects allObjects] valueForKey:#"objectID"];
[currentObjects filterUsingPredicate:[NSPredicate predicateWithFormat:#"NOT (objectID IN %#)", oldObjectIDs]];
Is there a faster way I can filter these out? Would fast enumeration be faster even in this case?
Sorry for getting back to with such a delay.
I re-read your question, and now, that I've understood the setting completly I might have a solution for you.
This is not tested, but try something like this:
//Since the current objects set has registered its objects in the current context
//lets use that registration to see which of them is contained in the old object set
NSMutableSet* oldRegisteredSet = [NSMutableSet new];
for (NSManagedObject* o in oldObjects) {
NSManagedObject* regObject = [context objectRegisteredForID:[o objectID]];
if (regObject) {
//You could do here instead: [currentObjects removeObject:regObject];
//You should optimize here after testing performance
[oldRegisteredSet addObject:regObject];
}
}
[currentObjects minusSet:oldRegisteredSet];
Can anyone explain why given the following MagicalRecord import code
__block NSManagedObject *importedObject = nil;
[MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
id entityClass = NSClassFromString( name );
importedObject = [entityClass importFromObject:dictionary inContext:localContext];
}];
NSManagedObjectID *importedObjectID = importedObject.objectID;
NSManagedObject *relatedObject = ( (CustomRelatedExampleObject *) [[NSManagedObjectContext defaultContext] objectWithID:importedObjectID] ).relatedObject;
This works fine, setting the relationship and saving as expected
[MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
someObjectInDefaultContext.alsoRelated = relatedObject;
}];
But this causes exec bad access, when I expected that this is technically more correct because I am using the local context for saving my data. (Note: I left out the code that gets objectIDs from both objects for brevity)
[MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
AnotherCustomExampleObject *localSomeOtherObjectInDefaultContext = (AnotherCustomExampleObject *) [localContext objectWithID:someOtherObjectInDefaultContextObjectID];
CustomRelatedExampleObject *localRelatedObject = (CustomRelatedExampleObject *) [localContext objectWithID:localRelatedObjectID];
localSomeOtherObjectInDefaultContext.alsoRelated = localRelatedObject;
}];
I get exec bad access on the last line when I try to assign the object to the relationship in the other object.
UPDATE 1
This problem was caused by using temporary object ids when getting local copies of the objects in another managed object context.
UPDATE 2
I have discovered that simply changing the method used to retrieve the object removes the error.
[MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
AnotherCustomExampleObject *localSomeOtherObjectInDefaultContext = (AnotherCustomExampleObject *) [localContext existingObjectWithID:someOtherObjectInDefaultContextObjectID];
CustomRelatedExampleObject *localRelatedObject = (CustomRelatedExampleObject *) [localContext existingObjectWithID:localRelatedObjectID];
localSomeOtherObjectInDefaultContext.alsoRelated = localRelatedObject;
}];
using existingObjectWithID instead of objectWithID returns and object with a permanent id instead of a temporary one.
Check your object ids. If one or both are temporary, core data will not like that. But, in general, the crash you describe is because you are trying to relate two objects from a different context. I know you're going through the proper steps here, but perhaps double check in the debugger to make sure the context on those objects are the same.
Part of my iOS project polls a server for sets of objects, then converts and saves them to Core Data, to then update the UI with the results. The server tasks happens in a collection of NSOperation classes I call 'services' that operate in the background. If NSManagedObject and its ~Context were thread safe, I would have had the services call delegate methods on the main thread like this one:
- (void)service:(NSOperation *)service retrievedObjects:(NSArray *)objects;
Of course you can't pass around NSManagedObjects like this, so this delegate method is doomed. As far as I can see there are two solutions to get to the objects from the main thread. But I like neither of them, so I was hoping the great StackOverflow community could help me come up with a third.
I could perform an NSFetchRequest on the main thread to pull in the newly added or modified objects. The problem is that the Core Data store contains many more of these objects, so I have to add quite some verbosity to communicate the right set of objects. One way would be to add a property to the object like batchID, which I could then pass back to the delegate so it would know what to fetch. But adding data to the store to fix my concurrency limitations feels wrong.
I could also collect the newly added objects' objectID properties, put them in a list and send that list to the delegate method. The unfortunate thing though is that I have to populate the list after I save the context, which means I have to loop over the objects twice in the background before I have the correct list (first time is when parsing the server response). Then I still only have a list of objectIDs, which I have to individually reel in with existingObjectWithID:error: from the NSManagedObjectContext on the main thread. This just seems so cumbersome.
What piece of information am I missing? What's the third solution to bring a set of NSManagedObjects from a background thread to the main thread, without losing thread confinement?
epologee,
While you obviously have a solution you are happy with, let me suggest that you lose some valuable information, whether items are updated, deleted or inserted, with your mechanism. In my code, I just migrate the userInfo dictionary to the new MOC. Here is a general purpose routine to do so:
// Migrate a userInfo dictionary as defined by NSManagedObjectContextDidSaveNotification
// to the receiver context.
- (NSDictionary *) migrateUserInfo: (NSDictionary *) userInfo {
NSMutableDictionary *ui = [NSMutableDictionary dictionaryWithCapacity: userInfo.count];
NSSet * sourceSet = nil;
NSMutableSet *migratedSet = nil;
for (NSString *key in [userInfo allKeys]) {
sourceSet = [userInfo valueForKey: key];
migratedSet = [NSMutableSet setWithCapacity: sourceSet.count];
for (NSManagedObject *mo in sourceSet) {
[migratedSet addObject: [self.moc objectWithID: mo.objectID]];
}
[ui setValue: migratedSet forKey: key];
}
return ui;
} // -migrateUserInfo:
The above routine assumes it is a method of a class which has an #property NSManagedObjectContext *moc.
I hope you find the above useful.
Andrew
There's a section of the Core Data Programming Guide that addresses Concurrency with Core Data. In a nutshell, each thread should have its own managed object context and then use notifications to synchronize the contexts.
After a little experimentation, I decided to go for a slight alteration to my proposed method number 2. While performing background changes on the context, keep a score of the objects you want to delegate back to the main thread, say in an NSMutableArray *objectsOfInterest. We eventually want to get to the objectID keys of all the objects in this array, but because the objectID value changes when you save a context, we first have to perform that [context save:&error]. Right after the save, use the arrayFromObjectsAtKey: method from the NSArray category below to generate a list of objectID instances, like so:
NSArray *objectIDs = [objectsOfInterest arrayFromObjectsAtKey:#"objectID"];
That array you can pass back safely to the main thread via the delegate (do make sure your main thread context is updated with mergeChangesFromContextDidSaveNotification by listening to the NSManagedObjectContextDidSaveNotification). When you're ready to reel in the objects of the background operation, use the existingObjectsWithIDs:error: method from the category below to turn the array of objectID's back into a list of working NSManagedObjects.
Any suggestions to improve the conciseness or performance of these methods is appreciated.
#implementation NSArray (Concurrency)
- (NSArray *)arrayFromObjectsAtKey:(NSString *)key {
NSMutableArray *objectsAtKey = [NSMutableArray array];
for (id value in self) {
[objectsAtKey addObject:[value valueForKey:key]];
}
return objectsAtKey;
}
#end
#implementation NSManagedObjectContext (Concurrency)
- (NSArray *)existingObjectsWithIDs:(NSArray *)objectIDs error:(NSError **)error {
NSMutableArray *entities = [NSMutableArray array];
#try {
for (NSManagedObjectID *objectID in objectIDs) {
// existingObjectWithID might return nil if it can't find the objectID, but if you're not prepared for this,
// don't use this method but write your own.
[entities addObject:[self existingObjectWithID:objectID error:error]];
}
}
#catch (NSException *exception) {
return nil;
}
return entities;
}
#end
I'm having a problem figuring out how to represent a many-to-many relationship model in a NSTokenField. I have two (relevant) models:
Item
Tag
An item can have many tags and a tag can have many items. So it's an inverse to-many relationship.
What I would like to do is represent these tags in a NSTokenField. I would like to end up with a tokenfield automatically suggesting matches (found out a way to do that with tokenfield:completionsForSubstring:indexOfToken:indexOfSelectedItem) and being able to add new tag entities if it wasn't matched to an existing one.
Okay, hope you're still with me. I'm trying to do all this with bindings and array controllers (since that makes most sense, right?)
I have an array controller, "Item Array Controller", that is bound to my app delegates managedObjectContext. A tableview showing all items has a binding to this array controller.
My NSTokenField's value has a binding to the array controllers selection key and the model key path: tags.
With this config, the NSTokenField won't show the tags. It just gives me:
<NSTokenFieldCell: 0x10014dc60>: Unknown object type assigned (Relationship objects for {(
<NSManagedObject: 0x10059bdc0> (entity: Tag; id: 0x10016d6e0 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Tag/p102> ; data: <fault>)
)} on 0x100169660). Ignoring...
This makes sense to me, so no worries. I've looked at some of the NSTokenField delegate methods and it seems that I should use:
- (NSString *)tokenField:(NSTokenField *)tokenField displayStringForRepresentedObject:(id)representedObject
Problem is, this method is not called and I get the same error as before.
Alright, so my next move was to try and make a ValueTransformer. Transforming from an array with tag entity -> array with strings (tag names) was all good. The other way is more challenging.
What I've tried is to look up every name in my shared app delegate managed object context and return the matching tags. This gives me a problem with different managed object contexts apparently:
Illegal attempt to establish a relationship 'tags' between objects in different contexts (source = <NSManagedObject: 0x100156900> (entity: Item; id: 0x1003b22b0 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Item/p106> ; data: {
author = "0x1003b1b30 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Author/p103>";
createdAt = nil;
filePath = nil;
tags = (
);
title = "Great presentation";
type = "0x1003b1150 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Type/p104>";
}) , destination = <NSManagedObject: 0x114d08100> (entity: Tag; id: 0x100146b40 <x-coredata://9D77D47A-1171-4397-9777-706F599D7E3B/Tag/p102> ; data: <fault>))
Where am I going wrong? How do I resolve this? Is it even the right approach (seems weird to me that you woud have to use a ValueTransformer?)
Thanks in advance!
I've written a custom NSValueTransformer to map between the bound NSManagedObject/Tag NSSet and the NSString NSArray of the token field. Here are the 2 methods:
- (id)transformedValue:(id)value {
if ([value isKindOfClass:[NSSet class]]) {
NSSet *set = (NSSet *)value;
NSMutableArray *ary = [NSMutableArray arrayWithCapacity:[set count]];
for (Tag *tag in [set allObjects]) {
[ary addObject:tag.name];
}
return ary;
}
return nil;
}
- (id)reverseTransformedValue:(id)value {
if ([value isKindOfClass:[NSArray class]]) {
NSArray *ary = (NSArray *)value;
// Check each NSString in the array representing a Tag name if a corresponding
// tag managed object already exists
NSMutableSet *tagSet = [NSMutableSet setWithCapacity:[ary count]];
for (NSString *tagName in ary) {
NSManagedObjectContext *context = [[NSApp delegate] managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSPredicate *searchFilter = [NSPredicate predicateWithFormat:#"name = %#", tagName];
NSEntityDescription *entity = [NSEntityDescription entityForName:[Tag className] inManagedObjectContext:context];
[request setEntity:entity];
[request setPredicate:searchFilter];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
if ([results count] > 0) {
[tagSet addObjectsFromArray:results];
}
else {
Tag *tag = [[Tag alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
tag.name = tagName;
[tagSet addObject:tag];
[tag release];
}
}
return tagSet;
}
return nil;
}
CoreData seems to automatically establish the object relationships on return (but I have not completely verified this yet)
Hope it helps.
Your second error is caused by having two separate managed object context with the same model and store active at the same time. You are trying to create an object in one context and then relate it another object in the second context. That is not allowed. You need to lose the second context and make all your relationships within a single context.
Your initial error is caused by an incomplete keypath. From your description it sounds like you are trying to populate the token fields with ItemsArrayController.selectedItem.tags but that will just return a Tag object which the token filed cannot use. Instead, you need to provide it with something that converts to a string e.g. ItemsArrayController.selectedItem.tags.name
2 questions:
1) Do you have an NSManagedObjectContext being used other than your app delegate's context?
2) Is the object that implements tokenField:displayStringForRepresentedObject: set as the delegate for the NSTokenField?
If I create an array of strings (via key-value coding) containing the names of a Managed Object entity's attributes which are stored in the App Delegate the first time, I get an array of NSStrings without any problems. If I subsequently make the same call later from the same entry point in code, that same collection becomes an array of NULL objects- even though nothing in the Core Data Context has changed.
One unappealing work-around involves re-creating the string array every time, but I'm wondering if anyone has a guess as to what's happening behind the scenes.
// Return an array of strings with the names of attributes the Activity entity
- (NSArray *)activityAttributeNames {
#pragma mark ALWAYS REFRESH THE ENTITY NAMES?
//if (activityAttributeNames == nil) {
// Create an entity pointer for Activity
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Activity" inManagedObjectContext:managedObjectContext];
NSArray *entityAttributeArray = [[NSArray alloc] initWithArray:[[entity attributesByName] allValues]];
// Extract the names of the attributes with Key-Value Coding
activityAttributeNames = [entityAttributeArray valueForKeyPath:#"name"];
[entityAttributeArray release];
//}
return activityAttributeNames;
}
I never did figure out what was happening. I gave up, rebuilt the data model, and all was fine.