Magical Record add object, different context error - ios

I'm using Magical Record in my app, and want to add the functionality for a user to add a 'Note', which is a child of 'entry'.
I added this code:
[MagicalRecord saveWithBlock: ^(NSManagedObjectContext *localContext) {
Note *newNote = [Note MR_createInContext: localContext];
newNote.content = noteContent;
newNote.name = #"User Note";
[self.entry addNotesObject: newNote];
}
completion: ^(BOOL success, NSError *error) {
if (error != nil)
{
// show alert
}
else if (success)
{
[[self tableView] reloadData];
}
}];
The error I keep getting on the last line is "Illegal attempt to establish a relationship 'entry' between objects in different contexts"
I tried setting the context of both 'entry' and 'newNote' to 'localContext', but I still get the same error.
What am I missing?

self.entry was created in different context, so you can't access it from this one.
Instead of:
[self.entry addNotesObject: newNote];
you should first find self.entry object in localContext:
[[self.entry MR_inContext:localContext] addNotesObject: newNote];
You can find an explanation of using MagicalRecord in a concurrent environment at Performing Core Data operations on Threads. Though it's quite short, so in my opinion it's worthwhile to read Core Data Programming Guide even though you don't use CD directly.

Related

NSPersistentContainer concurrency for saving to core data

I've read some blogs on this but I'm still confused on how to use NSPersistentContainer performBackgroundTask to create an entity and save it. After creating an instance by calling convenience method init(context moc: NSManagedObjectContext) in performBackgroundTask() { (moc) in } block if I check container.viewContext.hasChanges this returns false and says there's nothing to save, if I call save on moc (background MOC created for this block) I get errors like this:
fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x17466c500) for NSManagedObject (0x1702cd3c0) with objectID '0xd000000000100000 <x-coredata://3EE6E11B-1901-47B5-9931-3C95D6513974/Currency/p4>' with oldVersion = 1 and newVersion = 2 and old cached row = {id = 2; ... }fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x170664b80) for NSManagedObject (0x1742cb980) with objectID '0xd000000000100000 <x-coredata://3EE6E11B-1901-47B5-9931-3C95D6513974/Currency/p4>' with oldVersion = 1 and newVersion = 2 and old cached row = {id = 2; ...} and new database row = {id = 2; ...}"
)}
So I've failed to get the concurrency working and would really appreciate if someone could explain to me the correct way of using this feature on core data in iOS 10
TL:DR: Your problem is that you are writing using both the viewContext and with background contexts. You should only write to core-data in one synchronous way.
Full explanation: If an object is changed at the same time from two different contexts core-data doesn't know what to do. You can set a mergePolicy to set which change should win, but that really isn't a good solution, because you will lose data that way. The way that a lot of pros have been dealing with the problem for a long time was to have an operation queue to queue the writes so there is only one write going on at a time, and have another context on the main thread only for reads. This way you never get any merge conflicts. (see https://vimeo.com/89370886#t=4223s for a great explanation on this setup).
Making this setup with NSPersistentContainer is very easy. In your core-data manager create a NSOperationQueue
//obj-c
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
//swift
let persistentContainerQueue = OperationQueue()
persistentContainerQueue.maxConcurrentOperationCount = 1
And do all writing using this queue:
// obj c
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = self.persistentContainer.newBackgroundContext;
[context performBlockAndWait:^{
blockCopy(context);
[context save:NULL]; //Don't just pass NULL here, look at the error and log it to your analytics service
}];
}]];
}
//swift
func enqueue(block: #escaping (_ context: NSManagedObjectContext) -> Void) {
persistentContainerQueue.addOperation(){
let context: NSManagedObjectContext = self.persistentContainer.newBackgroundContext()
context.performAndWait{
block(context)
try? context.save() //Don't just use '?' here look at the error and log it to your analytics service
}
}
}
When you call enqueueCoreDataBlock the block is enqueued to ensures that there are no merge conflicts. But if you write to the viewContext that would defeat this setup. Likewise you should treat any other contexts that you create (with newBackgroundContext or with performBackgroundTask) as readonly because they will also be outside of the writing queue.
At first I thought that NSPersistentContainer's performBackgroundTask had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.

