64

Room DB: Advanced Data Persistence [FREE]

 5 years ago
source link: https://www.tuicool.com/articles/hit/Fnqm63e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Using the Room persistence library from Google allows you to add relational persistence to you app without the need to write a lot of boilerplate code. But once you have it in your app, you might need to do things like map relationships to other records, update your data schema and optimize your queries.

This tutorial introduces you to more advanced concepts when using Android’s Room database. If you haven’t gone through the Data Persistence With Room tutorial, you should head over there first in order to become familiar with the basic use of Room .

In this tutorial, you will add features to the List Master app, while learning about the following:

  • Migrations for making changes to an existing data store.
  • Foreign Keys to map relationships between entities.
  • Indexes to make your queries faster.

Time to get started!

Note : This tutorial assumes that you have some experience developing Android apps. A few points to keep in mind:

  • You use the Android RecyclerView to display lists. If you’ve never used them, or you need a refresher, the Android RecyclerView Tutorial with Kotlin is a great place to start.
  • This tutorial utilizes Data Binding and Binding Adapters . Again, if you have never used these, or you need a refresher, you should take a look at the data binding documentation from the Android project pages, or check out ourMVVM on Android course.
  • The code snippets in this tutorial do not include the needed import statements. Use the key combination option+return on Mac — or Alt+Enter on PC — to resolve any missing dependencies as you work through your project.

Getting Started

Start by downloading the materials for this tutorial using the Downloand amterials button at the top or bottom of this tutorial. Unzip the file and start Android Studio 3.2.1 or later.

In the Welcome to Android Studio dialog, select Import project (Eclipse ADT, Gradle, etc.) :

AS321.png

Choose the ListMaster directory of the starter project and click Open :

import.png

If you see a message to update the project’s Gradle plugin, since you’re using a later version of Android Studio, choose Update .

Check out the project for the List Master app, and you will see two packages for list categories and list items.

Build and run the app, and your app will allow you to click the + button. Add a Category Name and see it in a list:

Room-Basic-Wiring.png

Creating Migrations

While having categories is a good start, your categories will be a lot more useful if each one has a list of items. The only problem is that you’ve already released a version of the app. When you run your Room -enabled app the first time, it generates the database and schema with its tables and attributes, and it saves them to the app storage. Unfortunately, as is the case with most ORM s, that table generation mechanism doesn’t have the ability to update your current database to a new structure.

One way to deal with the situation could be to delete your old database, including its data, and have Room generate a new database. Unfortunately, your users have begun to save category data in the app and may not be happy if they have to re-enter their list of categories . You can use a migration to update the data structure while preserving the users’ data.

Just like a bird migrating to warmer climates in the winter, your database can migrate to a better place for your app as you add, delete and refine your data structure.

Before you start to create migrations, it’s important to have an exported version of the database schema before the change. This allows you to test the migration and to ensure that is works correctly. To enable that in a fresh project, it requires a change to your app level build.gradle file. Your starter project already has this.

In your project, open the build.gradle file with the (Module: app) annotation:

Screen-Shot-2018-06-28-at-9.21.35-PM-650x110.png

You will see a section that looks like this:

android {
  ...
  defaultConfig {
      ...
      javaCompileOptions {
          annotationProcessorOptions {
              arguments = ["room.schemaLocation":
                           "$projectDir/schemas".toString()]
          }
      }
  }
}

Next, compile the app by selecting the Build menu option, followed by Reduild Project . A version of the current schema will now be stored in the app/schemas/com.raywenderlich.listmater.AppDatabase folder of your project. In the Android Studio Project view, it shows up under your assets package:

Screen-Shot-2018-03-25-at-2.45.47-PM-650x164.png

Now that you have the first version of the database saved, it’s time to set up your new list item and migration. The ListCategory object is going to have a one-to-many relationship to your ListItem objects. When you’re done the relationship will look like this:

room_db_category_to_list_item-650x268.png

