Reputation: 1502
Saving a large amount of data at a time is very slow.
In my app there's a private-queue NSManagedObjectContext
as the parent that talks to the NSPersistentStoreCoordinator
directly to save data. A child main-queue context is consumed by a NSTreeController
for the UI(NSOutlineView
).
(My goal was to prevent any occurence of the beach ball. Currently I remedy the problem by only saving data when the app goes inactive. But since the data that are planed to be deleted are not deleted yet, they may still come up in a fetch result. That's another problem I'm trying to solve.)
The child main-queue context can only wait when fetching when the parent context is busy saving.
Core Data: parent context blocks child This perhaps is essentially the same question. I've noticed the answer says the setup is intrinsically wrong. Then I wonder is there a way to do the writing and reading at the same time with Core Data?
Correct implementation of parent/child NSManagedObjectContext A commonly asked question lacks answers with insights (sorry...).
What is the most efficient way to delete a large number (10.000+) objects in Core Data? If the objects can't be designated by a NSPredicate
, we still need to rely on the traditional delete()
(and maybe the Cascade delete rule) Also, it doesn't eliminate the blocking-the-UI issue.
I will update this question when I have more findings.
Upvotes: 2
Views: 705
Reputation: 3791
I'm guessing you're developing for OS X / macOS (NSTreeController
& NSOutlineView
). I've no experience with macOS - I develop for iOS - so you might need to take that into account when you're reading my response.
I've not yet made the switch to swift - my code is, perhaps obviously, Objective-C...
I'll start with how I prepare the Core Data stack.
I set up two public properties in the header file:
@property (nonatomic, strong) NSManagedObjectContext *mocPrivate;
@property (nonatomic, strong) NSManagedObjectContext *mocMain;
Although this is unnecessary, I also prefer to set up private properties for my Core Data objects, including, for example:
@property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;
Once I've pointed to my model URL, established my managed object model NSManagedObjectModel
, pointed to my store URL for my NSPersistentStore
and established my persistent store coordinator NSPersistentStoreCoordinator
(PSC), I set up my two managed object contexts (MOC).
Within the method to "build" my Core Data stack, after I've completed the code per the above paragraph, I then include the following...
if (!self.mocPrivate) {
self.mocPrivate = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[self.mocPrivate setPersistentStoreCoordinator:self.persistentStoreCoordinator];
} else {
// report to console the use of existing MOC
}
if (!self.mocMain) {
self.mocMain = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[self.mocMain setParentContext:self.mocPrivate];
} else {
// report to console the use of existing MOC
}
(I usually include a few NSLog
lines in this code to report to my console but I've excluded that here to keep the code clean.)
Note two important aspects to this code...
Why is this done? First let's highlight a couple of important points:
The private queue is asynchronous to the main queue. The User Interface (UI) operates on the main queue. The private queue operates on a separate thread "in the background" working to maintain context and coordinate data persistence with the PSC, perfectly managed by Core Data and iOS. The main queue operates on the main thread with the UI.
Written a different way...
In theory this should ensure your UI is never blocked.
But there is more to this solution. How we manage the "save" process is important...
I write a public method:
- (void)saveContextAndWait:(BOOL)wait;
I call this method from any class that needs to persist data. The code for this public method:
- (void)saveContextAndWait:(BOOL)wait {
// 1. First
if ([self.mocMain hasChanges]) {
// 2. Second
[self.mocMain performBlockAndWait:^{
NSError __autoreleasing *error;
BOOL success;
if (!(success = [self.mocMain save:&error])) {
// error handling
} else {
// report success to the console
}
}];
} else {
NSLog(@"%@ - %@ - CORE DATA - reports no changes to managedObjectContext MAIN_", NSStringFromClass(self.class), NSStringFromSelector(_cmd));
}
// 3. Third
void (^savePrivate) (void) = ^{
NSError __autoreleasing *error;
BOOL success;
if (!(success = [self.mocPrivate save:&error])) {
// error handling
} else {
// report success to the console
}
};
// 4. Fourth
if ([self.mocPrivate hasChanges]) {
// 5. Fifth
if (wait) {
[self.mocPrivate performBlockAndWait:savePrivate];
} else {
[self.mocPrivate performBlock:savePrivate];
}
} else {
NSLog(@"%@ - %@ - CORE DATA - reports no changes to managedObjectContext PRIVATE_", NSStringFromClass(self.class), NSStringFromSelector(_cmd));
}
}
So I'll work through this to explain what is happening.
I offer the developer the option to save and wait (block), and depending on the developer's use of the method saveContextAndWait:wait
, the private queue MOC "saves" using either:
performBlockAndWait
method (developer calls method with wait
= TRUE
or YES
); orperformBlock
method (developer calls method with wait
= FALSE
or NO
).First, the method checks whether there are any changes to the main queue MOC. Let's not do any work unless we have to!
Second, the method completes a (synchronous) call to performBlockAndWait
on the main queue MOC. This performs the call to save
method in a code block and waits for completion before allowing the code to continue. Remember this is for regular "saves" of small data sets. The (asynchronous) option to call performBlock
is not required here and in fact will derail the effectiveness of the method, as I experienced when I was learning to implement this in my code (failure to persist data due to the save
call on the main queue MOC attempting to complete after completion of the save
on the private queue MOC).
Third, we write a little block within a block that contains the code to save the private queue MOC.
Fourth, the method checks whether there are any changes to the private queue MOC. This may be unnecessary but it is harmless to include here.
Fifth, depending on the option the developer chooses to implement (wait
= YES
or NO
) the method calls either performBlockAndWait
or performBlock
on the block within a block (under third above).
In this last step, regardless of the implementation (wait
= YES
or NO
), the function of persisting data to disc, from the private queue MOC to the PSC, is abstracted to the private queue on an asynchronous thread to the main thread. In theory the "save to disc" via the PSC can take as long as it likes because it has nothing to do with the main thread. AND because the private queue MOC has all the data in memory, the main queue MOC is fully and automatically informed of the changes because it is the child of the private queue MOC.
If you import large volumes of data into app, something I am currently working on implementing, then it makes sense to import this data into the private queue MOC.
The private queue MOC does two things here:
Finally, I use NSFetchedResultsController
(FRC) to manage my data fetches, which are all completed against the main queue MOC. This maintains data hierarchy. As changes are made to the data sets in either context, the FRC updates the view.
This solution is simple! (Once I spent weeks wrangling my head around it and another few weeks refining my code.)
There is no requirement to monitor notifications for merges or other changes to MOC. Core Data and iOS handle everything in the background.
So if this doesn't work for you - let me know - I may have excluded or overlooked something as I wrote this code well over a year ago.
Upvotes: 3