Migrating to Room in the real world — Part 2

Sebastiano Gottardo
ProAndroidDev
Published in
6 min readJun 24, 2019

--

Preparing the ground and getting our hands dirty.

Photo by Scott Blake on Unsplash

In the first part, David and I outlined the process we would be following for migrating to Room from our previous ORM, Cupboard, and we identified 4 main phases for doing so.

In this second part, we will look at the first two phases:

  • Phase 1: review existing tests and add the missing ones
  • Phase 2: extract interfaces and add a Room-based implementation for each repository

Phase 1: review existing tests and add the missing ones

Because each entity model came with its own repository, it was fairly easy to split the task between two people: every time one of us finished adding or reviewing tests for a particular repository, they would pick the next one and communicate it to the other. Overall, this is the amount of PRs that were opened and merged.

Overview of all the PRs that added or reworked existing tests.

In order to ease the task of reviewing these PRs, we adopted a commit-by-commit approach and we followed the same structure every time. Let’s take a look at one of them, for example:

Detail of one sample PR that shows the recurring commit structure of all the other PRs.

The reviewer might find zero or more conversions to Kotlin (as per the boy scout rule we adopted), in addition to new tests or reviewed ones. Having the same structure removed some overhead from the reviewer, because they consistently knew what to expect when opening a PR for review.

While one could argue that adding new tests or reviewing existing ones doesn’t have any impact on the app, that is not necessarily true every single time. Say, for example, that a model for an entity was converted to Kotlin in the process, or even something more complex, like a repository: this does have an impact on the app (for example, in making assumptions about nullability), thus requiring additional testing efforts together with the QA team. For this reason, we coordinated again with them to make sure that they were aware of what could possibly have changed by adding these suits of tests.

This process of reviewing tests was useful in and of itself. Not only it helped us gaining a better understanding of the codebase and the database layer, but we also found and fixed some bugs that were spotted in the meantime. Like this one, for example:

After having gone through all the repositories and having made sure that as many test cases as possible were covered, it was time to move to Phase 2.

Phase 2: Room implementation for repositories

This is where the real fun begins. Let’s recap the high-level requirements:

  • Do not block other teams
  • Minimize the impact
  • Minimize the regressions

While investigating the best approach to satisfy those requirements, we stumbled upon these two articles by the amazing Florina Muntenescu, and we decided to take a similar approach:

  • One repository/entity at a time
  • One PR per each repository/entity
  • Being able to switch implementation easily (interfaces)
  • Test every repository in isolation, using the existing test suite

In the previous part, we highlighted what our desired state for a Room-based implementation looked like. As a refresher, here it is:

Data diagram for the Library feature, using Room.

And so, we set out to do just that. Once more, we tried to give a common structure to every PR, in order to ease the reviewers’ task. Here’s roughly how it looked like:

Detail of a PR that adds a Room-based repository. Note the commit structure.

As you can see, we first extract an interface out of the concrete (and Cupboard-dependent) repository implementation; in doing so, we also make sure that we bind the two in our dependency graph.

We then move to annotating the entity model for Room. A small setback was that both Cupboard and Room use the same name for some annotations, like @Index and @Ignore. Since we wanted both of them to co-exist for the time being, and didn’t want to use the fully-qualified name, we resorted to Kotlin’s handy import alias:

Import alias to the rescue!

After the model is set, we create the DAO and a Room-based implementation for the repository. In doing that, we finally started to really appreciate how much more concise and pretty to look at the code with Room is. On top of that, we even had compile time-safe query statements and autocomplete 🙌

Comparison between the previous Cupboard implementation and the corresponding Room (DAO, in this case) implementation for a repository.

Creating the Room-based repository implementations has mostly been trivial, with some notable exceptions. In some cases, our queries were fairly dynamic, so we had to build the SQL statement at runtime and use @RawQuery in the DAO definition. In some other cases, the data we wanted to persist wasn’t trivial and required some kind of conversion, which was done using Cupboard’s converters; luckily, Room provides the exact same feature, by means of Type Converters.

Finally, some Cupboard-based repositories were performing side effects on other tables (remember, the database schema is very simple, to the point of relations being implemented manually). We decided to keep the current behavior, and to simply allow for a Room-based repository to have another Room-based repository or DAO as a dependency. Although not ideal, we agreed to revisit this after the migration to Room was completed, and on the upside, it still allowed us to perform tests in isolation.

The final step is to run the existing tests that were revisited in Phase 1. Room supports SQLite’s ability of running a so-called “in-memory” database: intuitively this means that, instead of persisting the data into a file, Room will simply keep it in memory, therefore making it volatile. In doing so, we were able to test the Room-based repositories as well as the Cupboard-based ones by simply providing the respective one in the dependency graph.

Creating an in-memory database with Room.

The most interesting fact about this approach is that you don’t need to have the new database fully migrated to Room to test it. Instead, you can test a subset of tables at a time, which comes with the huge advantage of allowing us to make sure that our queries were correct, together with the Room-based implementation, without even having to run the app once.

You don’t need to have the new database fully migrated to Room to test it

Once again, we decided to scatter the PRs into different releases, so to make sure that our changes weren’t affecting the overall functionalities of the application. So far, so good: the few regressions we had were caught during the stabilization phase of our release process, thanks to the joint efforts between the QA team and the Android team. It was time to move to Phase 3.

We hope you enjoyed part 2 of this series! Curious to hear how this all ends? Stay tuned for the third and final part! 🤩

EDIT: part 3 is now live! And make sure you don’t miss out on part 1!

In the meantime, make sure to let Sebastiano and David know what you think on Twitter!

--

--