4

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!

Mert Dümenci
  • 493
  • 1
  • 5
  • 18
  • Might `-[RLMRealm cancelWriteTransaction]` be what you're looking for? – segiddins May 13 '15 at 17:16
  • Ideally cancelling the transaction would be perfect for me, since it rolls back everything done in the transaction automatically — but in this case, I have to commit the transaction before sending the first modified value since I need to update my UI. – Mert Dümenci May 14 '15 at 07:09
  • Using the favoriting example again: User favorites a tweet, I favorite the tweet in Realm and commit the transaction. UI updates off of the database. Now I make a network request, syncing the favorite with the API. If the API fails, I have to rollback the favorite. In the favorite example — which is just a BOOL, this seems pretty easy to do manually, but I'd much prefer a -[RLMRealm cancelWriteTransaction] like way of doing this where every change (implicit and explicit. I can't fix broken relationships etc manually) is undone. – Mert Dümenci May 14 '15 at 07:09
  • 2
    User-friendliness side of the problem: Is it reasonable to roll back changes in database? Shouldn't you rather repeat the request if it failed? As a user, when I favourited a tweet, I would expect that it stays favourited (in my app, and will be synced with server eventually) - regardless of network errors. – Jakub Vano May 14 '15 at 08:14
  • The request might fail for reasons other than network errors — but yes, I see what you mean. – Mert Dümenci May 14 '15 at 08:29
  • Mert, in this case you're probably best off making a new transaction. – segiddins May 14 '15 at 16:28
  • Where can I do that? – Mert Dümenci May 14 '15 at 20:03
  • In your network call, if you receive error, you just roll back(set your property back to what it was) the change that you made and commit it. – pteofil May 15 '15 at 07:01

0 Answers0