Room is a persistent repository from Google that provides an abstraction layer on SQLite. This paper will introduce Room from the following aspects:

  • Why use Room?
  • Through a case, how to use Room
  • Analyze the composition and use principle of Room
  • Summarize the use of Room

1. Why use Room?

Using SQLite directly in Android has several disadvantages:

  • You have to write a lot of boilerplate code;
  • Object mapping must be implemented for every query you write;
  • Difficult to implement database migration;
  • It’s hard to test the database;
  • If you’re not careful, you can easily run long-running operations on the main thread.

To address these issues, Google created Room, a persistent repository that provides an abstraction layer on SQLite.

Room is a robust database framework based on the object Relational Mapping (ORM) model. Room provides a sqLite-based abstraction layer that fully implements SQLite’s full capabilities while enabling more powerful database access.

In view of the above shortcomings of SQLite database, Room framework has the following characteristics:

  • The use of dynamic proxies reduces boilerplate code;
  • Compile-time annotations are used in Room framework, and the syntax check of SQL is completed during compilation.
  • Relatively convenient database migration;
  • Convenient testability;
  • Keep database operations away from the main thread.
  • Room also supports RxJava2 and LiveData.

2. Introduce how to use Room through a case

To introduce Room, we start with a real case, from the design of ER graph at the beginning, to the operation of adding, deleting, changing and checking database, to the database migration online.

To sum up, this case of using the Room framework goes through the following processes:

    1. Design ER diagrams for the database (optional);
    1. Add a dependency on Room;
    1. Create database Entity Entity;
    1. Create daOs for database access;
    1. Create Database Database;
    1. A Repository that encapsulates database interactions with business logic;
    1. Create type converters used in the database;
    1. Consider database migration;
    1. Database testing.

Next, we will introduce the use of Room from these steps.

2.1 ER diagram of database

We need to complete the storage of information of NBA teams and players in the database, specifically involving two tables, team table and Player table. In order to screen out players with top data indicators, we create a star player table, and the ER charts of the three tables are as follows:

Among them:

  • A team has multiple players;
  • A star belongs to a certain team, and the list of stars can be from multiple teams.
  • Star players are chosen from among the players.

2.2 Adding Room Dependencies

Now that the relationships between the three tables are clear, we introduce Room and add dependencies:

implementation "androidx.room:room-runtime:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
Copy the code

Since the annotation processor is used in the Room framework, the kapt dependency is required and the kapt plugin needs to be introduced in the build.gradle file.

apply plugin: 'kotlin-kapt'
Copy the code

For Java projects, the annotationProcessor keyword is required.

Here we use the latest official version 2.1.0.

roomVersion : '2.1.0'.Copy the code

2.3 Creating Entities

Start creating entities (tables). Here’s a Player example:

/** ** player table */
@Entity(tableName = "player")
data class PlayerModel(
    @ColumnInfo(name = "player_code") var code: String,
    @ColumnInfo(name = "player_country") var country: String, / / country
    @ColumnInfo(name = "player_country_en") var countryEn: String,// The English name of the country
    @ColumnInfo(name = "player_display_name") var displayName: String,// Player name
    @ColumnInfo(name = "player_display_name_en") var displayNameEn: String,// Player name in English
    @ColumnInfo(name = "player_dob") var dob: Calendar = Calendar.getInstance(),// Date of birth.@ColumnInfo(name = "player_team_name") var teamName: String,// Team
    @Embedded var statAverage: StatAverageModel,// Average data
    @Embedded var statTotal: StatTotalModelAccording to the total / /
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id: Long = 0
}
Copy the code

Here are a few notes:

The serial number Note the name describe
1 @Entity Declare that the class marked is a data table, and @entity includes the following arguments: TableName (tableName), indices (table index), primaryKeys (primary key), foreignKeys (foreign key), ignoredColumns (ignore attributes in entities and do not act as columns in data tables), InheritSuperIndices (If inheritSuperIndices are integrated with parent classes, default false)
2 @ColumnInfo Used to declare the names of fields in the database
3 @PrimaryKey The modified property serves as the PrimaryKey of the data table, and @primarykey contains one parameter: autoGenerate(whether automatic creation is allowed, false by default)
4 @Embedded Used to modify nested fields. All fields in the modified property will exist in the data table

