Switching between Core Data stores - ios

I am currently updating an app to use Core Data. The app you could say is a "database viewer", only one database is able to be viewed at a time. Each database is kept in its own separate folder. Currently the data is downloaded and stored as a set of plist files.
In the new version I need to convert these plist databases into Core Data stores (one store for each database.) I've already setup the methods that create the separate store files, and crete the entities. The problem is that all the entities are saved to the first database I created, not to the "current" or "lastly created" file.
The basic process I'm using is:
//For each database {
//Create the sqlite file and set up NSManagedObjectContext
[MagicalRecord setupCoreDataStackWithStoreNamed:
[NSURL fileURLWithPath:
[NSString stringWithFormat:#"%#/%#/%#.sqlite",
dirPath, directory, directory]]];
NSManagedObjectContext *managedObjectContext =
[NSManagedObjectContext MR_contextForCurrentThread];
//Iterate through all the plist files and create the necessary entities.
//Save new entities to file
[managedObjectContext MR_save];
//Clean up all cashes
[MagicalRecord cleanUp];
}
How would one properly switch between stores, essentially "reseting" everything between each switch. Preferably (if possible) using magical record.
EDIT:
I've found out a portion of the problem, and removed most of the unwanted behavior. It turns out, you can't reliably call [MagicalRecord cleanUp] on a background thread. Also, It isn't doing what I think it should (see below). I ended up calling back to the main thread after each "save" to reset the Core Data stack. Doing this creates a new context for the first three databases. after that, it duplicates the context from the database three databases ago. So the same three contexts are used in a loop.
This is what I currently have;
I start the process by creating a background thread and run the code to create a single database in the background:
backgroundQueue = dispatch_queue_create("com.BrandonMcQuilkin.myQueue", NULL);
dispatch_async(backgroundQueue, ^(void) {
[self createSQLiteDatabase:updateList];
});
Then creating the stack and database:
- (void)createSQLiteDatabase:(NSArray *)updateList
{
NSString *directory = [updateList objectAtIndex:0];
[MagicalRecord setupCoreDataStackWithStoreNamed:
[NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/%#/%#.sqlite",
dirPath, directory, directory]]];
NSManagedObjectContext *managedObjectContext =
[NSManagedObjectContext MR_contextForCurrentThread];
//Check to see if the stack has reset
NSLog(#"Before:%i", [[Competition MR_findAllInContext:managedObjectContext] count]);
//Create and add entities to context...
//Prepare for next loop
NSLog(#"After:%i", [[Competition MR_findAllInContext:managedObjectContext] count]);
[managedObjectContext MR_saveNestedContexts];
[NSManagedObjectContext MR_resetContextForCurrentThread];
NSMutableArray *temp = [[NSMutableArray alloc] initWithArray:updateList];
[temp removeObjectAtIndex:0];
dispatch_async(dispatch_get_main_queue(), ^(void){
[self shouldContinueUpdating:temp];
});
Then reset everything and repeat for all databases:
- (void)shouldContinueUpdating:(NSArray *)databases
{
//preform cleanup on main thread and release background thread
[MagicalRecord cleanUp];
dispatch_release(backgroundQueue);
if ([databases count] != 0) {
backgroundQueue = dispatch_queue_create("com.BrandonMcQuilkin.myQueue", NULL);
dispatch_async(backgroundQueue, ^(void) {
[self createSQLiteDatabase:databases];
});
}
}
With the two NSLogs, I get this in the console: (using six databases, the pattern is the same no matter how many databases I convert.)
//First Loop
Before:0
After:308
//Second Loop
Before:0
After:257
//Third Loop
Before:0
After:37
//Fourth Loop
Before:308
After:541
//Fifth Loop
Before:257
After:490
//Sixth Loop
Before:37
After:270
... Keep adding to each of the three contexts.
And [MagicalRecord cleanUp] isn't doing what It say it's doing. Here is what the method is supposed to do.
+ (void) cleanUpStack;
{
[NSManagedObjectContext MR_cleanUp];
[NSManagedObjectModel MR_setDefaultManagedObjectModel:nil];
[NSPersistentStoreCoordinator MR_setDefaultStoreCoordinator:nil];
[NSPersistentStore MR_setDefaultPersistentStore:nil];
}
But It turns out that the NSStoreCoordinator every time I save, is the same coordinator, in the same memory location, and each store is hanging around. Something is not working right...

MagicalRecord may not be the best tool for this job for you...
First, let's correct your usage of the setupCoreDataStackWithStoreNamed: method. The parameter takes an NSString, not a URL, nor a file path. MagicalRecord will pick the proper path for you and create your store there. your resulting sqlite file is likely to be named with the path you intended it to be.
Next thing, you'll need to dynamically create your CoreData model for this file. This is kind of tough, but possible. You'll need to traverse these plist files, and interpret entities, attributes and relationships, and create corresponding NSEntityDescriptions, NSAttributeDescriptions and NSRelationshipDesctiptions and populate an NSManagedObjectModel "manually". Youll want to look for the method
- [NSManagedObjectModel setEntities:(NSArray *)]
as well as the creation methods for NSEntityDescription, NSAttributeDescription and NSRelationshipDescription.
You'll also want to save this model somewhere so you don't have to recreate it every time. Luckily, it conforms to NSCoding, so you should just be able to save it to disk.
After that, you'll probably want to populate your data. From here, MagicalRecord can help you. I suggest looking at the Importing Data Made Easy blog post I wrote for Cocoa is My Girlfriend
If you want to "switch stores", which I guess means you want to create a new store for each plist file you've got, then you're going to have to tear down the entire Core Data stack for each file. If you manage to use MagicalRecord for this project, you'll need to look at [MagicalRecord cleanUp], and start over. If each model was the same, you could get by with releasing your Persistent Store Coordinator, and creating a new one to your store. But since your "schemas" will probably be different, you'll just want to scratch everything and start over.

Turns out The problem I'm having is because of a bug in MagicalRecord. I've submitted a git issue here: https://github.com/magicalpanda/MagicalRecord/issues/270

Related

Magical Record, multiple configurations and Restkit

I am using Magical Record in a fairly large IOS project. I use configurations to separate a large seed database from user data. Since Magical Record doesn't support configurations, I deconstructed Magical Record's setupCoreDataStackWithAutoMigratingSqliteStoreNamed method and replaced it with the following:
+(void) RB_setupMultipleStores:(NSString *) seedStoreName userStore:(NSString *) userStoreName
/* change persistent store to one with multiple configurations. Assumes Magical Record is initialized to perform auto migration. */
{
NSError * error= nil;
[MagicalRecord cleanUp]; //Tear down Magical Record
NSManagedObjectModel * model = [NSManagedObjectModel MR_defaultManagedObjectModel];
NSURL *seedURL = [NSPersistentStore MR_urlForStoreName:[seedStoreName stringByAppendingString:#".sqlite"]];
NSPersistentStoreCoordinator * coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
nil];
NSPersistentStore * seedStore =[coordinator
addPersistentStoreWithType:NSSQLiteStoreType
configuration:#"Seed"
URL:seedURL
options:options
error:&error];
if (!seedStore || error)
{
NSLog(#"Error setting up seed store:%# for %#", [error localizedDescription], seedURL);
exit(-1);
}
NSURL *userURL = [NSURL URLForDocumentDirectoryWithAppendedPath:[userStoreName stringByAppendingString:#".sqlite"]];
NSPersistentStore * userStore = [coordinator
addPersistentStoreWithType:NSSQLiteStoreType
configuration:#"User"
URL:userURL
options:options
error:&error];
if (!userStore || error)
{
NSLog(#"Error setting up user store:%# for %#", [error localizedDescription], userURL);
exit (-1);
}
[NSPersistentStoreCoordinator MR_setDefaultStoreCoordinator:coordinator];
//Bring back Magical Record with updated coordinator.
[NSManagedObjectContext MR_initializeDefaultContextWithCoordinator:coordinator];
[[NSManagedObjectContext MR_defaultContext] setUndoManager:nil];
}
Now I will be adding Restkit to the mix. I'd need to share the object model and persistent store, and I'd rather use one set of contexts rather than have two different stacks.
I see five potential approaches:
Modify https://github.com/blakewatters/RKMagicalRecord to support multiple configurations. This looks trivial but it requires I use a category to surface some private methods, and the good MR developers recommend against explicitly setting MR default and root saving contexts.
Create the Magical Record contexts first, and then assign them to Restkit. Will this even work? Does it make sense at all?
Initialize Restkit and Magical Record from the same NSPersistentStoreCoordinator. Does this make sense?
Create two separate stacks with different but similar NSPersistentStoreCoordinators.
Create my own stack and contexts, and make all calls to Restkit and MR using those contexts.
Can anyone recommend any of these or any other approaches? Each requires significant effort to even test. I'm about to head down the #1 path.
Thanks...
Fundamentally, CoreData has ways to deal with this already. You can have multiple coordinators point to the same store, or whatever your scenario is. It's up to each framework to allow you to be used against a stack that framework itself did not create or setup.
MagicalRecord boils down to a collection of helper methods for fetching and saving. You don't need to use ANY of the setup methods for MagicalRecord to work its magic on your fetches. The setup methods are there to help you get going faster on new projects, as well as providing a single access point for a "default" contexts for use when you perform fetches on the main thread/queue. For all other use, use the explicit inContext: parameter every time. Using that parameter, you can use whatever MOC you want, everything will work. This is by design. MagicalRecord was never written to replace CoreData. It was written to make the simple tasks easier.
With that, you could just let RestKit deal with your entire CoreData stack and use MagicalRecord only for the convenience APIs for fetching and saving. This would save you having to do any of those solutions and just get back to tackling your app specific issues...

iOS Core Data data insert

I have an app which asynchronously downloads a JSON file and then it should insert those objects within Core Data for persistent storage. Regarding the insert, is it a good idea to do it from the main thread? What if there are thousands of objects? Should I do the inserts on a different thread? Could you provide me with some snippets regarding this matter? Regarding the fetching of the objects after I've saved them, should I also use a different thread?
My code for inserting into Core Data is:
- (void) insertObjects:(NSArray*)objects ofEntity:(NSString *)entityName
{
NSString *key;
NSManagedObject *managedObject;
NSError *error;
for(NSDictionary *dict in objects){
managedObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:_managedObjectContext];
for(key in dict){
[managedObject setValue:dict[key] forKey:key];
}
}
[_managedObjectContext save:&error];
}
PS: The objects are of the same entity. The project runs on iOS 7.0 or higher.
Since I can't comment yet..
What iOS Versions do you plan to support? If 5 and higher, this might help Concurrency stack
Summary of the link:
you create a context of private concurreny type to access your physical data
based on this you create a context of main concurreny type
on top of this you use private concurrency type stores again.
Don't forget so save in every store, otherwise, the data seems to be saved while the app is running, but after restart it is lost.
And yes, you want to do it an extra thread, since it would otherwise block the UI if there are to many items.

How do I have Core Data managedObjects created from JSON file and have them persist between app launches? [duplicate]

This question already has an answer here:
Checking for duplicates when importing to CoreData
(1 answer)
Closed 8 years ago.
I have a bunch of NSManagedObjects that are created from a JSON file online. Currently, I am creating them all each time the app launches (not ideal).
What is the best way to check to see if the objects are already there before I try to create them?
if I do [self saveContext] it seems to work, but as I don't know how to check if they are already loaded, it ends up duplicating everything.
Obviously, I am relatively new to Core Data and seem to be missing a key concept.
[EDIT] After reading more and more about where and when to load this many objects into Core Data, it looks like pre-loading the data is the best option for me (the data is static and will likely only be update a few times per year).
I chose not to use the "find or create pattern" as I assumed it would be more expensive given the number of objects that need to be checked/created and would like to save learning about background queues for next time ;)
I was then having trouble getting the sqlite file to work, and solved it by saving the context after each object was created, rather than once after all the objects were loaded.
The way this is handled usually in my experience is via one of the two options:
You first check if the item exists, and if it does, then you update it, else insert it. Here's a sample of what I have used in the past for a vouchers model:
Voucher *newObject = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Voucher"];
request.predicate = [NSPredicate predicateWithFormat:#"voucher_id = %#",[dictionary objectForKey:#"voucher_id"]];
NSError *error = nil;
NSArray *matches = [context executeFetchRequest:request error:&error];
if ([matches count] == 0 ){
newObject = [NSEntityDescription insertNewObjectForEntityForName:#"Voucher" inManagedObjectContext:context];
newObject.number = [json_dictionary objectForKey:#"number"];
newObject.valid_from = [json_dictionary objectForKey:#"valid_from"];
newObject.valid_to = [json_dictionary objectForKey:#"valid_to"];
}
else {
newObject = [matches lastObject];
newObject.number = [json_dictionary objectForKey:#"number"];
newObject.valid_from = [json_dictionary objectForKey:#"valid_from"];
newObject.valid_to = [json_dictionary objectForKey:#"valid_to"];
newObject.voucher_id = [json_dictionary objectForKey:#"voucher_id"];
}
return newObject;
The other way is to select all, put into an NSOrderedSet, and then run a comparison, and only insert if not in the set.
If you look at "Core Data Performance Optimization and Debugging" on this page https://developer.apple.com/wwdc/videos/ , it's got a great explanation of this
If you haven't worked on it before, the learning curve might be a bit steep. But one good way is to use RestKit.
https://github.com/RestKit/RestKit/wiki/Object-mapping#core-data
Ray Wenderlich has a detailed tutorial on Core Data that show you how to do it step by step: (make sure to turn on Google Translate)
In response to your question under comments, here it is:
create a new file and choose to create datamodel (under Core Data)
add your entities - entities are what you declared as class data models. Note that I have Location, Marker, and Village because I have created those as classes (Location.m/.h, etc)]
Add attributes (properties) associated with those entities.
http://i.stack.imgur.com/wOUvF.png
http://i.stack.imgur.com/5AJGZ.png

Magical Record appears to save, but changes are lost if app is terminated

I'm using Magical Record 2.1 to handle the data persistence in my app. If I create a new entity, set some it's attributes and save, it works fine. However, later, if I fetch that entity, update it's attributes and save, subsequent fetches have the new data until I terminate the app and restart. During the new app session the old data reappears.
This is how I create a new entity:
self.localContext = [NSManagedObjectContext MR_defaultContext];
self.theNewListing = [Listing MR_createInContext:self.localContext];
I'm using MRDefaultContext having read this blog post: http://saulmora.com/2013/09/15/why-contextforcurrentthread-doesn-t-work-in-magicalrecord/
In this case my main attribute is a dictionary, and I set it like this:
NSMutableDictionary *tempDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:#"A description", #"slFieldDescription", etc, etc, nil];
self.theNewListing.dataDictionary = tempDictionary;
This is how I save it:
[self.presentingViewController dismissViewControllerAnimated:YES completion:^(void) {
[self.localContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error){
if(!success) {
NSLog(#"%#", error);
}
else {
[self.thePresentingVC refreshCollectionViews:nil];
}
}];
}];
I display my data in a collection view, and at this point everything looks fine. If I terminate and restart the data is still there.
If I fetch the entity again and update the attributes like this:
NSMutableDictionary *newTempDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:#"A new description", #"slFieldDescription", etc, etc, nil];
self.theNewListing.dataDictionary = newTempDictionary;
Then save using the same save code as above, and then update my collection view using the code below, all looks good.
self.listingsArray = [[NSMutableArray alloc] initWithArray:[Listing MR_findAllSortedBy:#"dateListed" ascending:NO]];
[self.mainCollectionView reloadData];
That is, until I quit the app and restart.
If you're wondering, I'm using FTASync, and this only supports MR 2.1, which is why I haven't upgraded to the latest version.
Thanks!
Not sure about MR, nor why you would need it. If that framework can give you the main context, just call the native Core Data save.
[context save:nil];
Cracked it!!
I noticed that my other attributes were saving, just not this one (this one holds all the data that is presented in the UI) and this led me on another line of investigation.
So, it seems that I needed to be working with immutable dictionaries to store this data as explained here:
Core Data saving problem: can't update transformable attribute (NSArray)

How to customise save in Core Data?

I have defined a Core Data for my project and implemented an ENtity:attribute called isRealEntry.
#interface FTRecord : NSManagedObject
#property (nonatomic) NSTimeInterval lastUpdated;
#property (nonatomic) BOOL isRealEntry;
#end
Now when I save the context (NSManagedObjectContext *context;)
NSError *error = nil;
BOOL successful = [context save:&error];
I would like to save only those entities that have a true isRealEntry, otherwise the entry shall be ignored or undone.
How can I achieve this?
Update:
At first I found Martin's solution very promising. However I get a very nasty side effect when I save my data upon entering background:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[[FTRecordStore sharedStore] saveChanges];
}
When I resume the app, all the previous deleted records aren't gone for real but flagged to be deleted. The array still seems to have all of them (real or unreal in my case). The cells go completely nuts and show empty for all records.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FTRecord *record = [[[FTRecordStore sharedStore] getAllRecords] objectAtIndex:[indexPath row]];
FTRecordCellView *cell = [tableView dequeueReusableCellWithIdentifier:#"FTRecordCellView"];
[[cell notesLabel] setText:[record notes]];
return cell;
}
I am not sure how to solve this. My Store is a singleton. getAllRecords determines above the content for each cell. Hence I need to have the same value for getAllRecords as also in the tableView, or it would crash.
The other suggested solution with two sources one in memory and in db seems also not to be possible, how do I feed one TableView with two sources?
Update 2:
I had an embarassing oversight. Deleting the record from context is not enough. I also had to delete it from the array.
[allRecords removeObjectIdenticalTo:record];
Therefore I take it back. Martin's solution works perfect. However I am still curious to know if a UITableView can indeed be driven from two sources (db/memory) as suggested in teh other solution. Thanks
I've had to do something similar to this before and the way I approached it was to have a seperate managed object context for items that I was going to persist, and another for items that were staying in memory only.
I went about it by having a seperate persistent store cordinator as well as a separate managed object context that is in memory only, so when items are saved into it, they don't get persisted to the database with what you described as real items.
You can create an in memeory persistent store coordinator like this:
inMemoryPersistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
[inMemoryPersistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration: nil URL: nil options: nil error: &error];
If you want to change non real items into real items, you can copy them into the other managed object context which will persist the items to the database when it's saved.
The obvious issue here is that searching is done on a single managed object context so if you hoped to search through persisted and in memory objects, then you would need to do something more along the lines of what Arkadiusz suggested in his answer.
Saving a managed object contexts saves all changes make to that context. You cannot
exclude some objects from the save operation.
To undo all changes to the "unreal" objects, you could implement the willSave
method of the NSManagedObject subclass:
- (void)willSave
{
if (![self.isRealEntry boolValue]) {
if (self.isInserted) {
// Object was inserted, remove it again:
[self.managedObjectContext deleteObject:self];
} else if (self.isUpdated) {
// Object was modified, undo all changes:
[self.managedObjectContext refreshObject:self mergeChanges:NO];
}
}
}
(I never did this in a real project, but I built a small test app and
it seems to work.)
I don't believe there is a built in way to do this. I believe you would either have to delete them before saving or write cleanup code to find and delete them later.

Resources