Exclude entity from NSPersistentDocument - core-data

I have an existing (and fully working) app using NSPersistentDocument to save the application files.
Now I need to create a new entity, this new entity is totally unrelated to the application files and it will contain the application cache, so I will use it to save on a separated file.
My project contains the MyDocument.xcdatamodeld used by NSPersistentDocument, to implement the new feature I created a new data model Cache.xcdatamodeld and added a new entity to the model (I have not written code just used XCode wizards) but when I run the app and try to open an existing app file I receive the error
The model used to open the store is incompatible with the one used to
create the store
I understand this occurs because the model configuration for new entity is the same for NSPersistentDocument but how can I decouple it?
Creating a new configuration in data model doesn't work because the entity can't be deleted from the default one.
Any idea how to make NSPersistentDocument ignore the new entity and continue to work with the old data model?
I don't post source code because this happens simply adding the new model and entity to project

From the documentation of NSPersistDocument's managedObjectModel property:
#property(readonly, strong) NSManagedObjectModel *managedObjectModel
Discussion
By default the Core Data framework creates a merged model from all models in the application bundle ([NSBundle mainBundle]). You can reimplement this property and return a specific model to use to create persistent stores. A typical implementation might include code similar to the following fragment:
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *path = [bundle pathForResource:#"MyModel" ofType:#"mom"];
NSURL *url = [NSURL fileURLWithPath:path];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:url];

Related

FileDocument with UIManagedDocument/core data