Changes on Background NSManagedObjectContext not visible on Main, using NSFetchedResultsController

This is a really bizarre issue, and I thought I understood Core Data.
I use a background context that has no parent. Hooked right into the Persistent Store Coordinator. I update objects on this background context then save it. I listen to the ContextDidSaveNotification and merge those changes into my main thread context. Those updated objects are not faults on the main thread as they are already used to populate table view cells. So I would expect those changes to actually merge. But they are not.
Without getting into the details of my data models, it suffices to say that an object has a property "downloadState". Once the parsing work is done on the background thread, downloadStateValue (an enum) gets set to "3", which corresponds to 'completed'.
I subscribe to the ContentWillSave notification now to inspect what's going on. I get this at the end of my parsing work:
2016-06-13 10:19:21.055 MyApp[29162:52855206] Going to save background context.
updated:{(
<QLUserPinnedCourse: 0x7fe195403c10> (entity: QLUserPinnedCourse; id: 0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11> ; data: {
course = "0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55>";
courseId = 2794;
/* other fields redacted */
}),
<QLCourse: 0x7fe1954cded0> (entity: QLCourse; id: 0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55> ; data: {
/* other fields redacted*/
contentDownloadState = 3;
courseId = 2794;
pinnedUserData = "0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11>";
})
The NSFetchedResultsController that is listenting to QLUserPinnedCourse objects gets the delegate calls, which triggers cell reloads in my tables.
The predicate is:
// Specify criteria for filtering which objects to fetch
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"pinned == %# && course.contentDownloadState IN %#",
#YES,
#[#(QLDownloadStateSucceeded), #(QLDownloadStateNotYetAttempted), #(QLDownloadStateFailed), #(QLDownloadStateIncomplete)]
];
Now when I get to the cell code, I have a QLUserPinnedCourse object to work with. I set a breakpoint in the debugger and get:
(lldb) po userCourse.course
<QLCourse: 0x7fe19568f740> (entity: QLCourse; id: 0xd000000000dc0008 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLCourse/p55> ; data: {
contentDownloadState = 1;
courseId = 2794;
pinnedUserData = "0xd0000000002c0002 <x-coredata://95821ADC-8A1F-4DAC-B20C-EDD8F8F413EA/QLUserPinnedCourse/p11>";
})
The question is, WHY is contentDownloadState not 3, but still 1 ?? I don't get it.
Shouldn't these changes have been merged??
Details as to my stack:
PSC -> Private Concurrent (saving context) -> Main Thread context
PSC -> Private Concurrent (import context)
ContextDidSave:
if the context was an import context, merge changes into both contexts above:
_contextSaveObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification* note)
{
NSManagedObjectContext *contextSaved = note.object;
NSManagedObjectContext *moc = weakself.mainQueueContext;
// basically, if this was a background worker thread
DDLogDebug(#"updatedObjects:%#", note.userInfo[NSUpdatedObjectsKey]);
if ([contextSaved.userInfo[CoreDataUserInfoKeyIsWorkerContext] boolValue])
{
[weakself.privateSavingContext performBlock:^(){
for (NSManagedObject *object in note.userInfo[NSUpdatedObjectsKey]) {
[[weakself.privateSavingContext objectWithID:[object objectID]] willAccessValueForKey:nil];
}
[weakself.privateSavingContext mergeChangesFromContextDidSaveNotification:note];
[moc performBlock:^(){
for (NSManagedObject *object in note.userInfo[NSUpdatedObjectsKey]) {
[[moc objectWithID:[object objectID]] willAccessValueForKey:nil];
}
[moc mergeChangesFromContextDidSaveNotification:note];
}];
}];
}
}];
Note that I'm asking the userCourse.course object for its attribute, although my FRC is interested in QLUserPinnedCourse objects. I thought because I specify a keypath in the predicate that relates to a QLCourse object, these changes are refreshed.
It's a quirk of Core Data. You actually need to re-fault objects in the main context which were updated by the save operation.
Here's an example in Swift:
mainContext.performBlock {
let updatedObjects : Set<NSManagedObject> = notification.userInfo![NSUpdatedObjectsKey] as! Set<NSManagedObject>
for obj in updatedObjects {
self.mainContext.objectWithID(obj.objectID).willAccessValueForKey(nil)
}
self.mainContext.mergeChangesFromContextDidSaveNotification(notification)
}
The main part is the call to willAccessValueForKey:nil, which causes the object to be marked as a fault. This will cause NSFetchedResultsControllers in the main context to fire.
So I found a solution but I can't tell you why it works.
The problem I suppose is that I would have a method 'start downloading content' and I would update the property contentDownloadState on the main thread context to 'incomplete/downloading', then proceed to get all the content.
All the rest of the work was done on a background thread context. When finished I updated that value with 'succeeded'. It wasn't merging that change. I have no idea why.
Once I decided to do everything on the worker context. i.e. change its value then save the context to disk, the changes, ALL the changes were propagating.
So in the end I solved it, but really don't understand the problem.