This means each ListCategory will have multiple ListItem objects and each ListItem will only be associated with one ListCategory object. ListItem will have the following fields:

  • itemDescription : Contents of the list item.
  • itemPriority : A priority for the item to assist in organizing the items.
  • listCategoryId : A reference to the unique id of the category that the item is associated with for the relationship.
  • id : The unique ID for the record in the database.

When you open up the ListItem object under the listitem package, you will see a data class with all of these fields.

Replace it with the following:

@Entity(
    tableName = "list_items",
    foreignKeys = [ForeignKey(
        entity = ListCategory::class,
        parentColumns = ["id"],
        childColumns = ["list_category_id"],
        onDelete = CASCADE)])
data class ListItem(
  @ColumnInfo(name = "item_description") var itemDescription: String,
  @ColumnInfo(name = "item_priority") var itemPriority: Int,
  @ColumnInfo(name = "list_category_id") var listCategoryId: Long,
  @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) var id: Long = 0)

Most of the new annotations are similar to what are in your ListCategory but, in the @Entity annotation, you will notice that you’ve added a foreignKeys argument. A foreignKey is not a key to some secret place in another country, but rather an object that takes the following parameters to establish your one-to-many relationship:

  • entity : The entity that contains the foreign key.
  • parentColumns : The column(s) of the parent entity object that contains the key.
  • childColumns : The column(s) of the current entity, which is the child, that specified the parent key.
  • onDelete : You are setting this to CASCADE meaning that if a parent category is deleted, all of the children will be as well. It’s similar to going back in time and changing events so that the parent was never born and, as a result, the children would never be born because of the change in the space-time continuum. :]

Now, you are going to need to create a Dao for the ListItem entity. To do that, right-click on the listitem package. Select New ▸ Kotlin File/Class . Next, name it ListItemDao and press OK . Then paste in the following:

@Dao
interface ListItemDao {

  @Query("SELECT * FROM list_items")
  fun getAll(): LiveData<List<ListItem>>

  @Query("SELECT * FROM list_items WHERE list_category_id = :listCategoryId")
  fun getAllByListCategoryId(listCategoryId: Long): LiveData<List<ListItem>>

  @Insert
  fun insertAll(vararg listItems: ListItem)
}

The getAll() and insertAll() queries are performed the same way that they are for the ListCategory object in the Data Persistence with Room tutorial, with the addition of a LiveData object as the return value.

Note : If you’re new to the Android Architecture Components , you might be wondering that this LiveData object is. The short answer: It is an alternative to performing multiple query tasks in a background thread . You’ll see it in action here but, for a deeper dive, you can see the tutorial Android Architecture Components: Getting Started .

For the getAllByListCategory() query, there is a parameter named listCategoryId and a reference to it in the SQL statement with a : appended to the front of it. You are using this to pass a parameter to the SQL command in the Dao method. You add the parameter you want to pass to the function definition and then reference it in the SQL by appending : to it.

Now, it’s time to flap your wings by creating your migration. To do that, add a migrations package by right-clicking on your com.raywenderlich.listmaster package, selecting New ▸ Package . Next, enter migrations for the package name, and then press OK . Then, right-click on your migrations package, select New ▸ Kotlin File/Class . Give it a name of Migration1To2 and press OK . When the file opens, paste in the following:

@VisibleForTesting
class Migration1To2 : Migration(1, 2) {

  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("CREATE TABLE IF NOT EXISTS list_items" +
        "('item_description' TEXT NOT NULL, 'item_priority' INTEGER NOT NULL," +
        "'list_category_id' INTEGER NOT NULL, 'id' INTEGER NOT NULL, PRIMARY KEY(id)," +
        "FOREIGN KEY('list_category_id') REFERENCES list_categories('id') ON DELETE CASCADE)")
  }
}

This migration does two things:

  1. It extends the Migration class passing in the version of the database you are migrating from, 1 , and the version of the database you are migrating to, 2 .
  2. It overrides the migrate method and executes an SQL command to create the table that belongs with your ListItem class.

Note: You may have noticed a @VisibleForTesting annotation. As the name implies, this tells the compiler to make this component visible to your tests.

Finally, open the AppDatabase file and update the class to be the following:

//1
@Database(entities = [ListCategory::class, ListItem::class], version = 2)
abstract class AppDatabase : RoomDatabase() {

  abstract fun listCategoryDao(): ListCategoryDao
  //2
  abstract fun listItemDao(): ListItemDao

  companion object {
    //3
    @VisibleForTesting
    val MIGRATION_1_TO_2 = Migration1To2()
  }
}

You updated AppDatabase by adding the following features:

ListItem::class
ListItemDao

Testing Your Migrations

Now that you have the code needed to update your database, you’ll want to test your migration. To do that, you could add some code and Log statements to your activity. But a better way is to write an Espresso test.

Creating Your Espresso Test

To create an Espresso test, right-click on the (androidTest) version of your listmaster package. Select New ▸ Kotlin File/Class . Name it ListItemMigrationTest and press OK :

room_db_android_test.png

When the file opens, add in the following:

@RunWith(AndroidJUnit4::class)
class ListItemMigrationTest {

  private val TEST_DB_NAME = "migration_test"

  private lateinit var database: SupportSQLiteDatabase

  //1
  @Rule
  @JvmField
  val migrationTestHelperRule = MigrationTestHelper(
      InstrumentationRegistry.getInstrumentation(),
      "com.raywenderlich.listmaster.AppDatabase",
      FrameworkSQLiteOpenHelperFactory())

  //2
  @Before
  fun setup(){
    database = migrationTestHelperRule.createDatabase(TEST_DB_NAME, 1)
    database.execSQL("INSERT INTO list_categories (id, category_name) VALUES" +
        " (1, 'Purr Programming Supplies'), (2, 'Canine Coding Supplies')")
  }

  //3
  @After
  fun teardown(){
    database.execSQL("DROP TABLE IF EXISTS list_categories")
    database.execSQL("DROP TABLE IF EXISTS list_items")
    database.close()
  }
}

The code in the test class has the following capabilities:

  1. Creates a rule that initializes an Espresso test with the AppDatabase .
  2. Creates a version 1 instance of the database with data and runs the migration before every test.
  3. Removes all tables from the database after each test.

In the test setup, SQL statements are used to insert test data into version 1 of the database. This is because the new DAOs for version 2 are not available until all migrations have been executed.

As part of testing your migration, you need to get a version of the database that has already been migrated. To make your test more readable, paste the following helper method into your ListItemMigrationTest class.

private fun getMigratedRoomDatabase(): AppDatabase {
  //1
  val appDatabase = Room.databaseBuilder(
      InstrumentationRegistry.getTargetContext(),
      AppDatabase::class.java, TEST_DB_NAME)
      //2
      .addMigrations(AppDatabase.MIGRATION_1_TO_2)
      //3
      .build()
  //4
  migrationTestHelperRule.closeWhenFinished(appDatabase)
  return appDatabase
}

Breaking down the parts of this method:

  1. Call the Room database builder, passing in the name of your test database.
  2. Add your migration to the builder.
  3. Build the database.
  4. Tell the test rule to close the database when finished.

LiveData Espresso Testing

When you are testing code that runs in another thread, a common problem is understanding how to get your test to wait for the threads to finish before doing an assert on the result.

In the case of LiveData , a query normally runs on a background thread , and you attach an observer to process the retrieved values. To get around this in tests, the projects includes a small Kotlin extension called blockingObserve in TestExtensions.kt . This file is under the root listmaster package in the (androidTest) section of the project.

Open it up and you will see the following:

fun <T> LiveData<T>.blockingObserve(): T? {
  var value: T? = null
  val latch = CountDownLatch(1)
  val innerObserver = Observer<T> {
    value = it
    latch.countDown()
  }
  observeForever(innerObserver)
  latch.await(2, TimeUnit.SECONDS)
  return value
}

It adds an observer to the LiveData object and blocks until a value is returned so that your test does not finish executing before the values are returned from the database.

Note : If this is your first time working with extensions , the Kotlin documentation is a great place to start for understanding how they work.

Now that you’ve built up the scaffolding of your test, it’s time to create a test to validate that the migration works. Paste the following method into ListItemMigrationTest :

