Android Native - Run multiple statements in a Room transaction

dimitrilc 1 Tallied Votes 452 Views Share

Introduction

When working with Room, you might have wondered how to run multiple statements in a single transaction. Running multiple statements in one transaction has two main benefits:

  1. Your statements can reuse the same connection.
  2. They can all fail together if something goes wrong.

Regarding the first benefit, it is not well documented whether Room keeps the connection alive or closes them after every statement. Developers normally do not have to worry about manually closing database connections when using Room. Upon close inspection of the RoomDatabase source code, it appears that RoomDatabase does have a property called mAutoCloser that is used to run transactions with, at least when it is not null.

public void beginTransaction() {
    assertNotMainThread();
    if (mAutoCloser == null) {
        internalBeginTransaction();
    } else {
        mAutoCloser.executeRefCountingFunction(db -> {
            internalBeginTransaction();
            return null;
        });
    }
}

From the AutoCloser source code, this class is described as:

AutoCloser is responsible for automatically opening (using delegateOpenHelper) and closing (on a timer started when there are no remaining references) a SupportSqliteDatabase.

Because we now know that AutoCloser exists, we are going to assume that Room opens and closes the database connection for every transaction (maybe not immediately, but with Handler#postDelayed()), and this opening and closing can be expensive. If the situation applies, we should wrap multiple statements into a single transaction.

There are two different ways, that I am aware of, to run multiple statements in a transaction, using the Dao or the convenient methods from the RoomDatabase class. In this tutorial, we will learn how to use both.

Goals

At the end of the tutorial, you would have learned:

  1. How to run multiple statements in a Room transaction.

Tools Required

Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.

Prerequisite Knowledge

  1. Intermediate Android.
  2. SQL.
  3. Basic Room database.
  4. Kotlin coroutines.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Add the dependencies below for Room into the Module build.gradle.

     def room_version = "2.4.2"
      implementation "androidx.room:room-runtime:$room_version"
      kapt "androidx.room:room-compiler:$room_version"
      implementation "androidx.room:room-ktx:$room_version"
      implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
  3. In the same file, add the kapt plugin under plugins

     id 'kotlin-kapt'
  4. Create a new Entity called Student using the code below.

     @Entity(tableName = "student")
     data class Student(
        @PrimaryKey(autoGenerate = true) val id: Long = 0,
        val name: String,
        val age: Int,
     )
  5. Create a new Dao for the Student entity using the code below.

     @Dao
     interface StudentDao {
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     abstract fun insert(student: Student)
    
     @Update(onConflict = OnConflictStrategy.REPLACE)
     abstract fun update(student: Student)
    
     @Delete
     abstract fun delete(student: Student)
     }
  6. Create a MyDatabase class using the code below.

     @Database(entities = [Student::class], version = 1)
     abstract class MyDatabase : RoomDatabase() {
        abstract fun studentDao(): StudentDao
     }

Running statements in a transaction using @Transaction

The first way that we are going to learn is with the annotation @Transaction. This annotation can be applied to a Dao method, which will run all of the contained statements in one single transaction.

Our StudentDao contains three different methods, @Insert, @Update, and @Delete, and we would like to run all three in a transaction. Follow the steps below to do this.

  1. Create a new concrete function called insertUpdateDelete() in StudentDao using the code below.

     suspend fun insertUpdateDelete(student: Student){
    
     }
  2. Call all three functions insert(), update(), and delete() like the code below.

     suspend fun insertUpdateDelete(student: Student){
        insert(student)
        update(student)
        delete(student)
     }
  3. Add the @Transaction annotation to insertUpdateDelete() like the code below.

     @Transaction
     suspend fun insertUpdateDelete(student: Student){
        insert(student)
        update(student)
        delete(student)
     }

And that is it. We can append the code below into MainActivity#onCreate() to see if it works. This code just creates an instance of MyDatabase and executes the @Transaction insertUpdateDelete() function.

    val db = Room.databaseBuilder(
       applicationContext,
       MyDatabase::class.java, "my-database"
    ).build()

    lifecycleScope.launch(Dispatchers.IO) {
       val student = Student(
           id = 1,
           name = "John",
           age = 6
       )
       db.studentDao().insertUpdateDelete(student)
    }

Running statements in a transaction using convenient methods

Another way to run multiple statements in Room would be to use the convenient methods below from the RoomDatabase class. Your options depend on whether you are using Java or Kotlin.

For Java, you have two options:

  1. runInTransaction (Callable<V> body)
  2. runInTransaction (Runnable body)

For Kotlin, in addition to the two Java methods above, you can also use the extension function below (requires androidx.room:room-ktx):

  1. withTransaction(block: suspend () -> R)

I cannot think of a use case where the Java methods are preferred over the Kotlin extension, so I will only showcase the Kotlin extension function. The only two important things that you need to know if you are suing the Kotlin withTransaction() are:

  1. The Dao functions called inside of the suspend block should be suspending functions themselves.
  2. withTransaction() must be used inside of a coroutine scope.

Follow the steps below to use withTransaction().

  1. Make the 3 Dao functions in StudentDao suspend.

     @Insert(onConflict = OnConflictStrategy.REPLACE)
     abstract suspend fun insert(student: Student)
    
     @Update(onConflict = OnConflictStrategy.REPLACE)
     abstract suspend fun update(student: Student)
    
     @Delete
     abstract suspend fun delete(student: Student)
  2. In onCreate(), comment out the insertUpdateDelete() call because we do not need it anymore.
    Create a reference called studentDao to improve readability a little bit.

     val studentDao = db.studentDao()
  3. Inside of the launch(), call withTransaction() with an empty body.

     db.withTransaction {
    
     }
  4. Add all 3 Dao functions to the withTransaction block.

     db.withTransaction {
        studentDao.insert(student)
        studentDao.update(student)
        studentDao.delete(student)
     }

And we are done. If we run the app, the student table should be empty because we inserted, updated, but also deleted the same entity.

Solution Code

MainActivity.kt

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val db = Room.databaseBuilder(
           applicationContext,
           MyDatabase::class.java, "my-database"
       ).build()

       lifecycleScope.launch(Dispatchers.IO) {
           val student = Student(
               id = 1,
               name = "John",
               age = 6
           )
           //db.studentDao().insertUpdateDelete(student)

           val studentDao = db.studentDao()

           db.withTransaction {
               studentDao.insert(student)
               studentDao.update(student)
               studentDao.delete(student)
           }
       }
   }
}