As a further explanation for @Embedded, our Player entity has two attributes that are modified by @Embedded. Let’s look at one of these attributes, statTotal, which is of type StatTotalModel and is used to describe Player data.

/** * total data */
data class StatTotalModel(
    var assists: Int./ / assists
    var blocks: Int./ / blocks
    var defRebs: Int.// Defensive rebound
    var fga: Int.var fgm: Int.var fgpct: Float.var fouls: Int./ / foul
    var fta: Int.var ftm: Int.var ftpct: Float.var mins: Int.// Playing time
    var offRebs: Int.Offensive rebounds
    var points: Int./ / score
    var rebs: Int./ / total rebounds
    var secs: Int.var steals: Int./ / a steal
    var tpa: Int.var tpm: Int.var tppct: Float.var turnovers: Int/ / error
)
Copy the code

Typically, there are two ways to store this data in a database:

  • Select * from StatTotalModel; select * from StatTotalModel; select * from StatTotalModel; select * from StatTotalModel;
  • Storing fields from StatTotalModel entities in Player tables reduces the creation of data tables and reduces the complexity of federated queries.

If breaking these fields up in the Player table is not object-oriented, you can use the @Embedded annotation to make it object-oriented without creating a table. It’s very elegant.

In addition to the annotations mentioned above, the Room framework also includes the following annotations:

The serial number Note the name describe
1 @ColumnInfo.SQLiteTypeAffinity SQLite column type constants that can be used in typeAffinity () include: UNDEFINED, TEXT, INTEGER, REAL, BLOB, where UNDEFINED UNDEFINED type association will be resolved by type; TEXT The SQLite column type is String. INTEGER The SQLite column type is INTEGER or Boolean. The REAL SQLite column type is Float or Double; BLOB SQLite column type is binary
2 @Dao Marking a class as a Data Access Object
3 @Database Mark the class as RoomDatabase
4 @Delete Mark the methods in the DAO as methods related to deletion
5 @Embedded Can be used as a comment on an entity or Pojo field to indicate nested fields
6 @ForeignKey Declare a foreign key on another entity
7 @ForeignKey.Action Constant definitions of values that can be used in onDelete () and onUpdate (). The value can be NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, and CASCADE
8 @Ignore Ignore the marked elements in Room’s processing logic
9 @Index Declare the index of the entity
10 @Insert Mark methods in Dao annotation classes as insert methods
11 @OnConflictStrategy The Dao methods handle a set of conflicting policies, including REPLACE, ROLLBACK, ABORT,FAIL,IGNORE, where ROLLBACK and FAIL have been marked @deprecated, REPLACE replaces old rows with new ones; ABORT directly rolls back a conflicting transaction; IGNORE keeps existing rows.
12 @PrimaryKey Mark fields in entities as primary keys
13 @Query Mark methods in Dao annotation classes as query methods
14 @RawQuery Mark the methods in the Dao annotation class as raw query methods that can be passed as SupportSQLiteQuery
15 @Relation A handy annotation that can be used in poJOs to automatically retrieve relational entities.
16 @SkipQueryVerification Skip database validation of annotated elements
17 @Transaction Mark methods in the Dao class as transactional methods
18 @TypeConverter Mark the method as a type converter
19 @TypeConverters Specify other types of converters that Room can use
20 @Update Mark methods in Dao annotation classes as update methods

2.4 create a Dao

Let’s start by creating the DATA Access Object layer Dao, where we need to define some methods to add, delete, change, or query the database.

Specifically, create an interface that uses @DAO annotations. And declare all the functions needed to use the database on it, and write the corresponding SQL query statements, Room will implement these functions for you, and run them in a single transaction, Room supports the query statements include: insert, update, delete and query. Queries are validated at compile time, which means that if you write an invalid application, you will immediately find the error.

Look at the Dao of Player:

@Dao
interface PlayerDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertPlayer(player: PlayerModel)

    @Delete
    fun deletePlayers(players: List<PlayerModel>)

    @Update
    fun updatePlayers(players: List<PlayerModel>)

    @Query("SELECT * FROM player WHERE id=:id")
    fun findPlayerById(id: Long): PlayerModel? ... }Copy the code