@Test
fun migrating_from_1_to_2_retains_version_1_data() {
  val listCategories =
      getMigratedRoomDatabase().listCategoryDao().getAll().blockingObserve()
  assertEquals(2, listCategories!!.size)
  assertEquals("Purr Programming Supplies",
      listCategories.first().categoryName)
  assertEquals(1, listCategories.first().id)
  assertEquals("Canine Coding Supplies",
      listCategories.last().categoryName)
  assertEquals(2, listCategories.last().id)
}

Reading the assertEquals statements, you will see verifications for the following things in the list_categories table using its DAO :

  • There are total of two records.
  • The first record is Purr Programming Supplies with an ID of 1.
  • The second record is Canine Coding Supplies with an ID of 2.

Now, run your test by right-clicking on your ListItemMigrationTest file and clicking Run :

room_db_pt_2_run_tests-423x500.png

You’ll need to select a device or emulator to run the Espresso tests on.

Verify that the result is “green” (passing):

room_db_pt2_passing_tests-650x152.png

Next, look in your assets directory and you will see a schema file for version 2 of your database called 2.json :

room_db_pt_2_assets_version_2-650x200.png

Note : You should version these files in your project so that you can test your migrations as you increase the versions.

Now that you are in the testing groove, you are going to test inserting a record into the new table while referencing an existing category. To do this, paste the following method into your ListItemMigrationTest class:

@Test
fun inserting_a_record_into_list_items_after_migrating_from_1_to_2_succeeds() {
  val listCategories =
      getMigratedRoomDatabase().listCategoryDao().getAll().blockingObserve()
  // insert a record in the new table
  val listItemDao = getMigratedRoomDatabase().listItemDao()
  val purrProgrammingListItem = ListItem("desk cushion", 1,
      listCategories!!.first().id)
  listItemDao.insertAll(purrProgrammingListItem)

  // validate that a record can be added to the new table
  val purrProgrammingList = listItemDao.getAll().blockingObserve()
  assertEquals(1, purrProgrammingList!!.size)
  val firstPurrProgrammingItem = purrProgrammingList.first()
  assertEquals("desk cushion", firstPurrProgrammingItem.itemDescription)
  assertEquals(1, firstPurrProgrammingItem.itemPriority)
  assertEquals(listCategories.first().id,
      firstPurrProgrammingItem.listCategoryId)
  assertEquals(1, firstPurrProgrammingItem.id)
}

This test does the following using the entity DAOs :

  • Inserts a record with a priority of 1 and item description of desk cushion associated with the Purr Programming Supplies category.
  • Checks that only one record was actually inserted.
  • Verifies that the persisted values match what we’ve added.

Now, run your test by right-clicking on your ListItemMigrationTest file and clicking Run . All tests should be green.

Migrating When Your App Initializes

Great! You’re confident that your migration code works. Now, you need to run this migration in the app. Since the goal is to have your app update the database, you’re going to need it to execute it when the user first opens the app. To do that, open the ListMasterApplication class and replace the onCreate() method with the following:

override fun onCreate() {
  super.onCreate()
  ListMasterApplication.database = Room.databaseBuilder(
      this,
      AppDatabase::class.java,
      "list-master-db")
      .addMigrations(AppDatabase.MIGRATION_1_TO_2)
      .build()
}

The addMigrations() call does the following:

  • Checks if the migration has been applied to the database. If not, it runs the migration.
  • If migration has already been applied to the database, it will do nothing.

Wiring in the User Interface

Now that you have your ListItem hooked into the database, it’s time to wire it into your interface.

To start, open up the ListCategoryViewHolder class and add the following lines below the existing code inside the setListCategoryItem(listCategory: ListCategory) method:

holderListCategoryBinding.categoryName.rootView.setOnClickListener {
  val intent = Intent(listCategoriesActivity, ListItemsActivity::class.java)
  intent.putExtra(ListItemsActivity.LIST_CATEGORY_ID, listCategory.id)    
  intent.putExtra(ListItemsActivity.CATEGORY_NAME, listCategory.categoryName)
  listCategoriesActivity.startActivity(intent)
}

