How to implement DATABASE TRANSACTIONS in couchbase lite?

Hi,
I have integrated Couchbase Lite in my android application, now i want to use Database transactions in it.

SCENARIO 1:
I have documents of 2 types “Customer” and “Sync” .

  1. Whenever a customer is created the document is saved in database with type “Customer” [Operation 1]
  2. After that another document is saved with type “Sync” [Operation 2]

But if operation 2 failed whole transaction should rollback (both documents should not be saved).
SCENARIO 2:

  1. Update name of customer in a document of type “Customer”.
    2- The update should reflect in other documents as well. Let’s say In documents of type “Sync” and “Account”.

How i can implement these scenarios? As per my knowledge its called “MULTI DOCUMENT TRANSACTIONS” but i am unable to find any documentation or help to implement this in couchbase lite. Can somebody please help me out with this?

Couchbase Lite does not support transactions today. So scenario #1 with rollback will not be supported.
For scenario #2, you will be responsible for updating all related documents . You can use the inBatch API for efficiency for bulk operations but note that these operations are not handled in a transaction

1 Like

inBatch does actually run a transaction. If an exception occurs during the transaction, it will be aborted.

1 Like

Thank you for response.

As you said its transactional wherever a current database operation failed all operations post this operations will fail because of exception but the operations that were successful before exception occurrence will be rolled back or not?

It’s a transaction. When a transaction is aborted, all the changes are rolled back.

I tried executing 3 “Save Operations” in a single BATCH but it didn’t worked as a single transaction as a '“CouchbaseLiteException” occured on 2nd Operation but the BATCH did not aborted.

3rd operation was successful and 1st operation changes were not rolled back.

Breaking the loop when exception occurs avoided performing the operations following the "Operation that caused Exception" but sill the operations preceding it were not rolled back.

Below is test class written in JAVA for Android platform.

public class DatabaseManager {

    private Context context;
    Database database;
    File directory;

    public Database getDatabase() {
        return database;
    }

    public File getDirectory() {
        return directory;
    }

    public DatabaseManager(Context context) {
        try {
            CouchbaseLite.init(context);
            this.context = context;
            // Set Database configuration
            DatabaseConfiguration config = new DatabaseConfiguration();

            directory = context.getDir("MYDB", Context.MODE_PRIVATE);
            config.setDirectory(directory.toString());

            // Create / Open a database with specified name and configuration
            database = new Database("MYDB", config);

        } catch (CouchbaseLiteException e) {
            e.printStackTrace();
        }
    }

    public String executeTransaction() {
        final String[] response = {"transaction successfull"};

        try {
            String SAVE_OP_1 = "{\"name\":\"john\",\"age\":22,\"class\":\"mca\"}";
            String SAVE_OP_2 = "{\"name\":\"keth\",\"age\":25,\"class\":\"mca\"}";
            String SAVE_OP_3 = "{\"name\":\"skye\",\"age\":36,\"class\":\"mca\"}";

            Map<String, JSONObject> dataMap = new LinkedHashMap<>();
            try {
                dataMap.put("FirsrtDocId", new JSONObject(SAVE_OP_1));
                dataMap.put("_SecondDocId", new JSONObject(SAVE_OP_2));
                dataMap.put("ThirdDocId", new JSONObject(SAVE_OP_3));
            } catch (JSONException e) {
                e.printStackTrace();
            }
            database.inBatch(new Runnable() {
                @Override
                public void run() {
                    Iterator it = dataMap.entrySet().iterator();
                    while (it.hasNext()) {
                        Map.Entry pair = (Map.Entry) it.next();
                        HashMap documentMap = new Gson().fromJson(pair.getValue().toString(), HashMap.class);
                        MutableDocument mutableDocument = new MutableDocument(String.valueOf(pair.getKey()));
                        mutableDocument.setData(documentMap);
                        try {
                            database.save(mutableDocument);
                        } catch (CouchbaseLiteException e) {
                            response[0] = "CouchbaseLiteException while saving " + pair.getKey() + e.getMessage();
                            e.printStackTrace();
                        }
                    }

                }
            });
        } catch (CouchbaseLiteException e) {
            e.printStackTrace();
            response[0] = "CouchbaseLiteException in transaction \n" + e.getMessage();
        }
        return response[0];
    }
}

Below is CouchbaseLiteException on Operation#2 because the document id started with an underscore _

Below is Database that contains 2 documents after the execution of above BATCH.

Researching more found another thread on Github ( Couchbase Lite Issues) with discussion on same topic that says "Couchbase Lite doesn’t support rollback.

Also in Couchbase Lite documentation on Batch Operations Section couldn’t find any infromation on Rollback.

As said by @priya.rajagopal Couchbase Lite does not support transactions today. So scenario #1 with rollback will not be supported.

inBatch is for efficiency for bulk operations but it doesn’t run as a single transaction.