The annotations corresponding to the add, Delete, alter and Query methods of the data table are @INSERT, @delete, @update, and @query respectively, where:

  • @insert and @update can set parameters onConflict. The following parameters can be set: REPLACE, ROLLBACK, ABORT,FAIL, and IGNORE. ROLLBACK and FAIL have been marked as @Deprecated and will be Deprecated. REPLACE replaces old rows with new ones. ABORT directly rolls back a conflicting transaction; IGNORE keeps existing rows.
  • @query declares the SQL statement we want to Query. Using @query, we can not only complete the Query, but also add, delete and modify the operation.

These queries are all synchronous, which means they will run on the same thread that you triggered the query. If this is the main thread, your app will crash and display an IllegalStateException, so use the thread handling method recommended in Android and make sure it stays away from the main thread.

Room also supports asynchronous queries when using LiveData or RxJava, and more importantly, queries that return LiveData or Flowable are observable queries. That is, you will be notified whenever the data in the table is updated.

    @Query("SELECT * FROM player WHERE id=:id")
    fun findPlayerByIdLD(id: Long): LiveData<PlayerModel>

    @Query("SELECT * FROM player WHERE player_team_name=:teamName")
    fun findPlayersByTeamLD(teamName: String): LiveData<List<PlayerModel>>
Copy the code

2.5 Creating a Database

The class that integrates the entity and DAO is RoomDatabase, which starts by creating an abstract class that extends RoomDatabase, annotating it, and declaring the entity and the corresponding DAO.

Database creation is a very resource-intensive endeavor, so we designed the database as a singleton to avoid creating multiple database objects. In addition, all operations on the database cannot be completed in the UI thread, otherwise an exception will occur:

Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Copy the code

Give the database we designed:

@Database(entities = [PlayerModel::class, TeamModel::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class NBADatabase : RoomDatabase() {

    abstract fun playerDao(a): PlayerDao
    abstract fun teamDao(a): TeamDao

    companion object {
        @Volatile
        private var INSTANCE: NBADatabase? = null

        fun getInstance(context: Context): NBADatabase {
            returnINSTANCE ? : synchronized(this) {
                Room.databaseBuilder(
                    context.applicationContext,
                    NBADatabase::class.java."nba_db"
                ).addCallback(object : RoomDatabase.Callback() {
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        super.onCreate(db)
                    }

                    override fun onOpen(db: SupportSQLiteDatabase) {
                        super.onOpen(db)
                    }
                }).build().also {
                    INSTANCE = it
                }
            }
        }
    }
}
Copy the code

When creating a database, you need to do the following:

  • The singleton pattern is designed to avoid creating multiple database objects and consuming resources.
  • Create a database class that inherits from RoomDatabase, which is declared abstract;
  • Methods need to be provided to obtain data Access Object Layer (Dao) objects, which are declared as abstract methods;
  • The Database class requires the @database annotation, which takes several parameters: Entities (tables included in the database, entities modified by the @entities annotation), default (views included in the database), Version (version number of the database), exportSchema (which can be interpreted as a switch, if the switch is true, The Room framework outputs some database-related schemas to the specified directory via the annotation handler, default true)

2.6 packaging Repository

A player-specific Repository is encapsulated:

class PlayerRepository(private val playerDao: PlayerDao) {

    @WorkerThread
    suspend fun insert(players: List<PlayerModel>) {
        playerDao.insertPlayers(players)
    }

    fun findAllPlayers():List<PlayerModel>{
        return playerDao.findAllPlayers()
    }
}
Copy the code

There are only two methods defined here: Insert () to insert data, and findAllPlayers() to query all players. Methods can be added to the Repository as required by business logic.

Finally, call the method in PlayerRepository to finish storing the NBA team and player information in the database.

2.7 TypeConverter @typeconverter

In our player entity, PlayerModel, the player’s date of birth doB is of the Calendar type, but SQLite only supports five types: NULL, INTEGER, REAL, TEXT, and BLOB. You can use the @typeconverters annotation.

class Converters {
    @TypeConverter
    fun calendarToDatestamp(calendar: Calendar): Long = calendar.timeInMillis

    @TypeConverter
    fun datestampToCalendar(value: Long): Calendar =
        Calendar.getInstance().apply { timeInMillis = value }
}
Copy the code

Add the annotation to the database

@TypeConverters(Converters::class)
Copy the code

2.8 Database Migration Migration

If you want to do a Database migration operation, you need to do the following in the Database class:

Start by updating your database version.

@Database(entities = {User.class}, version = 2)
abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao getUserDao(a);
}
Copy the code