This adds an OnClickListener to each category in the list. The click listener launches a ListItemActivity , passing it the category ID and name.

Now, run the app by clicking on Run ▸ Run ‘app’ (you may need to switch the run configuration from the test class back to app first). Then, click on a category, such as Purr Programming Supplies , and you will see a screen that looks like this:

room_db_2_clicking_category-250x500.png

Your app is set up to use the Android Architecture Components MVVM pattern. When evaluating where to put the database query/insert , if you are not using MVVM , you might be inclined to put those queries in your activity . Part of the power of MVVM is the ability to put that logic into a component that is focused solely on data access.

In your case, you’re going to use two components:

  1. A repository object that focuses on interacting with your DAO and anything that is database-specific.
  2. A ViewModel that is lifecycle -aware by extending AndroidViewModel .

Create a ListItemRepository by right-clicking on the listitem package. Next, select New ▸ Kotlin File/Class . Name it ListItemRepository and press OK . Finally, paste in the following:

class ListItemRepository {
  //1
  private val listItemDao = ListMasterApplication.database!!.listItemDao()
  
  //2
  fun insertAll(vararg listItems: ListItem) {
    AsyncTask.execute {
      listItemDao.insertAll(*listItems)
    }
  }
}

You are doing two things in this class:

  1. Getting a reference to your DAO .
  2. Providing a function to insert listItems in a background thread.

Next, you are going to create a lifecycle -managed ViewModel that will abstract the details of working with your repository. To do that, create a new Kotlin class in the listitem package by right-clicking the package name, selecting New and then Kotlin File/Class . Name it ListItemsViewModel and press OK . Finally, paste in the following:

//1
class ListItemsViewModel(application: Application) : AndroidViewModel(application) {

  //2
  private val listItemRepository: ListItemRepository = ListItemRepository()

  //3
  fun insertAll(vararg listItems: ListItem) {
    listItemRepository.insertAll(*listItems)
  }
}

This is doing a few things for you:

  1. Extends AndroidViewModel and takes a reference to the application in its constructor.
  2. Creates an instance of your repository and keeps a reference to it.
  3. Exposes an insertAll() method from your repository.

Note : If you are new to using View Models , check outthis tutorial to learn about MVVM and how View Models fit into the pattern, and also our courseMVVM on Android.

Now you are going to do some work in ListItemsActivity . To start, open it up and add a property for your ListItemsViewModel above onCreate() :

private lateinit var listItemsViewModel: ListItemsViewModel

Inside onCreate() , add the following line right above the call to setupAddButton() :

listItemsViewModel =
    ViewModelProviders.of(this).get(ListItemsViewModel::class.java)

This line of code initializes listItemsViewModel by calling ViewModelProviders to get an instance of your ListItemsViewModel .

Next, replace the setupAddButton() method with the following:

private fun setupAddButton() {
  activityListItemsBinding.fab.setOnClickListener {

    // Setup the dialog
    val alertDialogBuilder = AlertDialog.Builder(this).setTitle("Title")
    val dialogAddItemBinding = DialogAddItemBinding.inflate(layoutInflater)
    // 1
    val listItemViewModel = ListItemViewModel(ListItem("", 0, listCategory.id))
    dialogAddItemBinding.listItemViewModel = listItemViewModel

    alertDialogBuilder.setView(dialogAddItemBinding.root)

    /**
     * Setup the positive and negative buttons.
     * When the user clicks ok, a record is added to the db,
     * the db is queried and the RecyclerView is updated.
     */
    alertDialogBuilder.setPositiveButton(android.R.string.ok)
    { _: DialogInterface, _: Int ->
      // 2
      listItemsViewModel.insertAll(listItemViewModel.listItem)
    }
    alertDialogBuilder.setNegativeButton(android.R.string.cancel, null)
    alertDialogBuilder.show()
  }
}

This hooks up the + button by doing the following:

  1. Creates an instance of your ListItemViewModel and binds it to the dialog. Note that ListItemViewModel is different from ListItemsViewModel .
  2. Calls insertAll() on ListItemsViewModel when the user clicks OK .