StudentDao.kt

@Dao
interface StudentDao {
   @Insert(onConflict = OnConflictStrategy.REPLACE)
   abstract suspend fun insert(student: Student)

   @Update(onConflict = OnConflictStrategy.REPLACE)
   abstract suspend fun update(student: Student)

   @Delete
   abstract suspend fun delete(student: Student)

   @Transaction
   suspend fun insertUpdateDelete(student: Student){
       insert(student)
       update(student)
       delete(student)
   }
}

Student.kt

@Entity(tableName = "student")
data class Student(
   @PrimaryKey(autoGenerate = true) val id: Long = 0,
   val name: String,
   val age: Int,
)

MyDatabase.kt

@Database(entities = [Student::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
   abstract fun studentDao(): StudentDao
}

Module build.gradle

plugins {
   id 'com.android.application'
   id 'org.jetbrains.kotlin.android'
   id 'kotlin-kapt'
}

android {
   compileSdk 32

   defaultConfig {
       applicationId "com.example.daniwebandroidroomtransaction"
       minSdk 21
       targetSdk 32
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   kotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {
   def room_version = "2.4.2"
   implementation "androidx.room:room-runtime:$room_version"
   kapt "androidx.room:room-compiler:$room_version"
   implementation "androidx.room:room-ktx:$room_version"
   implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'

   implementation 'androidx.core:core-ktx:1.7.0'
   implementation 'androidx.appcompat:appcompat:1.4.1'
   implementation 'com.google.android.material:material:1.5.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
   testImplementation 'junit:junit:4.13.2'
   androidTestImplementation 'androidx.test.ext:junit:1.1.3'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

Summary

We have learned how to use Room transactions in this tutorial. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidRoomTransaction

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.