Second, implement a Migration class that defines how to handle migrations from the old version to the new version:

static final Migration MIGRATION_1_2 = new Migration(1.2)

    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE users" +
                "ADD COLUMN address STRING");
    }
Copy the code

Third, add the Migration class as a parameter to the Database builder,

Room.databaseBuilder(context.getApplicationContext(),
    MyDatabase.class,"sample.db").addMigrations(MIGRATION_1_2).build();
Copy the code

When the migration is triggered, Room validates the Schema for you to ensure that the migration has completed correctly.

2.9 Database testing

We’ve created entities, DAOs, databases, and migrations, so how do we test them?

To test the DAO, you need to implement AndroidJunitTest to create an in-memory database that will retain data only as long as the process is active, that is, the database will be wiped after each test.

@RunWith(AndroidJunit4.class)
    public class UserDaoTest {
        private UserDatabase database;

        @Before
        public void initDb(a) throws Exception {
            database = Room.inMemoryDatabaseBuilder(
                    InstrumentationRegistry.getContext(),
                    UsersDatabase.class).build();
        }

        @After
        public void closeDb(a) throws Exception { database.close(); }}Copy the code

To test asynchronous queries, add the test rule InstantTaskExecutorRule to execute each task synchronously.

@RunWith(AndroidJUnit4.class)
    public class UserDaoTest{
        @Rule
        public InstantTaskExecutorRule rule = new InstantTaskExecutorRule();
    }
Copy the code

In the implementation of your application, you’ll end up referencing daOs in other classes that you can unit test by using a framework like Mockito to simulate the DAO.

public class UsersRepository {
        private final UserDao userDao;

        public UsersRepository(UserDao userDao) {
            this.userDao = userDao; }}Copy the code

Also, extend CountingTaskExecutorRule and use it in Espresso tests to count tasks at the start and end.

@Rule
public CountingTaskExecutorRule rule = new CountingTaskExecutorRule();
Copy the code

Finally, don’t forget migration tests. We have another very handy test rule, MigrationTestHelper. It allows you to create databases using older versions, then run and verify migrations. All you need to do is check to see if the data you inserted in the older version still exists after the migration.

public MigrationTestHelper testHelper = new MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            MyDatabase.class.getCanonicalName(),
            new FrameworkSQLiteOpenHelperFactory()
    )
Copy the code

3. Analyze the composition and use principle of Room

Below, let’s analyze the composition and implementation principle of Room. According to the convention, we combed a class diagram:

In the class diagram, common annotations are drawn, color-coded by database, DAO, and entity. If you want to understand how Room implements database creation and SQL statement generation through annotations, you may want to know about dynamic proxy technology and annotation handlers.

  • Dynamic agent technique
  • Annotation processor

As for the principle of Room, Room is actually encapsulated in SQLite, which is convenient for developers through annotations.

From the figure, we can clearly see how each component of Room (Database, Dao and Entity) works together, which can be summarized as follows:

  • You start by creating a database class that inherits from RoomDatabase and provides an abstract method to get the Dao. The Annotation handler of the Room framework implements the concrete method to generate the Dao.
  • In Dao, we will declare some methods to operate the add, delete, change and query of specific database tables. Using these methods, we can operate some specific entities.
  • Once you have the Entity, you can handle some business logic related to your app data.

4. Summarize the use of Room

The use of Room is introduced through a completed case. From the initial design of ER diagram, create entity, create Dao, create database, and encapsulate Repository. If data type conversion is required, use @typeconverter annotation. You also use the Migration class.

The Room framework has reduced boilerplate code, compile-time validation queries, easy migration, a high degree of testability, and keeping database operations off the main thread. All of these features of Room make it easier to use the database and help you build better apps.

For more, you can subscribe to my blog


Refer to the link

Save data in a local database using Room Understanding migrations with Room