Run the app by selecting the Run ▸ Run ‘app’ on the menu:

room_db_2_add_item.png

If you click into a category, such as Purr Programming Supplies , you can then click the + button, add an item and priority , click OK , and it will take you back to the list that does not show anything. At this point, your first response might be:

room_db_wheres_the_data.jpg

That’s because you have not hooked up your LiveData object to view these items, so time to fix that! Start by opening up ListItemRepository and add a new method named getAllByListCategoryId() :

fun getAllByListCategoryId(listCategoryId: Long): LiveData<List<ListItem>> {
  return listItemDao.getAllByListCategoryId(listCategoryId)
}

This method takes a listCategoryId and returns a LiveData object from listItemDao . Next, open ListItemsViewModel and add a method also named getAllByListCategoryId() with different implementation:

fun getAllByListCategoryId(listCategoryId: Long): LiveData<List<ListItem>> {
  return listItemRepository.getAllByListCategoryId(listCategoryId)
}

This method takes same listCategoryId but returns a LiveData object from your listItemRepository .

Now, open up your ListItemsActivity and paste the following method:

private fun setupRecyclerAdapter() {
  val recyclerViewLinearLayoutManager = LinearLayoutManager(this)

  contentListItemsBinding = activityListItemsBinding.listItemsViewInclude!!
  contentListItemsBinding.listItemRecyclerView.layoutManager =
      recyclerViewLinearLayoutManager
  listItemAdapter = ListItemAdapter(listOf(), this)
  listItemsViewModel.getAllByListCategoryId(listCategory.id).observe(
      this, Observer { listItems: List<ListItem>? ->
    listItems?.let {
      listItemAdapter.itemList = it
      listItemAdapter.notifyDataSetChanged()
    }
  })
  contentListItemsBinding.listItemRecyclerView.adapter = listItemAdapter
}

This sets up your RecyclerView by placing an observer in your LiveData object returned by getAllByListCategoryId() to update the RecyclerView when new ListItem objects are added to a category.

Now, it’s time to add a call to your setupRecyclerAdapter() method at the end of onCreate :

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  ...

  setupRecyclerAdapter()
}

It’s time to run the app again by clicking on the Run option and clicking on Run ‘app’ . You should now see the item you added before.

When clicking into Purr Programming Supplies , you will see the Cat Nip you added before. Now, tap the + button, type in Cat Bed with a priority of 2 , tap OK and you will see it in your list:

room_db_2_final_flow.png

Indexes

When a database table begins to get a lot of records in it, queries can often start to slow down. To mitigate that with SQLite, you can add an index to fields you frequently query to speed up these queries. Under the hood, the database makes a copy of the fields that you are indexing in a data structure that is more efficient to query.

Note : Check out this Wikipedia article to learn more about indexing .

In your app, there can be several list_category records, and each list_category can have multiple list_item records. More importantly, you are regularly performing queries on the list_category_id field to query for them by for selected list_category IDs. Because of that, you are going to add an index to this field.

To add an index, start by replacing the @Entity annotation of the ListItem entity with the following:

@Entity(
    tableName = "list_items",
    foreignKeys = [ForeignKey(
        entity = ListCategory::class,
        parentColumns = ["id"],
        childColumns = ["list_category_id"],
        onDelete = CASCADE)],
    indices = [Index(value = ["list_category_id"],
        name = "index_list_category_id")])

Here, you’ve added an indices property to the entity that is passing in an array of Index objects. In that array, you are then creating the Index with two fields:

  • value : Which field(s) you want to index.
  • name : A unique name for the index.

Note : The index name is used for things such as migrations. When you perform queries, you still query against the fields as you did before adding the index.

Now that you have your index, you’re going to need to create a migration for users who have the previous version of the schema. To do that, create a file called Migration2To3 in the migrations package and paste in the following:

@VisibleForTesting
class Migration2To3 : Migration(2, 3) {

  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL(
        "CREATE INDEX 'index_list_category_id' ON list_items('list_category_id')")
  }
}