Saving Modified Data in CloudKit

I have been testing out CloudKit as i wish to release an app using it when the release of iOS8 occurs. It seems simple enough to save data using the code below:
CKRecordID * recordID = [[CKRecordID alloc] initWithRecordName:#"basicRecord"];
CKRecord * record = [[CKRecord alloc] initWithRecordType:#"basicRecordType" recordID:recordID];
[record setValue:#"defaultValue" forKey:#"defaultKey"];
CKDatabase *database = [[CKContainer defaultContainer] publicCloudDatabase];
[database saveRecord:record completionHandler:^(CKRecord *record, NSError *error) {
if (error) {
NSLog(#"Error: %#", error);
} else {
NSLog(#"Record Saved!");
}
}];
and I receive no errors from this. However, if i try to run the code again, maybe because i have changed the record value to
[record setValue:#"newValue" forKey:#"defaultKey"];
I receive an error which begs the question, how do i go about saving a modified piece of data. After all, this is a fundamental part of saving things to the cloud. The error is below and any help would be greatly appreciated, don't hesitate to ask for further information.
Error: <CKError 0x17024afb0: "Server Record Changed" (14/2017); "Error saving record <CKRecordID: 0x144684a80; basicRecord:(_defaultZone:__defaultOwner__)> to server: (null)"; uuid = 182C497F-966C-418A-9E6A-5563BA6CC6CD; container ID = "iCloud.com.yourcompany.CloudKit">
This error is probably because saveRecord: works only for new records or records that are newer than the version on the server:
This method saves the record only if it has never been saved before or if it is newer than the version on the server. You cannot use this method to overwrite newer versions of a record on the server. CKDatabase docs
The recommended approach to modify an existing record (or set of records) is to use a CKModifyRecordsOperation set with the desired savePolicy to deal with conflicts:
After modifying the fields of a record, use this type of operation object to save those changes to a database. (...)
When saving records, the value in the savePolicy property determines how to proceed when conflicts are detected on the server. CKModifyRecordsOperation docs
From the docs of CKRecord:
New records exist only in memory until you explicitly save them to iCloud.
When you set the new value [record setValue:#"newValue" forKey:#"defaultKey"]; you have already saved the record, making it invalid.
You can use CKModifyRecordsOperation and in most situations it might be preferrable but you don't have to. Just fetch your data using a fresh CKRecord, then feed that record into saveRecord: as described here.
After you save the record, fetch it so that the retured record will then have the RecordID that Cloudkit added
Then on that same fetched record, use setValue to change the data you want to change
Then you can use CFModifyRecordsOperation
In the example below, cachedCKRecordsServiceCenter contains the fetched records from cloudkit and those records have the CloudKit RecordID's in them......
//find this service center in the cached records
for (_,serviceCenter) in (theModel?.cachedCKRecordsServiceCenter.enumerated())! //is data for logged in Co ONLY with NO Co name attached
{
let name = serviceCenter["name"] as! String
returnValue = "Try Again"
if name == displayedRecordName
{
serviceCenter.setValue(displayedRecordName! + "_" + (theModel?.companyName)!, forKey: "name") //db values have Co name appended
serviceCenter.setValue(label2Text.text, forKey:"street1")
serviceCenter.setValue(label3Text.text, forKey:"street2")
serviceCenter.setValue(label4Text.text, forKey:"city")
serviceCenter.setValue(label5Text.text, forKey:"state")
serviceCenter.setValue(label6Text.text, forKey:"zip")
serviceCenter.setValue(label7Text.text, forKey:"phone")
serviceCenter.setValue(label8Text.text, forKey:"email")
serviceCenter.setValue(label9Text.text, forKey:"note")
let saveRecordsOperation = CKModifyRecordsOperation()
var ckRecordsArray = [CKRecord]()
// set values to ckRecordsArray
ckRecordsArray.append(serviceCenter)
saveRecordsOperation.recordsToSave = ckRecordsArray
saveRecordsOperation.savePolicy = .ifServerRecordUnchanged
appDelegate.locked = true
saveRecordsOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
if error != nil {
// Really important to handle this here
////////print("ERROR: Unable to update Driver Location: Error= \(error)")
self.returnValue = "ERROR: Unable to update Driver Location: ERROR = \(error)"
self.appDelegate.locked=false
}
else
{
////print("Successfully updated Service Center")
self.appDelegate.locked=false
self.returnValue = "Successfully updated Service Center"
self.appDelegate.locked=false
//reget the data into the cach
self.theModel?.fetchServiceCenterFromCloudKit1()
}
}
CKContainer.default().publicCloudDatabase.add(saveRecordsOperation)
}
}

How do I save thousands of objects to Parse.com? [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question appears to be off-topic because it lacks sufficient information to diagnose the problem. Describe your problem in more detail or include a minimal example in the question itself.
Closed 8 years ago.
Improve this question
In my iOS app that uses Parse, some users will need to save thousand of objects in one action. I've tried iterating through the array of data and creating/saving the objects one by one, but this results in the objects saving to my data browser pretty slowly. Each object only needs to contain a few strings, so I don't understand why its taking so long to save these objects.
Is there a faster way to save thousands of objects to Parse?
Edited:
I've always used [PFObject saveAllInBackground:array block:^(BOOL succeeded, NSError *error) {}]; but....another method I have just attempted semi-successfully, was to upload a Json string as a PFFile(no 128k limit), and then use cloud code to parse it and create the necessary PFObjects. I was able to get this to work with small quantities, but unfortunately the cloud code timed out when using a large quantity. I instead opted to utilize a background job to perform the parsing. This takes a considerable amount of time before the data is completely available, but can handle the large amount of data. The upload time itself was much quicker. When using 1000 objects with 3 strings each upload was roughly .8 seconds, vs 23 seconds doing a save all in background, 5000 objects 3 strings each was only 2.5 seconds upload time. In addition to the quicker time you also get progress updates. Depending on the use-case, utilizing this alternative may work best if immediate and quick upload is important, vs making the data immediately available.
IOS Code:
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i<5; i++) {
//subclass of PFObject
Employee *employee = [Employee object];
employee.firstName = #"FName";
employee.lastName = #"LName";
employee.employeeID = #"fid54";
[array addObject:[employee dictionaryWithValuesForKeys:employee.allKeys]];
}
//Seperate class only to store the PFFiles
PFObject *testObject = [PFObject objectWithClassName:#"fileTestSave"];
testObject[#"testFile"] = [PFFile fileWithData:[NSJSONSerialization dataWithJSONObject:array options:0 error:nil]];
NSLog(#"started");
//**notice I am only saving the test object with the NSData from the JSONString**
[testObject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (!error && succeeded) NSLog(#"succeeded");
else NSLog(#"error");
}];
Edited: Instead of saving in the beforeSave or afterSave Cloud Code which can cause timeout issues, the background job below can be run anytime. It grab all rows in the "fileTestSave" table, parses the JSON strings in those rows, and adds them to the "Person" table. Once completed it will rows from the table. All asynchronously!
var _ = require('underscore.js');
Parse.Cloud.job("userMigration", function(request, status)
{
// Set up to modify user data
Parse.Cloud.useMasterKey();
//Table called fileTestSave stores a PFFile called "testFile" which we will use an HTTPRequest to get the data. Is there a better way to get the data?
//This PFFile stores a json string which contains relavent data to add to the "Person" table
var testFileSave = Parse.Object.extend("fileTestSave");
var query = new Parse.Query(testFileSave);
query.find().then(function(results)
{
//Generate an array of promises
var promises = [];
_.each(results, function(testFileSaveInstance){
//add promise to array
promises.push(saveJsonPerson(testFileSaveInstance));
});
//only continue when all promises are complete
return Parse.Promise.when(promises);
}).then(function()
{
// Set the job's success status
console.log("Migration Completed NOW");
status.success("Migration completed");
}, function(error) {
// Set the job's error status
status.error("Uh oh, something went wrong.");
});
});
function saveJsonPerson(fileTestSave)
{
//Get the pffile testfile
var testFile = fileTestSave.get("testFile");
//get the fileURL from the PFFile to generate the http request
var fileURL = testFile["url"]();
//return the promise from the httpRequest
return Parse.Cloud.httpRequest({
method:"GET",
url: fileURL
}).then(function(httpResponse){
//return the promise from the parsing
return parsehttpResponse(httpResponse,fileTestSave);
},
function(error){
console.log("http response error");
}
);
}
function parsehttpResponse(httpResponse,fileTestSave)
{
var jsonArray = eval( '(' + httpResponse.text + ')' );
var saveArray =[];
//parse each person in the json string, and add them to the saveArray for bulk saving later.
for (i in jsonArray)
{
var personExtend = Parse.Object.extend("Person");
var person = new personExtend();
person.set("classDiscriminator",jsonArray[i]["classDiscriminator"]);
person.set("lastName",jsonArray[i]["lastName"]);
person.set("firstName",jsonArray[i]["firstName"]);
person.set("employeeID",jsonArray[i]["employeeID"]);
saveArray.push(person);
};
//return the promise from the saveAll(bulk save)
return Parse.Object.saveAll(
saveArray
).then(function(){
//return the promise from the destory
return fileTestSave.destroy(
).then(function(){
},function(error){
console.log("error destroying");
}
);
},function(error){
console.log("Error Saving");
}
);
}
Old Cloud Code that timed out as reference:
Parse.Cloud.afterSave("fileTestSave", function(request) {
//When accessing PFFiles you don't get the actual data, there may be an easier way, but I just utitlized an HTTPRequest to get the data, and then continued parsing.
var file = request.object.get("testFile");
var fileURL = file["url"]();
console.log("URL:"+fileURL);
Parse.Cloud.httpRequest({
method:"GET",
url: fileURL,
success: function(httpResponse)
{
var jsonArray = eval( '(' + httpResponse.text + ')' );
var saveArray =[];
for (i in jsonArray)
{
var personExtend = Parse.Object.extend("Person");
var person = new personExtend();
//May be a better way to parse JSON by using each key automatically, but I'm still new to JS, and Parse so I set each individually.
person.set("classDiscriminator",array[i]["classDiscriminator"]);
person.set("lastName",array[i]["lastName"]);
person.set("firstName",array[i]["firstName"]);
person.set("employeeID",array[i]["employeeID"]);
saveArray.push(person);
};
Parse.Object.saveAll(saveArray,
{
success: function(list) {
// All the objects were saved.
},
error: function(error) {
// An error occurred while saving one of the objects.
},
});
},
error: function(httpResponse) {
console.log("http response error");
}
});
});
Another method for uploading thousands of objects in the background, again this takes some time but can be sized to avoid timing out, as arrays are saved in chunks recursively. I've had no problem saving 10k+ items. Implemented as a category, just enter how many objects at a time you want save at a time, it will save them in the background serially, and recursively until all objects are saved, it also features progress updating via a separate block.
// PFObject+addOns.h
#import <Parse/Parse.h>
#interface PFObject (addOns)
+(void)saveAllInBackground:(NSArray *)array chunkSize:(int)chunkSize block:(PFBooleanResultBlock)block progressBlock:(PFProgressBlock)progressBlock;
#end
#import "PFObject+addOns.h"
#interface PFObject (addOns_internal)
+(void)saveAllInBackground:(NSArray *)array chunkSize:(int)chunkSize block:(PFBooleanResultBlock)block trigger:(void(^)())trigger;
#end
#implementation PFObject (addOns)
+(void)saveAllInBackground:(NSArray *)array chunkSize:(int)chunkSize block:(PFBooleanResultBlock)block progressBlock:(PFProgressBlock)progressBlock
{
unsigned long numberOfCyclesRequired = array.count/chunkSize;
__block unsigned long count = 0;
[PFObject saveAllInBackground:array chunkSize:chunkSize block:block trigger:^() {
count++;
progressBlock((int)(100.0*count/numberOfCyclesRequired));
}];
}
+(void)saveAllInBackground:(NSArray *)array chunkSize:(int)chunkSize block:(PFBooleanResultBlock)block trigger:(void(^)())trigger
{
NSRange range = NSMakeRange(0, array.count <= chunkSize ? array.count:chunkSize);
NSArray *saveArray = [array subarrayWithRange:range];
NSArray *nextArray = nil;
if (range.length<array.count) nextArray = [array subarrayWithRange:NSMakeRange(range.length, array.count-range.length)];
[PFObject saveAllInBackground:saveArray block:^(BOOL succeeded, NSError *error) {
if(!error && succeeded && nextArray){
trigger(true);
[PFObject saveAllInBackground:nextArray chunkSize:chunkSize block:block trigger:trigger];
}
else
{
trigger(true);
block(succeeded,error);
}
}];
}
#end
I think you should be able to do this with sending the save process in quantities of five into the background, so to speak fork it, "thread" it, as apple would refer to it.
here is the link to apples ios threading guides.
I have not used it yet, but I will soon need it as well, as I'm working on a massive database app.
here's the link
https://developer.apple.com/library/mac/Documentation/Cocoa/Conceptual/Multithreading/AboutThreads/AboutThreads.html
If you have an array of objects, you can use saveAllInBackgroundWithBlock. This method takes an array of PFObjects as its argument:
https://parse.com/docs/ios/api/Classes/PFObject.html#//api/name/saveAllInBackground:block:
For the faster processing, you can use the parse cloud code, whihc is simply a javascript. You can create a function which takes the array of the data as argument and then in function, you can save the objects.
Parse cloud code has better processing speed than the native one.
For its use, you can refer :
https://parse.com/docs/cloud_code_guide
https://parse.com/docs/js_guide

MagicalRecord: multiple databases

I have an app that uses MagicalRecord, and I'm pre-populating the database with a large amount of data that is used for reference. Within that same data model, I have user-definable information pertaining to things the user may do in the app.
The app was rejected because the pre-populated data is supposed to be marked as "do not backup". So, I'd like to have that data in a separate datastore so that I can keep the user data as backupable.
Is there a way to have two separate datastores using MagicalRecord?
I think it's possible, but not too easy though.
As you know, to work with more than one database, you should bring some changes to your PersistentStoreCoordinator, so it will have two PersistentStores. After this, you Core Data stack would look like this:
The other way is two make two separate PersistentStoreCoordinators, each carrying one store.
In Magical Record, there are several class methods for adding stores in
NSPersistentStoreCoordinator+MagicalRecord.h.
(NSPersistentStore *) MR_addInMemoryStore;
(NSPersistentStore *) MR_addAutoMigratingSqliteStoreNamed:(NSString *) storeFileName;
(NSPersistentStore *) MR_addSqliteStoreNamed:(id)storeFileName withOptions:(__autoreleasing NSDictionary *)options;
I think, that this is the place where you could do the thing you want.
Also i should mention, that the whole proccess of setting up the stack goes in MagicalRecord+Setup.h
+ (void) setupCoreDataStackWithStoreNamed:(NSString *)storeName
So you can add your Stores and Coordinators there.
I've never managed it by myself, that was just a brief investigation of a possible solution.
I was able to solve this issue using configurations. Since Magical Record always sends null for the configuration parameter, I broke apart setupCoreDataStackWithAutoMigratingSqliteStoreNamed and replaced it with a method that supports multiple configurations.
Because Magical Record does a good job of handling auto migrations, I first call setupCoreDataStackWithAutoMigratingSqliteStoreNamed, followed by cleanup, and then I supply my replacement code.
I have one object model with my seed data objects assigned the the "Seed" configuration and user objects assigned to the "User" configuration. Magical Record has already been initialized so it could auto migrate if necessary.
+(void) RB_setupMultipleStores:(NSString *) seedStoreName userStore:(NSString *) userStoreName
/* change persistent store to one with multiple configurations. Assumes Magical Record is initialized. */
{
NSError * error= nil;
[MagicalRecord cleanUp];
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 = [NSPersistentStore MR_urlForStoreName:[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];
[NSManagedObjectContext MR_initializeDefaultContextWithCoordinator:coordinator];
}
Also, MR 3.0 has concurrent stacks which may solve the problem once it is done.
Keeping data for different Core Data entities in different store files is well supported and fairly straightforward. However, MagicalRecrd doesn't provide any convenience methods for setting up your Core Data stack in this way. You simply have to allocate your stack manually, and tell MagicalRecord to use the NSPersistentStoreCoordinator you create. Here's how I did it in swift:
import Foundation
import CoreData
import MagicalRecord
class CoreDataSetup {
static func setupAutoMigratingStack(withContentConfigurationName contentConfigurationName: String, userConfirgurationNameName: String) {
MagicalRecord.cleanUp()
let managedObjectModel = NSManagedObjectModel.MR_defaultManagedObjectModel()
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel!)
let contentURL = NSPersistentStore.MR_urlForStoreName(contentConfigurationName + ".sqlite")
let userURL = NSPersistentStore.MR_urlForStoreName(userConfirgurationNameName + ".sqlite")
let options = [
NSMigratePersistentStoresAutomaticallyOption : true,
NSInferMappingModelAutomaticallyOption: true,
NSSQLitePragmasOption: ["journal_mode": "DELETE"]
]
do {
try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: contentConfigurationName, URL: contentURL, options: options)
try persistentStoreCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: userConfirgurationNameName, URL: userURL, options: options)
NSPersistentStoreCoordinator.MR_setDefaultStoreCoordinator(persistentStoreCoordinator)
NSManagedObjectContext.MR_initializeDefaultContextWithCoordinator(persistentStoreCoordinator)
} catch {
print("Error adding persistent store to coordinator: \(error) ")
}
}
}
Note that in my code I'm referring to your concept of the "seed" store as "content" and the user-definable store as "user".
To accomplish the second aspect of your question, configuring the content store to not be backed up, you simply have to play around with the URLs where you store each store, placing the content store in a non-backed up temporary directory, and copying it to that location up launch from your app bundle if it doesn't exist.

Resources