When using SwiftUI to create a document based app, the default document type is to subclass FileDocument.
All examples lead to simple value types to be used in this document type.
I'm looking to create a UIManagedDocument in SwiftUI but there doesn't seem to be any mention of using FileDocument with core data. I noticed a ReferenceFileDocument but this leads to no examples either...
Has anyone had any experience of using either SwiftUI document type for core data based documents?
After some more months, I came across this question once again.
Since my last comment on September 18th, I've worked myself on solving the puzzle of building a SwiftUI document-based app using Core Data.
Looking more in-depth I learned that the UIManagedDocument (respectively its parent UIDocument) infrastructure is really close/similar to what SwiftUI tries to implement. SwiftUI even uses UIDocument in the background to do "its magic". UIDocument and UIManagedDocument are simply some more archaic remnants of times where Objective-C was the dominant language. There was no Swift and there were no value-types.
In general I can give you the following tips to solve your challenge using Core Data within a UIManagedDocument:
first of all, if you want to use Core Data, you will have to use a package/bundle based document format. This means your UTType will have to conform to .package (=com.apple.package in your Info.plist file). You won't be able to make Core Data work with only a plain file document.
extension UTType {
static var exampleDocument: UTType {
UTType(exportedAs: "com.example.mydocument", conformingTo: .package)
}
}
use a ReferenceFileDocument based class to build a wrapper document for your UIManagedDocument. This is necessary because you will have to know about the time when the object is released. In deinit you will have to call managedDocument.close() to ensure the UIManagedDocument is properly closed and no data is lost.
the required function init(configuration:) is going to be called when an existing document is opened. Sadly, it is of no use when working with UIManagedDocument because we have only access to the FileWrapper of the document and not the URL. But the URL is what you need to initialize the UIManagedDocument.
the required function snapshot(contentType:) and fileWrappper(snapshot:, configuration:) is only used to create a new empty document. (This is because we won't use the SwiftUI integrated UndoManager but the one from UIManagedDocument respectively Core Datas NSManagedObjectContext.) Therefore it is not relevant what your type for Snapshot is. You can use a Date or Int because the snapshot taken with the first function is not what you are going to write in the second function.
The fileWrappper(snapshot:, configuration:) function should return the file structure of an empty UIManagedDocument. This means, it should contain a directory StoreContent and an empty file with the filename of the persistent store (default is persistentStore) as in the screenshot below.
The persistentStore-shm and persistentStore-wal files are going to be created automatically when Core Data is starting up, so we do not have to create them in advance.
I am using the following expression to create the FileWrapper representing the document: (MyManagedDocument is my UIManagedDocument subclass)
FileWrapper(directoryWithFileWrappers: [
"StoreContent" : FileWrapper(directoryWithFileWrappers: [
MyManagedDocument.persistentStoreName : FileWrapper(regularFileWithContents: Data())
])
])
above steps allow us to create an empty document. But it still cannot be connected to our UIManagedDocument subclass, because we have no idea where the document (represented by the FileWrapper we have created) is located. Luckily SwiftUI is passing us the URL of the currently opened document in the ReferenceFileDocumentConfiguration which is accessible in the DocumentGroup content closure. The property fileURL can then be used to finally create and open our UIManagedDocument instance from the wrapper. I'm doing this as follows: (file.document is an instance of our ReferenceFileDocument class)
DocumentGroup(newDocument: { DemoDocument() }) { file in
ContentView()
.onAppear {
if let url = file.fileURL {
file.document.open(fileURL: url)
}
}
}
in my open(fileURL:) method, I then instantiate the UIManagedDocument subclass and call open to properly initialize it.
With above steps you will be able to display your document and access its managedObjectContext in a view similar to this: (DocumentView is a regular SwiftUI view using for example #FetchRequest to query data)
struct ContentView: View {
#EnvironmentObject var document: DemoDocument
var body: some View {
if let managedDocument = document.managedDocument {
DocumentView()
.environment(\.managedObjectContext, managedDocument.managedObjectContext)
} else {
ProgressView("Loading")
}
}
}
But you will soon encounter some issues/crashes:
You see the app freeze when a document is opened. It seems as if UIManagedDocument open or close won't finish/return.
This is due to some deadlock. (You might remember, that I initially told you that SwiftUI is using UIDocument behind the scene? This is probably the cause of the deadlock: we are running already some open while we try to execute another open command.
Workaround: run all calls to open and close on a background queue.
Your app crashes when you try to open another document after having previously closed one. You might see errors as:
warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Item' so +entity is unable to disambiguate.
warning: 'Item' (0x6000023c4420) from NSManagedObjectModel (0x600003781220) claims 'Item'.
warning: 'Item' (0x6000023ecb00) from NSManagedObjectModel (0x600003787930) claims 'Item'.
error: +[Item entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
After having debugged this for some hours, I learned that the NSManagedObjectModel (instantiated by our UIManagedDocument) is not released between closing a document and opening another. (Which, by good reason, is not necessary as we would use the same model anyway for the next file we open). The solution I found to this problem was to override the managedObjectModel variable for my UIManagedDocument subclass and return the NSManagedObjectModel which I'm loading "manually" from my apps bundle. I suppose there are nicer ways to do this, but here is the code I'm using:
class MyManagedDocument: UIManagedDocument {
// We fetch the ManagedObjectModel only once and cache it statically
private static let managedObjectModel: NSManagedObjectModel = {
guard let url = Bundle(for: MyManagedDocument.self).url(forResource: "Model", withExtension: "momd") else {
fatalError("Model.xcdatamodeld not found in bundle")
}
guard let mom = NSManagedObjectModel(contentsOf: url) else {
fatalError("Model.xcdatamodeld not load from bundle")
}
return mom
}()
// Make sure to use always the same instance of the model, otherwise we get crashes when opening another document
override var managedObjectModel: NSManagedObjectModel {
Self.managedObjectModel
}
}
So this answer has become really lengthy, but I hope it is helpful to others struggling with this topic. I've put up this gist with my working example to copy and explore.

Core Data: NSObjectID and NSTemporaryObjectID leaks

Before I send my app to the App Store I like to check it for memory leaks and other fishy stuff with instruments. There is one Core Data issue that I can't seem to solve, so I've decided to create a small test app to illustrate the problem.
What's the problem?
When I save an entity in a (child) NSManagedObjectContext it is propagated to its parent NSManagedObjectContext. During this process Core Data creates internal instances of _NSObjectID and NSTemporaryObjectID. For some reason these instances are left behind and the only way to get rid of them is to reset the parent NSManagedObjectContext.
My app is of course a lot more complex than this little test app and resetting the NSManagedObjectContext isn't an option for me.
Test app
The test app is a standard iOS app based on the single view template with the CoreData option checked. I've used objective-c to keep it similar to my production app.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Initialize the Core Data stack
self.persistentStoreCoordinator = [self persistentStoreCoordinator];
// Create the a private context
self.rootContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.rootContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
// Create a child context
self.childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.childContext.parentContext = self.rootContext;
// Create a person
[self.childContext performBlockAndWait:^{
Person *person = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.childContext];
person.name = #"John Smith";
person.age = 30;
// Save the person
[self.childContext save:nil];
// Save the root context
[self.rootContext performBlockAndWait:^{
[self.rootContext save:nil];
}];
}];
return YES;
}
When you run the code above with instruments and the allocations instrument you can see that Core Data leaves some stuff behind.
You can find the full project here: https://github.com/Zyphrax/CoreDataLeak
Things I've tried
I've tried things like [context refreshObject:... mergeChanges:YES], adding #autoreleasepool and/or [context processPendingChanges] inside the blocks, it all doesn't help. The only way to get it clean is to do a [context reset] (sledgehammer approach).
It's hard to find other people reporting this problem.
This blog post seems similar:
http://finalize.com/2013/01/04/core-data-issues-with-memory-allocation/
I hope you guys can help me with this.
Here is what I see, which is very similar to yours...
However, I don't know that I would be concerned, unless you see lots of these, and they never go away. I assume the internals of Core Data (including the row cache has) some sort of object caching going on.
On the other hand, my Core Data usage has changed a bit over the past year or two.
Unless it is a very simple app, I almost never create new objects in a child context. I will fetch and modify them, but if I end up creating a new object, I make sure that is done in a sibling context.
However, if you modify your code slightly, by adding this line (with your appropriate error handling - it returns BOOL) before the initial save...
NSArray *inserted = self.childContext.insertedObjects.allObjects;
[self.childContext obtainPermanentIDsForObjects:inserted error:&error];
you should get something like this instruments report, which shows all objects created as being transient...
Thus, I don't necessarily think it is a permanent leak, because once I force the context to convert to a permanent ID, the objects go away. However, who knows how long they keep those object ID objects cached.
In general, when I create objects in a context that contains a hierarchy, I will always obtain permanent IDs first (for many reasons). However, as I said earlier, I usually create new objects in a context that is directly created to the persistent store (because I have had to deal with other issues related to hierarchies temporary object IDs, especially when using multiple non related contexts).

How can a custom NSManagedObject be created without store it in context?

How can I create a custom managed object, but without to save, just keep it in the memory and when app stops, temporary managed object can be dealloched too. But same time other managed objects I need to save.
There are a couple of possibilities depending on how your app works.
One is to just create the object and just not insert it. It's just that simple. Pass a nil value for the context.
NSManagedObjectModel *managedObjectModel =
[[self.managedObjectContext persistentStoreCoordinator] managedObjectModel];
NSEntityDescription *entity = [[managedObjectModel entitiesByName] objectForKey:#"EntityName"];
NSManagedObject *myObject = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
If you later want to insert the object, use [NSManagedObjectContext insertObject:].
Another is to create an in-memory Core Data store. Create a second persistent store, but replace NSSQLiteStoreType with NSInMemoryStoreType. Then create and use objects as usual. When the app exits, the in-memory store will just disappear with all of its objects.

A Second In-Memory-Store As A Cache

Dear community.
I try to discover opportunity to using 2 persistent stores for improve performance of my application.
What i do here:
CREATE 2 PERSISTENT STORES
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:[NSNumber numberWithBool:YES]
forKey:NSMigratePersistentStoresAutomaticallyOption];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType
configuration:nil
URL:[NSURL URLWithString:#"memory://store"]
options:dict
error:&error])
{
[[NSApplication sharedApplication] presentError:error];
[persistentStoreCoordinator release], persistentStoreCoordinator = nil;
return nil;
}
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:url
options:dict
error:&error])
{
[[NSApplication sharedApplication] presentError:error];
[persistentStoreCoordinator release], persistentStoreCoordinator = nil;
return nil;
}
ASSIGN new created objects to in-Memory store
NSManagedObject *objectCarrier = [NSEntityDescription
insertNewObjectForEntityForName:#"Carrier"
inManagedObjectContext:managedObjectContext];
[objectCarrier setValue:startForCarrier
forKey:#"name"];
NSURL *url = [NSURL URLWithString:#"memory://store"];
[managedObjectContext assignObject:objectCarrier
toPersistentStore:[[appDelegate persistentStoreCoordinator] persistentStoreForURL:url]];
SAVE FINAL OBJECT
A difference between in-memory and particular persistent store using is
i have wrong using objects from predicates for same code.
If i just change persistent store type, i pickup object:
NSManagedObject *destination = [[codeAfterComparing lastObject] valueForKey:codeRelationshipName];
But set values for this object is doesn't work.
If i try to assignObject for received object, i have error (it's doesnt matter, how this object was save as inMemory or asSqlLite store object, error is start every time).
2011-02-16 14:32:45.037 snow
server[44411:1803] * Terminating app
due to uncaught exception
'NSInvalidArgumentException', reason:
'Can't reassign an object to a
different store once it has been
saved.'
Attempt to save a final object's graph with two different stores gives me error "CoreData does not support persistent cross-store relationships", and it's doesn't matter, where cureent object assing.
Migration was as :
for (NSPersistentStore *persistentStore in [persistentStoreCoordinator persistentStores]) {
if (persistentStore.type == NSInMemoryStoreType) {
// migrate the in-memory store to a SQLite store
NSError *error;
[persistentStoreCoordinator migratePersistentStore:persistentStore toURL:[NSURL fileURLWithPath:[[self applicationSupportDirectory] stringByAppendingPathComponent:#"storedata.sql"]] options:nil withType:NSSQLiteStoreType error:&error];
if (! newPersistentStore) {
Product error: "Can't add the same store twice"
So, the result is a very strange for me:
1. Looks like managed object context have no difference for objects between 2 stores. If i ask save, it take whole object and save so same sqlite store
2. maybe a way to using different persistent store coordinator's but i don't know exactly, how is easy transfer objects between 2 stores. Of course, i can do a copy (include relationships e.t.c.) but this is a hard code for this simple issue, i guess.
Maybe somebody can suggest about my code wrong or good working examples of code to review and understand a good way to do in memory cache with core data? Google search gives not too much examples.
If you look at the Core Recipe example code on Apple's website, they use multiple stores to save objects in memory and on disk.
Thought I'd take a stab here.
I've had this problem in the past. I ended up removing the functionality for two persistent stores in the same coordinator. If I understand Apple correctly, Object Entities cannot be shared between persistent stores. So to make things easier, I usually just do the following (though I suspect there is an efficiency issue with using an additional Coordinator)
1 NSPersistentStore per NSPersistentStoreCoordinator
break up the scratchpad work to the NSManagedObjectContexts
create a deep-copy method to your NSManagedObject subclasses
And then, when whatever class you have managing each persistent store utilize the copy function to import the managed objects.
I can't really think of an instance where you'd want to go through the extra trouble of individually assigning the managed objects to a specific store that wouldn't be taken car of in this way.
I have a program that utilizes two stores - one in memory for transient objects and another managing the document. It's working just fine.
In iOS 5 Apple introduce Nested Managed Object Contexts where you can work with two Managed Object contexts.
This may replace your approach with the in memory store because e.g. you can now use one of the (also) new concurrency types to run one context in the background (e.g. for background fetching) and another as your main context.
Take a look in the WWDC2011 Session 303.

What do I have to do to get Core Data to automatically migrate models?

I have read the documentation about automatic /lightweight migration for Core Data models - but I am having problems implementing it.
As I understand it the application should notice that the model it has and the model that exists on a device already are not the same. If you have only added attributes or relationships and similar simple changes then the model should be upgraded automatically.
Any pointers - do I need to set something in Xcode?
I've now found out that this is quite simple - once you know where to look.
In my AppDelegate I set-up the NSPersistentStoreCoordinator - and you need to add some options to this to tell it to handle auto-migrate:
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
NSError *error;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
// Handle error
NSLog(#"Problem with PersistentStoreCoordinator: %#",error);
}
Then you need to do a little trick in xCode:
Select your xcdatamodel file
Select the Design Menu at the top - then Data Model - then choose Add Model Version
Your xcdatamodel file will then get moved into a new directory with the same name as your xcdatamodel file but with the extension xcdatamodeld - there will be a second file in this directory with a 2 in the name. Select the new file and then Design->Data Model->Set Current Version (in Xcode 4 you do this)
If you have already made the changes that have caused your project to be incompatible - take these changes out of the original xcdatamodel file. If you have yet to make the changes - then just edit the 2.xcdatamodel file (the one you just made current version).
Now when you install this version onto a device that has the old model - it will automatically upgrade that model to the new model.
This seems great and as simple as I wanted - but I think you need to be careful during development as you change a model - otherwise you will have to create a new version for each change.
I think what I will do is that I will keep all of the changed files and then once I get ready to deploy my update I'll delete all the in-between files and just deploy with the oldest and latest models.
UPDATE (15/07/2011):
Thanks to #rockstarberlin for pointing out there is updated documentation at apple:
Xcode 4: Setting a Managed Object Model’s Current Version
Update: 8/19/2013 better link:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreDataVersioning/Articles/vmModelFormat.html
This was incredibly helpful. The Apple documentation was -- as usual -- woefully incomplete. I recommend doing a clean build, as I ran into an error "Can't merge models with two different entities xxx" when I first ran after making these changes. The clean build fixed it up.
Grouchal's answer is perfect...but if you are still having the "Can't merge models with two different entities xxx" even after cleaning up the build several times...Your might have issues with how the managedObjectModel is being loaded...take at look at this one...which helped me fix it..
core data migration problems
Also, if you stumbled upon this post, like I did, after getting the "The model used to open the store is incompatible with the one used to create the store" error and you are just debugging using the simulator and wanting to completely replace the old model installed, you can just Reset the Simulator app or deleting your app from the simulator would probably work as well.
It didn't occur to me to try this until reading the posts here, at which point I realized that I had installed the app in the simulator and then subsequently changed the model, causing the aforementioned run-time error.
To follow up on Santthosh's answer, figured I'd post the code snippet right here instead. You need to create your managedObjectModel with initWithContentsOfURL: instead of mergedModelFromBundles: otherwise you'll get error:
Can't merge models with two different
entities XXX and XXX
If your Model file is named "Model", here's how you create the managedObjectModel:
NSString *path = [[NSBundle mainBundle] pathForResource:#"Model" ofType:#"momd"];
NSURL *momURL = [NSURL fileURLWithPath:path];
managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];
Credit to this blog post.
the menu in Xcode 4 changed a bit. here´s a description how to do it in Xcode 4:
Xcode 4: Setting a Managed Object Model’s Current Version
I've had this issue for years, and I tried all of these answers to no avail. Today I finally figured out what I was doing wrong. Very simple problem, but I overlooked it. When creating a newer version of the data model, if you are ADDING columns make sure to mark them as OPTIONAL. If you do not the simple migration will not work because the new column values will not be filled in.
As soon as I made sure my new columns has "optional" checked, I tried the migration again and it worked.
I stumbled onto this post because of a different problems, but the error was "The model configuration used to open the store is incompatible with the one that was used to create the store."
Here was my problem and the solution to it. In my model, I was using configurations. I had some of the entities being stored in one file and the others in a second file. (I have some defaults that might periodically need to get downloaded, and it would be an incredible pain to merge them into the whole). Anyhow, I made a new entity. The program seemed to run fine, but whenever I'd quit, I got the above error.
The solution there was to look at my configurations, realize that I had an entity that wasn't currently in any of the configurations, and add it to one. Runs like a dream.
This won't fix the OP's problem. But maybe some frustrated person who lands here via google will be in the boat I was in :)
iOS 4.0+
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"model" withExtension:#"momd"];
managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
Minor edit to #Grouchal's awesome instructions above for Xcode version 5:
Old:
2. Select the Design Menu at the top - then Data Model - then choose Add Model Version
Version 5+:
2. Select the Editor menu, then Add Model Version…, type your Version name and Based on model (select your original model from the list)

Resources