Transactions and Room
By default, SQLite treats each individual SQL statement as an individual transaction. To the extent that Room winds up generating multiple SQL statements in response to our annotations, it is Room’s responsibility to wrap those statements in a suitable transaction.
However, sometimes, you have business logic that requires a transaction, for operations that require multiple DAO methods. For example, persisting an invoice might involve inserting an Invoice
and all of its InvoiceLineItem
objects, and that might require more than one DAO method to achieve.
Room offers two ways of setting up app-defined transactions: the @Transaction
annotation and some methods on RoomDatabase
.
Using @Transaction
Your DAO can have one or more methods that have the @Transaction
annotation. Whatever a @Transaction
-annotated method does is wrapped in a SQLite transaction. The transaction will be committed if the @Transaction
-annotated method does not throw an exception. If it does, the transaction will be rolled back.
There are two places to apply @Transaction
: custom methods on an abstract
DAO class, or on @Query
methods.
Custom Methods
Here, the idea is that your @Transaction
-annotated method would make multiple DAO calls to other methods (e.g., ones with @Insert
or @Query
annotations), so that the work performed in those other methods “succeed or fail as a whole”.
Given our fictitious Invoice
example, we might have something like this:
@Dao
public abstract class InvoiceStore {
@Insert
public abstract void _insert(Invoice invoice);
@Insert
public abstract void insert(List<InvoiceLineItem> lineItems);
@Transaction
public void insert(Invoice invoice) {
_insert(invoice);
insert(invoice.getLineItems());
}
}
Here, we still use an insert()
method to insert an Invoice
, but we use that to wrap two DAO calls to insert the Invoice
metadata and insert the InvoiceLineItem
objects.
Note that you will need to use an abstract
class, not an interface
, as an interface
cannot have arbitrary method implementations in them.
On @Query Methods
It may seem odd to have to specifically request a transaction on a @Query
-annotated method. After all, the default behavior of SQLite is to have each individual SQL statement be in its own transaction.
However, there are two scenarios called out in the documentation where @Transaction
would be a good idea. One is tied to @Relation
, which we will cover later in the book.
The other is tied to a little-known issue with Android’s SQLite support: things get weird when the result set of a query exceeds 1MB. In that case, using the regular Android SQLiteDatabase
API, the Cursor
that you get back does not contain the full result set. Instead, it contains a “window” of results, and if you position the Cursor
after that window, the query is re-executed to load in the next window. This can lead to inconsistencies, if the database is changed in between those two database requests to populate the window. Room, by default, will load the entire result set into your entities, quickly moving through the windows as needed, but there is still a chance that a database modification occurs while this is going on. Using @Transaction
would help ensure that this is not an issue, by having the entire query — including traversing the windows — occur inside a transaction.
Using RoomDatabase
Alternatively, RoomDatabase
offers the same beginTransaction()
, endTransaction()
, and setTransactionSuccessful()
methods that you see on SQLiteDatabase
, and so you use the same basic algorithm:
roomDb.beginTransaction();
try {
// bunch of DAO operations here
roomDb.setTransactionSuccessful();
}
finally {
roomDb.endTransaction();
}
The advantage to this approach is that you can put the transaction logic somewhere other than the DAO, if that would be more convenient or make more sense for your particular implementation. However, it is a bit more work.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.