Use of the Adroid Jetpack Room database

First, add maven dependencies:

dependencies {
  def room_version = "2.2.0 - alpha01"

  implementation "androidx.room:room-runtime:$room_version"
  annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
}
Copy the code

Use Room to save data to a local database

Room provides a layer of abstraction on top of SQlite, allowing you smooth access to the full functionality of SQlite databases.

When an application is dealing with a large number of data structures, it can get a lot of help from local data. The most common use case is to cache relevant data so that when the device cannot access the network, the user can still browse the content offline. Any user-initiated content changes can then be synchronized to the server once the device is reconnected.

Since Room solves these problems for us, Google strongly recommends using Room instead of SQLite.

Room is mainly composed of three parts:

  • Database: Contains the Database holder and serves as the primary access point for the underlying connection to the application’s persistent relational data

Classes that use the @Database annotation should meet the following criteria:

  • Is an abstract class inherited from RoomDatabase
  • Include a list of entities associated with the database in the annotations
  • Contains an abstract method with no arguments and returns an annotated @DAO

At run time, we can call Room. DatabaseBuilder () or Room. InMemoryDatabaseBuilder () to access the Database instance.

  • Entity: indicates a table in the database
  • DAO: Contains methods used to access the database

These components and their relationship to the rest of the application are shown in Figure 1:

The following code snippets contain the configuration of a sample database with an entity class and a DAO:

User.kt

@Entity(tableName = "users")
data class User(
    @PrimaryKey
    @ColumnInfo(name = "userid") val id: String = UUID.randomUUID().toString(),
    @ColumnInfo(name = "username") val userName: String)
Copy the code

UserDao.kt

@Dao
interface UserDao {

    @Query("SELECT * FROM Users WHERE userid = :id") fun getUserById(id: String): User? / * when the database has the user directly REPLACE * / @ Insert (onConflict = OnConflictStrategy. REPLACE) fun insertUser (user: User) @Update fun upDateUser(user: User) @Query("DELETE FROM Users")
    fun deleteAllUsers()
}
Copy the code

AppDatabase.kt

@Database(entities = arrayOf(User::class), version = 1)
abstract class UsersDatabase : RoomDatabase() { abstract fun userDao(): UserDao companion object { @Volatile private var INSTANCE: UsersDatabase? = null fun getInstance(context: Context): UsersDatabase = INSTANCE ? : synchronized(this) { INSTANCE ? : buildDatabase(context).also { INSTANCE = it } } private fun buildDatabase(context: Context) = Room.databaseBuilder( context.applicationContext, UsersDatabase::class.java,"Sample.db")
                .build()
    }
}
Copy the code

⚠️ Note: database instantiation is expensive, so it is recommended that we use the singleton pattern for initialization, and rarely need to access more than one instance.

Use Room entities to define data

By default, Room creates a column for each field defined in the entity class, and if there are fields in the entity that we don’t want to keep, we can annotate them with @Ignore. We must refer to the entity class through the Entities array in the Database class.

The following code shows how to define an entity:

@Entity data class User( @PrimaryKey var id: Int, var firstName: String? , var lastName: String? )Copy the code

To store a field, Room must be able to access it. We can make the fields public, or we can provide getters and setters. If we use getters and setters, they are based on the convention of JavaBeans in Room.

⚠️ Note: Entities can have an empty constructor (if the corresponding DAO class has access to each persistent field) or an entity whose constructor arguments contain the type and name of the matching field. Room can also use all or part of the constructor, such as some fields that the constructor only receives.

Use a primary key

Each entity class must define at least one primary key. Even if there is only one field, we still need to use the @primaryKey annotation. Alternatively, if we want to use Room to automatically assign an Entity ID, we can set the autoGenerate property for @primarykey, or annotate the primaryKeys property for @entity if the Entity has a compound PrimaryKey, as shown in the following code:

@Entity(primaryKeys = arrayOf("firstName"."lastName"))
data class User(
    valfirstName: String? .val lastName: String?
)
Copy the code

By default, Room uses the entity class name as the database table name. If you want to set a different tableName, annotate it with the @entity property tableName, as shown in the following code snippet:

@Entity(tableName = "users")
data class User (
    // ...
)
Copy the code

⚠️ Note: Table names are case insensitive in the database

Like the tableName property, Room uses the field name as the database column name. If you want to change the name of a column, add the @ColumnInfo annotation, as shown in the following code snippet:

@Entity(tableName = "users")
data class User (
    @PrimaryKey val id: Int.@ColumnInfo(name = "first_name") valfirstName: String? .@ColumnInfo(name = "last_name") val lastName: String?)
Copy the code
Ignore annotations

By default, Room creates a column for each field in the entity class. If there are fields in the entity class that you don’t want to store, you can use the @ignore annotation, as shown in the following code snippet:

@Entity
data class User(
    @PrimaryKey val id: Int.valfirstName: String? .vallastName: String? .@Ignore val picture: Bitmap?)
Copy the code

In this case, a class inherits from its parent, which is usually easier to ignore using the ignoredColumns attribute of @entity, as shown in the following code snippet:

open class User {
    var picture: Bitmap? = null
}

@Entity(ignoredColumns = arrayOf("picture"))
data class RemoteUser(
    @PrimaryKey val id: Int.val hasVpn: Boolean
) : User()
Copy the code

Define relationships between objects

Because SQLite is a relational database, you can define relationships between two objects. While most object-relational mapping libraries allow entity objects to refer to each other, Room explicitly forbade this. For the technical rationale behind this decision, see Understand Why Room Doesn’t Allow Object References

Define one-to-many relationships

Even if you can’t use direct relationships,Room still allows us to define entities for foreign key constraints.

For example, if another entity class calls Book, we can define the relationship between the User entity and it using the @ForeignKey annotation, as shown in the following code:

@Entity(foreignKeys = arrayOf(ForeignKey(
            entity = User::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("user_id"))))data class Book(
    @PrimaryKey val bookId: Int.valtitle: String? .@ColumnInfo(name = "user_id") val userId: Int
)
Copy the code

Foreign keys are powerful because they allow us to specify what happens when the referenced entity class is updated. For example, if you want to tell SQLite to delete all of user’s books, you can delete the corresponding User instance by including onDelete = CASCADE in the annotation @foreignKey.

⚠️ Note :SQLite handles @INSERT (onConflict = REPLACE) as a set of REMOVE and REPLACE operations, not as a single UPDATE operation. This way of replacing conflicting values can affect foreign key constraints.

Create nested objects

Sometimes we want to represent an entity or plain old Java object (POJO) as a combination of database logic, that is, an object that contains multiple fields. In these cases, we can use the @embedded annotation to indicate the object to be decomposed into a table neutron field. We can then query the embedded field just as we would any other single column.

For example, the user class can contain a field type of address that represents the street, city, country, and zip code that make up the fields named. The store is listed separately in a table, including an address field with @Embedded user-annotated classes, as shown in the following code snippet:

data class Address(
    valstreet: String? .valstate: String? .valcity: String? .@ColumnInfo(name = "post_code") val postCode: Int
)

@Entity
data class User(
    @PrimaryKey val id: Int.valfirstName: String? .@Embedded val address: Address?
)
Copy the code

⚠️ Note: Embedded fields can also be embedded in other fields

Define many-to-many relationships

There is another kind of relationship that you often think of in a relational database model: a many-to-many relationship between two entities, and each entity can be associated with zero or more instances of the others. For example, consider a music streaming app where users can add their favorite songs to a playlist. Each playlist can have any number of songs, and each song can contain any number of playlists

With this relational model, you will need to create three objects:

An entity class for a playlist.

An entity class for a song.

A scheduling class controls information about which songs are in which lists.

You can define entity classes separately

@Entity
data class Playlist(
    @PrimaryKey var id: Int.valname: String? .val description: String?
)

@Entity
data class Song(
    @PrimaryKey var id: Int.valsongName: String? .val artistName: String?
)
Copy the code

The middle class is then defined as an entity containing foreign key references to songs and playlists

@Entity(tableName = "playlist_song_join",
        primaryKeys = arrayOf("playlistId"."songId"),
        foreignKeys = arrayOf(
                         ForeignKey(entity = Playlist::class.parentColumns = arrayOf("id"),
                                    childColumns = arrayOf("playlistId")),
                         ForeignKey(entity = Song::class.parentColumns = arrayOf("id"),
                                    childColumns = arrayOf("songId"))))data class PlaylistSongJoin(
    val playlistId: Int.val songId: Int
)
Copy the code

This produces a many-to-many relationship model that allows you to query playlist songs and playlist songs using a DAO:

@Dao
interface PlaylistSongJoinDao {
    @Insert
    fun insert(playlistSongJoin: PlaylistSongJoin)

    @Query(""" SELECT * FROM playlist INNER JOIN playlist_song_join ON playlist.id=playlist_song_join.playlistId WHERE playlist_song_join.songId=:songId """)
    fun getPlaylistsForSong(songId: Int): Array<Playlist>

    @Query(""" SELECT * FROM song INNER JOIN playlist_song_join ON song.id=playlist_song_join.songId WHERE playlist_song_join.playlistId=:playlistId """)
    fun getSongsForPlaylist(playlistId: Int): Array<Song>
}
Copy the code

Use the Room DAO to access data

To access the application’s data using the Room database, we need to use a data access object or A DAO. This set of Dao objects is the main component that makes up Room, because each Dao contains abstract methods to access our application database.

⚠️ Note: Room does not support database access on the main thread because it may lock the UI for a long time unless we call RoomDatabase.Builder’s allowMainThreadQueries().

Asynchronous queries – Queries that return instances of LiveData or Flowable are not affected by this rule because they run asynchronous queries on background threads when needed.

Daos can be interfaces or abstract classes. If it is an abstract class, it can choose to use a constructor that takes RoomDatabase as its only argument. Room creates the implementation of each DAO at compile time

Define convenient methods
Insert

When you create a DAO method and annotation @INSERT,Room generates an implementation that inserts all parameters into the database in a single transaction.

@Dao
interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg users: User)

    @Insert
    fun insertBothUsers(user1: User, user2: User)

    @Insert
    fun insertUsersAndFriends(user: User, friends: List<User>)
}
Copy the code

If the @Insert method accepts only one argument, it can return a long, which is the new rowId for inserting items, or long[] or List< long > instead if the argument is an array or collection.

UpDate

The Update method makes it easy to modify a set of parameterized entities in the database, using a query that matches the primary key of each entity.

@Dao
interface MyDao {
    @Update
    fun updateUsers(vararg users: User)
}
Copy the code

Although it is usually not necessary, we can have this method return an int that represents the number of rows updated in the database.

Delete

The Delete method makes it easy to remove a set of parameterized entities from the database. It uses the primary key to find the entity to delete.

@Dao
interface MyDao {
    @Delete
    fun deleteUsers(vararg users: User)
}
Copy the code

Although it is usually not necessary, you can have this method return an int instead, representing the number of rows removed from the database

Query

@Query is the main annotation used in the DAO class, which allows us to perform read/write operations on the database. Each @Query method is validated at compile time, so if there is a problem with the Query, a compile error occurs rather than a runtime failure.

Room also validates the return value of the query, and if the field name in the returned object does not match the corresponding column name in the query response, Room alerts you in one of two ways:

  • It warns if only a few field names match.
  • If there is no field name match, an error occurs.
A simple query
@Dao
interface MyDao {
    @Query("SELECT * FROM user")
    fun loadAllUsers(a): Array<User>
}
Copy the code

This is a very simple query that loads all users. At compile time,Room knows to query all columns in the user table. If the query contains a syntax error, or if the user table does not exist in the database, Room displays an error message when the application is properly compiled.

Transfer parameter query

In most cases, we need to pass parameters to the query to perform filtering operations, such as showing only users older than a certain age. To do this, we need to use the method parameter in the Room annotation, as shown in the following code snippet:

@Dao
interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    fun loadAllUsersOlderThan(minAge: Int): Array<User>
}
Copy the code

When this query is processed at compile time, Room matches the :minAge binding parameter with the minAge method parameter, and Room performs the match using the parameter name. If there is a mismatch, an error will occur during application compilation.

You can also pass multiple parameters or reference them multiple times in a query, as shown in the following code snippet:

@Dao
interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

    @Query("SELECT * FROM user WHERE first_name LIKE :search " +
           "OR last_name LIKE :search")
    fun findUserWithName(search: String): List<User>
}
Copy the code
Returns a subset of the columns

Most of the time, you only need a few fields in an entity. For example, the UI might display only the user’s first and last name, not the details of each user. By fetching only the columns displayed in the application UI, you can save valuable resources and complete queries more quickly.

Room allows any Java-based object to be returned from the query result, as long as the collection of result columns can be mapped to the returned object. For example, we could create the following plain Old Java-based object (POJO) to get the user’s first and last name:

