I'm using Realm in a recent project of mine — and I'm struggling to find an elegant solution to a problem I have. In order to keep the UI responsive, I try to act on most operations instantly. The whole UI is driven off of RLMResults, so in order to act on something, I need to modify objects in Realm.
The thing is, these seemingly instant changes (think favoriting in a Twitter app) can fail, and I have to rollback the changes I've made if this happens to be the case.
I'm using ReactiveCocoa throughout the app, so continuing explanation using the favoriting example, I'm guessing I need something like the following for this:
- (RACSignal *)favoriteTweet:(Tweet *)tweet {
RACSignal *favoriteOnDatabase = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> *subscriber) {
[self.realm beginWriteTransaction];
tweet.favorited = YES;
[self.realm commitWriteTransaction];
[subscriber sendNext: tweet];
[subscriber sendCompleted];
return nil;
}];
RACSignal *syncChangesToAPI = [[[self.apiClient favoriteTweet:tweet] ignoreValues]
onError:^(NSError *error) {
[self.realm beginWriteTransaction];
// Do rollback here. How?
[self.realm commitWriteTransaction];
}]
catch:^RACSignal *(NSError *error) {
return [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) {
[subscriber sendNext: tweet]; // Any changes have already been rolled back at this point.
[subscriber sendError: error];
return nil;
}];
}];
return [favoriteOnDatabase concat: syncChangesToAPI];
}
The sent values by this signal in case of an error will be:
Favorited tweet - [Error in API request] -> Unfavorited tweet -> Error
Realm has rollback support in transactions AFAIK, doing exactly what I want, but in my case I can't use them since I have to commit the write transaction I have in the favoriteOnDatabase
signal before sending the favorited tweet in case the subscriber to this signal decides to do any Realm involving work in the subscriber block — if I send the value before closing off the transaction there is a possibility of creating nested transactions.
I've written a dumb method trying to rollback things manually, but it feels very fragile as it fails in cases such as a deletion of an object — all relations to that objects are gone even if I manage to replace it in database with this. (Since technically it's a different instance?)
/*
Important:
- This method only works as intended with changes on the main properties on the object given. (i.e., the object is copied `shallowly` for rollback purposes)
- This method breaks any existing explicit relations to the object in rollback if the changeBlock deletes an object.
- [DataManager autoRollbackSyncSignalWithObject:apiSignal:changeBlock] is for changes in persisted objects that needs instant UI response instead of showing a loading indicator while syncing the change with the API.
This signal sends values in following order:
- An updated object with the requested changes made. (changeBlock(object))
...after this value send, the network request to sync the change is made:
- If the operation is successful, it does not send any new values and completes the signal.
- If the operation is unsuccessful, it rolls back the changes and sends the original value and the error object.
*/
- (RACSignal *)autoRollbackSyncSignalWithObject:(RLMObject *)object
apiSignal:(RACSignal *)apiSignal
changeBlock:(DataManagerAutoRollbackSyncSignalChangeBlock)changeBlock {
RLMObject *originalObject = [object two_shallowCopy];
__block RLMObject *modifiedObject = object;
RACSignal *localChange = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self.realm beginWriteTransaction];
changeBlock(modifiedObject);
[self.realm commitWriteTransaction];
[subscriber sendNext:modifiedObject];
[subscriber sendCompleted];
return nil;
}];
RACSignal *syncOperation = [[[apiSignal ignoreValues] doError:^(NSError *error) {
[self.realm beginWriteTransaction];
/*
Rollback.
*/
if (object.invalidated) { // If the object was deleted in the change block
modifiedObject = originalObject;
[self.realm addObject:modifiedObject];
} else if (!originalObject && modifiedObject) { // If an object was created in the change block
[self.realm deleteObject:modifiedObject];
} else { // If the object was modified in the change block
[modifiedObject two_mergePropertiesFromObject:originalObject];
}
[self.realm commitWriteTransaction];
}] catch:^RACSignal *(NSError *error) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:modifiedObject];
[subscriber sendError:error];
return nil;
}];
}];
return [localChange concat:syncOperation];
}
What's the best way of solving this problem? Am I missing something in Realm that will help me do this?
Thank you!