What is your opinion on this issue?

You didn’t show where the exception was thrown from. Your code catches exceptions from the Document.save method and keeps going, so if the exception was thrown from save, the transaction would not abort.

The transaction only aborts if an exception is thrown out of the function (the Callable) invoked by inBatch.

What do you need a transaction for, by the way?

1 Like

It’s not a supported feature, and there is no API for aborting a transaction. But inBatch is implemented as a transaction, and will abort as a side effect of an exception being thrown out of it.

1 Like

Exception was thrown on document. save method because document id started with an underscore.
I actually Wanted to use transaction to save multiple documents together so that if any issue arise no document should be saved and if no issue arise all should be saved (just like we achieve this in SQL by starting a transaction).
Also if a document is saved another document is updated based on it and if any issue arrise both operations should fail.
But seems like the scenarios i stated will not be achieved using inBatch here in Couchbase Lite because there may be a case when there are any issues on Document. save or update so this will fail only that specific operation not all operations that are being performed in inBatch.

It sounds like the Java implementation of inBatch commits the transaction even if an exception is thrown. Is that true, @blake.meike?

Sorry, I misunderstood…

Exception was thrown on document. save method

And as I explained, you caught the exception yourself, so it was not thrown out of your function (Runnable). That means inBatch did not think the operation failed, so it didn’t abort the transaction. In other words, CBL is behaving correctly.

If you want the transaction to be aborted, you have to let the exception propagate out of your function.

1 Like

Admittedly, this is difficult, since the Runnable does not permit throwing checked exceptions. The lambda passed to the inBatch method has to look something like this:

try {
   doStuffMightThrow()
   doMoreStuffMightThrow()
}
catch (SomeException | SomeOtherException e) {
   throw new RuntimeException("batch action failed", e);
}

… which is silly. As soon as we are allowed to make some changes to the API, I will fix this.

1 Like

Great question! It helped me catch some of my bugs. I should have tested instead of assuming. The code below works in CBL 2.x iOS. I had to check each database save and throw if there’s an error to abort the inBatch transaction. It’s also worth highlighting this part of the docs, inBatch uses transactions on the device. But Couchbase Mobile is a distributed system, and due to the way replication works, there’s no guarantee that Sync Gateway or other devices will receive your changes all at once.’ Handling the fact I may not see all changes at once made the Couchbase portion of my app at least 2x more complex. I’d love to see CBL use the Distributed ACID transactions just introduced in Server 6.5!

// starting with an empty test db...
NSError  * __autoreleasing error;
BOOL batchSuccess;
@try {
   batchSuccess = [self.db inBatch:&error usingBlock:^{
        NSError  * __autoreleasing bError;
       NSException *exp = [NSException exceptionWithName:@"batch failed" reason:@"batch failed" userInfo:nil];

        CBLMutableDocument *mDoc = [CBLMutableDocument documentWithID:@"t-1"];
        [mDoc setString:@"v1" forKey:@"k1"];
        if([self.db saveDocument:mDoc error:&bError] == NO) {
            NSLog(@"%s error saving doc", __PRETTY_FUNCTION__);
            @throw exp;
        }
        
        CBLMutableDocument *mDoc2 = [CBLMutableDocument documentWithID:@"_t-2"];
        [mDoc setString:@"v2" forKey:@"k2"];
        if([self.db saveDocument:mDoc2 error:&bError] == NO) {
            NSLog(@"%s error saving doc", __PRETTY_FUNCTION__);
            @throw exp;
        }
    }];
} @catch (NSException *exception) {
    NSLog(@"%s exception caught: %@", __PRETTY_FUNCTION__, exception.name);
}

if (!batchSuccess) {
    NSLog(@"%s batch error:%@", __PRETTY_FUNCTION__, [error localizedDescription]);
}
    
CBLQuery *query = [CBLQueryBuilder select:@[[CBLQuerySelectResult expression: [CBLQueryMeta id]]] from:[CBLQueryDataSource database:self.db]];
CBLQueryResultSet *results = [query execute:&error];
NSArray *resultsArray = results.allObjects;

NSLog(@"%s result count: %ld", __PRETTY_FUNCTION__, resultsArray.count);
XCTAssertEqual(resultsArray.count, 0, @"no docs should be saved if any document in batch fails to save");
1 Like

It can’t. You can’t have transactions in a distributed system that supports disconnection or offline mode. Just imagine what happens if some phone begins a transaction, and immediately loses connectivity. Now no one else can create a transaction until either that phone comes back online, or some kind of lock timeout occurs. For similar reasons, it would be impossible to start a transaction while offline.

1 Like

Throwing Runtime exception from functionality inside lambda aborted the transaction when any operation failed. That’s what i was looking for thanks :slight_smile:.

Yes exception needed to be thrown from document.save.