data class NameTuple(
    @ColumnInfo(name = "first_name") valfirstName: String? .@ColumnInfo(name = "last_name") val lastName: String?
)
Copy the code

Now you can use this POJO method in a query:

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    fun loadFullName(a): List<NameTuple>
}
Copy the code

Room understands that the query returns the values of the first_name and last_name columns, and that these values can be mapped to the fields of the NameTuple class. Therefore, Room can generate the correct code. Room displays a warning if the query returns too many columns or columns that do not exist in the NameTuple class.

⚠️ Note: These POJOs can also be annotated with @embedded.

Pass a set of parameters

Some queries may require a variable number of arguments to be passed in without knowing the exact number of arguments before running. For example, we might want to retrieve information about all users from a subset of a zone. Room knows when a parameter represents a collection and automatically extends it at run time based on the number of parameters provided.

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
}
Copy the code
Observable queries

When executing queries, we typically expect the APPLICATION’s UI to update automatically as the data changes. To do this, return values of type LiveData can be used in the query method description, and Room generates all the necessary code to update LiveData when the database is updated.

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
}
Copy the code

⚠️ Note: Starting with version 1.0, the tables query that Room uses for table access determines whether to update instances of LiveData

Use RxJava for reactive queries

Room can also return RxJava2 Publisher and Flowable objects from the query we defined.

Room provides the following return values that support RxJava2:

  • The @query method: Room supports the return value types: Publisher, Flowable, and Observable

  • @insert, @update, and @delete etc. : Room 2.1.0 and later support the return value types: Completable, Single, and Maybe.

To use this feature, add the latest version of rxjava2 components to your application. App /build.gradle adds the following code

dependencies {
    def room_version = "2.1.0."
    implementation 'androidx.room:room-rxjava2:$room_version'
}
Copy the code

The following code snippet demonstrates some examples of how to use these return types:

@Dao
interface MyDao {
    @Query("SELECT * from user where id = :id LIMIT 1")
    fun loadUserById(id: Int): Flowable<User>

    // Emits the number of users added to the database.
    @Insert
    fun insertLargeNumberOfUsers(users: List<User>): Maybe<Int>

    // Makes sure that the operation finishes successfully.
    @Insert
    fun insertLargeNumberOfUsers(varargs users: User): Completable

    /* Emits the number of users removed from the database. Always emits at least one user. */
    @Delete
    fun deleteAllUsers(users: List<User>): Single<Int>}Copy the code

For more details, see the Google Developers Room and RxJava article.

Vernier caliper query

If your application’s logic requires direct access to the return line, you can return a cursor object from your query, as shown in the following code snippet:

@Dao
interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    fun loadRawUsersOlderThan(minAge: Int): Cursor
}
Copy the code

⚠️ Note: Google strongly discourels the use of the Cursor API because it does not guarantee the existence of rows or the values they contain. Use this feature only if we already have code that requires a cursor and cannot easily refactor it.

Query multiple tables

Some queries may require access to more than one table to compute the results. Room allows you to write any query, so we can join tables as well. In addition, if the response is of an observable data type, such as LiveData or Flowable, Room monitors all invalid tables referenced in the query.

The following code snippet shows how to perform a table join information table containing user borrowed books and a table containing data book currently rented

@Dao
interface MyDao {
    @Query(
        "SELECT user.name AS userName, pet.name AS petName " +
        "FROM user, pet " +
        "WHERE user.id = pet.user_id"
    )
    fun loadUserAndPetNames(a): LiveData<List<UserPet>>

    // You can also define this class in a separate file.
    data class UserPet(valuserName: String? .val petName: String?)
}
Copy the code

Write asynchronous methods through Kotlin’s coroutines

You can make your DAO methods use kotlin’s coroutine asynchrony by adding kotlin’s keyword suspend to your DAO methods, which will ensure that they cannot be executed on the main thread

@Dao
interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Update
    suspend fun updateUsers(vararg users: User)

    @Delete
    suspend fun deleteUsers(vararg users: User)

    @Query("SELECT * FROM user")
    suspend fun loadAllUsers(a): Array<User>
}
Copy the code

⚠️ Note: Using Kotlin assistance in Room requires Room2.1.0 and above, Kotlin version 1.3.0, coroutine 1.0.0 or higher. For more information, see Literal Dependencies.

Migrating and testing databases are not explained in this article. For more information, please visit migrating and testing databases (