Next, replace your AppDatabase with the following:

//1
@Database(entities = [ListCategory::class, ListItem::class], version = 3)
abstract class AppDatabase : RoomDatabase() {

  abstract fun listCategoryDao(): ListCategoryDao

  abstract fun listItemDao(): ListItemDao

  companion object {

    @VisibleForTesting
    val MIGRATION_1_TO_2 = Migration1To2()
    //2
    @VisibleForTesting
    val MIGRATION_2_TO_3 = Migration2To3()
  }
}

In this, you have done two things:

  1. Incremented the version of the schema from 2 to 3.
  2. Added a reference to your new migration.

Now, you need to tell your app database builder to use the new migration by replacing the onCreate() method in ListMasterApplication with:

override fun onCreate() {
    super.onCreate()
    ListMasterApplication.database = Room.databaseBuilder(
        this,
        AppDatabase::class.java,
        "list-master-db")
        .addMigrations(AppDatabase.MIGRATION_1_TO_2)
        .addMigrations(AppDatabase.MIGRATION_2_TO_3)
        .build()
  }

Run the app to make sure it still runs correctly after indexing.

Index Drawbacks

While indexes can be very helpful, there are some drawbacks to be aware of. One big drawback stems from the need to add a record to another data structure, which means inserts into a table with an index may take longer to perform. Another drawback is that indexes increase the amount of storage needed by the database.

room_db_reads_or_inserts_q-625x500.png

Some scenarios where an index to common query fields may be beneficial:

  • You read data from a table more often than you write to it.
  • Query speed is more important than insert speed.

Updating Tests

Since you made changes to your code, you might want to re-run your unit tests to make sure that you haven’t broken anything. Run them and you will see the following:

room_db_broken_test.png

Oops! This is happening because you are instantiating your own version of the migrated Room database in your getMigratedRoomDatabase() method in ListItemMigrationTest . Currently, it is only migrating your database to version 2 , but Room will not work unless your database is migrated to the current version, which is 3 .

To fix this, add .addMigrations(AppDatabase.MIGRATION_2_TO_3) after the .addMigrations(AppDatabase.MIGRATION_1_TO_2) in getMigratedRoomDatabase() . Your method will now look like this:

private fun getMigratedRoomDatabase(): AppDatabase {
    //1
    val appDatabase = Room.databaseBuilder(
        InstrumentationRegistry.getTargetContext(),
        AppDatabase::class.java, TEST_DB_NAME)
        //2
        .addMigrations(AppDatabase.MIGRATION_1_TO_2)
        .addMigrations(AppDatabase.MIGRATION_2_TO_3)
        //3
        .build()
    //4
    migrationTestHelperRule.closeWhenFinished(appDatabase)
    return appDatabase
  }

Since your tests are now testing version 3 of your migration instead of version 2 , rename the existing tests from the following,

@Test
fun migrating_from_1_to_2_retains_version_1_data() {
  ...
}

@Test
fun inserting_a_record_into_list_items_after_migrating_from_1_to_2_succeeds() {
  ...
}

to:

@Test
fun migrating_from_1_to_3_retains_version_1_data() {
  ...
}

@Test
fun inserting_a_record_into_list_items_after_migrating_from_1_to_3_succeeds() {
  ...
}

Because the index does not affect the structure of your data or how you access it, you do not need change any other code.

Now, run your tests and all of them will pass!

Screen-Shot-2018-09-21-at-12.25.43-AM.png

Where to Go From Here?

You can download the final project using the Download materials button at the top or bottom of this tutorial.

As you start to work with more complex data, you can use Type Converters to map database types to your own custom types. If you are new to data persistence on Android and want more background, you can check out the Saving Data on Android video course, which covers Shared Preferences , saving to files, SQLite and migrations.

As a challenge, you can also try to:

  • Add the ability to delete a list item.
  • Create an onClick event for each list item that allows you to edit the item and save the update to your database.

Feel free to share your feedback or findings, and please post your questions in the comments section below or in the forums. I hope that you’ve enjoyed this tutorial on advanced data persistence with Room!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK