Room is an abstraction layer provided on TOP of SQLite. In order to take full advantage of SQLite’s powerful features, while also more robust access to the database. Room can be used to cache data. When the device cannot access the network, users can still browse the content offline. After the device is reconnected to the network, all content changes initiated by the user are synchronized to the server. Google strongly recommends using Room rather than SQLite directly

One: Add dependencies

Add the following code to your app’s build.gradle

def room_version = "2.2.6." "
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
Copy the code

Two: Room structure

Room consists of 3 main components:

  • DataBase: Contains the DataBase holder and serves as the primary access point to the underlying connection to apply retained persistent relational data. Here’s a simple example:
    @Database(entities = arrayOf(User::class), version = 1)
    abstract class AppDatabase : RoomDatabase() {
          abstract fun userDao(a): UserDao
    }
    Copy the code

    What to look out for in classes using the @database annotation:

    • Abstract Class AppDatabase: RoomDatabase()
    • In the @database data, add the list of entities associated with the Database. @Database(entities = arrayOf(User::class), version = 1)
    • Contains an abstract method with zero argument cuts that returns a class annotated with @DAO. abstract fun userDao(): UserDao
  • Entity: indicates a table in the database. A simple example is as follows:
    @Entity
    data class User(
        @PrimaryKey var uid: Int.@ColumnInfo(name = "first_name") var firstName: String?
    )
    Copy the code
  • DAO: Contains methods used to access the database. Here’s a simple example:
    @Dao
    interface UserDao {
       @Query("SELECT * FROM user")
       fun getAll(a): List<User>
    }
    Copy the code

The relationship between different components of Room is shown in the figure below:

3: Room example

There is a User class

@Entity
data class User(
    @PrimaryKey(autoGenerate = true)
    var uid:Int=1.varname:String? =""):Serializable
Copy the code
  • There are two attributes in User: uid, which is the primary key, and a name attribute

There is a UserDao interface

@Dao
interface UserDao {

    @Query("SELECT * FROM user")
    suspend fun getAll(a):List<User>

    @Query("SELECT * FROM user WHERE uid in (:userIds)" )
    suspend fun loadAllByIds(userIds:IntArray):List<User>

    @Query("SELECT * FROM user WHERE name LIKE :userName LIMIT 1")
    suspend fun findByName(userName:String):User

    @Query("SELECT * FROM user WHERE uid = :uid")
    suspend fun findByUid(uid:Int):User

    // Insert one item, return the rowId of the inserted item. Insert multiple items, return an array
    @Insert
    suspend fun insertAll(vararg users:User)

    // Can return int, return the number of deleted rows
    @Delete
    suspend fun delete(user:User)

    // You can return an int that returns the number of updated rows
    @Update
    suspend fun updateUser(user:User)

}
Copy the code
  • The UserDao defines some methods for adding, deleting, modifying, and querying a database. Subspend means that it is the methods of these database operations that need to be run in coroutines.

There is an AppDataBase abstract class

@Database(entities = arrayOf(User::class),version = 1)
abstract class AppDataBase :RoomDatabase() {abstract fun userData(a):UserDao
}
Copy the code
  • This class inherits RoomDatabase and declares entities to be User, database to be 1, and the abstract interface to return the UserDao

There’s a RoomManager, management class

object RoomManager {
    private var db: RoomDatabase? = null
    fun getDB(context: Application): AppDataBase{
        if (db == null) {
            db = Room.databaseBuilder(context, AppDataBase::class.java, "ccm_db").enableMultiInstanceInvalidation().build()
        }
        return db as AppDataBase
    }
}
Copy the code
  • RoomManager is a singleton that declares the getDB method, passed in to Application to generate the AppDataBase instance. \

Note: If your application is running in a single process, follow the singleton design pattern when instantiating AppDatabase objects. The cost of each RoomDatabase instance is quite high, and you rarely need to access multiple instances in a single process. If your applications run in multiple processes, please include in the database builder call enableMultiInstanceInvalidation (). This way, if you have an AppDatabase instance in each process, you can invalidate shared database files in one process, and this invalidation will automatically propagate to AppDatabase instances in other processes. \

  • By default, Room does not support accessing the database from the main thread. To access the database from the main thread, add the allowMainThreadQueries() method to the constructor
    db = Room.databaseBuilder(context, AppDataBase::class.java, "ccm_db").allowMainThreadQueries().build()
    Copy the code

    However, it is best not to access the database in the main thread because of the risk of ANR due to time consumption.

A RoomViewModel class

class RoomViewModel(val context:Application) :AndroidViewModel(context){
    var users = MutableLiveData<List<User>>()
    fun queryUsers(a){
        viewModelScope.launch{
            val list = withContext(Dispatchers.IO){
                RoomManager.getDB(context).userData().getAll()
            }
            users.value = list
        }
    }
    fun insertUser(user:User){
        viewModelScope.launch {
            withContext(Dispatchers.IO){
                RoomManager.getDB(context).userData().insertAll(user)
            }
        }
    }
}
Copy the code
  • When RoomViewModel queries database data, our RoomManager needs the Application class to generate AppDataBase, so our RoomViewModel needs to inherit AndroidViewModel
  • There is a LiveData to store the user
  • Two methods are defined, one is queryUsers. Query the User list of the database
  • One is insertUser that inserts into the user database
  • ViewModelScope uses coroutines to handle database requests

Next comes the use of activities

class RoomTestActivity :AppCompatActivity(),View.OnClickListener{

    lateinit var viewModel:RoomViewModel
    var index = 1

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_roomtest)
        viewModel = ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(RoomViewModel::class.java)
        viewModel.users.observe(this,
            Observer<List<User>> {
                tv_name.text = if(it? .size? :0>0) it[0].name else "Name"
            })
        btn_add.setOnClickListener(this)
        btn_query.setOnClickListener(this)}override fun onClick(v: View?). {
        when(v){
            btn_add->{
                viewModel.insertUser(User(index,"ccm"))
                index++
            }
            btn_query->{
                viewModel.queryUsers()
            }
        }
    }
}
Copy the code
  • The Activity has three controls: an insert button btn_add, a query button btn_query, and a text button to display the first user data
  • viewModel=ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(RoomViewModel::class.java) Use the ViewModelProvider to create the corresponding ViewModel
  • Viewmodel.users. Observe to display user data to the text control by observing changes in data

An activity_roomtest.xml file:

<? xml version="1.0" encoding="utf-8"? > <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Add user"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_name" />

    <Button
        android:id="@+id/btn_query"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Query user"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_add" />

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

The above is a simple use example of Room. If you do not understand the usage of ViewModel and LiveData, you can read a series of articles.

Use Room entities to define data

With Room, we can define an Entity class with the @Entity annotation. Each Entity class has a table. We can refer to the Entity class through the Entities array in the Database class. The following

// entities = arrayOf(User::class) references User entity class
@Database(entities = arrayOf(User::class),version = 1)
abstract class AppDataBase :RoomDatabase() {abstract fun userData(a):UserDao
}
// User entity class
@Entity
data class User(
    @PrimaryKey(autoGenerate = true)
    var uid:Int=1.varname:String? =""):Serializable
Copy the code

4.1 Using primary Keys

You can define the PrimaryKey with @primarykey, and you can specify self-growth with autoGenerate:

@Entity
data class User(
    @PrimaryKey(autoGenerate = true)
    var uid:Int=1.varname:String? =""):Serializable
Copy the code

You can also define a compound primary key using the primaryKeys value of @entity

// Define a compound primary key
@Entity(primaryKeys = arrayOf("firstName"."lastName"))
data class User(varfirstName: String? .var lastName: String?)
Copy the code

4.2 Customizing table names

By default, Room uses the class name as the tableName. We can define the tableName using the tableName property of @entity.

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

4.2 Customizing Column names

By default, Room uses the attribute name as the column name. You can customize the column name with @columnInfo.

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

4.3 Ignoring Fields

By default, Room generates the corresponding table column for each field in the entity class. If we have a field that we do not want to create columns, we can use the @ignore annotation to indicate that the field is ignored

@Entity
data class User(
   @PrimaryKey var id: Int.varfirstName: String? .varlastName: String? .@Ignore var picture: Bitmap?
)
Copy the code

If the Entity class inherits columns from the parent class, you want to ignore the columns of the parent class by declaring them using the ignoredColumns field of @entity

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

5. Use Room DAO to access data

Daos can define methods for manipulating databases.

5.1 Insert

@insert inserts data into the database

 @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 long, which is the rowId of the inserted item. If the argument is an array or collection, return long[] or List< long >.

The 5.2 Update

@update updates the contents of the database

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

Although it is usually not necessary, you can have this method return an int value indicating the number of rows updated in the database

5.3 the Delete

Deletes data from the database

@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 value indicating the number of rows removed from the database.

5.4 the Query

Query data from the database

@Dao
interface MyDao {
  @Query("SELECT * FROM user")
  fun loadAllUsers(a): List<User>
}
Copy the code

Conditional queries, such as queries for users younger than minAge

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE uid = :uid")
    suspend fun findByUid(uid:Int):User
}
Copy the code

In most cases, you only need to get a few fields of the entity. For example, if your interface might only display a user’s first and last name, rather than every detail of the user, we could query for specific fields to deliver query efficiency

data class NameTuple(
   @ColumnInfo(name = "first_name") varfirstName: String? .@ColumnInfo(name = "last_name") var lastName: String?
)
@Dao
interface MyDao {
   @Query("SELECT first_name, last_name FROM user")
   fun loadFullName(a): List<NameTuple>
}
Copy the code

Some of your queries may require you to pass in an indefinite number of parameters, the exact number of which is not known until run time. For example, you might want to retrieve information about all users from a section. You can use in

@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

Multi-table query

@Dao
interface MyDao {
        @Query(
            "SELECT * FROM book " +
            "INNER JOIN loan ON loan.book_id = book.id " +
            "INNER JOIN user ON user.id = loan.user_id " +
            "WHERE user.name LIKE :userName"
        )
        fun findBooksBorrowedByNameSync(userName: String): List<Book>
}
Copy the code

5.5 Querying the Return Type

Room supports a variety of return types for query methods, including special return types that interoperate with specific frameworks or apis. The following table shows the applicable return types by query type and framework:

Types of queries coroutines RxJava Guava The life cycle
Observable reading Flow<T> Flowable<T> Publisher<T> Observable<T> There is no LiveData<T>
A single reading suspend fun Single < T >, Maybe < T > ListenableFuture<T> There is no
A single write suspend fun Single<T>, Maybe<T>, Completable<T> ListenableFuture<T> There is no
5.5.1 Using Streams for Reactive Query

In Room 2.2 and later, you can use Kotlin’s Flow feature to keep your app’s interface up to date. If you need to update the interface automatically when the underlying data changes, write a query method that returns a Flow object:

@Query("SELECT * FROM User")
fun getAllUsers(a): Flow<List<User>>    
Copy the code

As soon as any data in the table changes, the returned Flow object triggers the query again and reissues the entire result set. Reactive queries using Flow have one important limitation: Whenever any row in the table is updated (whether or not it is in the result set), the Flow object rerun the query. Applying the distinctUntilChanged() operator to the returned Flow object ensures that the interface is notified only when the actual query result changes:

@Dao
abstract class UsersDao {
  @Query("SELECT * FROM User WHERE username = :username")
  abstract fun getUser(username: String): Flow<User>
  fun getUserDistinctUntilChanged(username:String)=getUser(username).distinctUntilChanged()
}
Copy the code

Note: To use Room with Flow, you need to include the Room-ktx artifact in the build.gradle file. For more information, see Declaring dependencies.

5.5.2 Using Kotlin coroutines for asynchronous query

You can add the suspend Kotlin keyword to YOUR DAO methods to make them asynchronous using the Kotlin coroutine functionality. This ensures that these methods are not 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: To use Room with the Kotlin coroutine, you need to use Room 2.1.0, Kotlin 1.3.0, and Cordoines 1.0.0 or higher. For more information, see Declaring dependencies. The suspend keyword also applies to DAO methods annotated with @Transaction that run in a single database Transaction.

@Dao
abstract class UsersDao {
    @Transaction
    open suspend fun setLoggedInUser(loggedInUser: User) {
        deleteUser(loggedInUser)
        insertUser(loggedInUser)
    }

    @Query("DELETE FROM users")
    abstract fun deleteUser(user: User)

    @Insert
    abstract suspend fun insertUser(user: User)
}
Copy the code
5.5.3 Use LiveData for observable queries

When performing queries, you can ensure that the application’s interface updates automatically when the data changes by using the return value of type LiveData, and when the database updates, Room generates all the code necessary to update the LiveData.

@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
5.5.4 Using RxJava for responsive Query

Room provides the following support for RxJava2 type return values:

  • The @query method: Room supports Publisher, Flowable, and Observable returns.
  • @INSERT, @update, and @delete methods: Room 2.1.0 and later support return values of the Completable, Single

    , and Maybe

    types.

If you want to use RxJava with room, you need app/build.gradle

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

Rxjava and Room

@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

Define relationships between objects

Since SQLite is a relational database, we can specify relationships between entities

6.1: Creating nested objects

We often have cases where a class contains a class. For example, the User class can contain a field of type Address, and Address has a city,street attribute. We want the Address class to be included in User, and the attributes in Address directly as User columns. You can use the @Embedde annotation. As follows:

data class Address( varstreet: String? .var city: String? )

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

The table representing the User object will contain columns with the following names: ID, firstName, Street, city.

6.2: Define one-to-one relationships

A one-to-one relationship between two entities is one in which each instance of the parent corresponds exactly to one instance of the child, and vice versa. For example, there is only one song library per user, and each song library corresponds to exactly one user. Therefore, there should be a one-to-one relationship between the User and Library entities.

@Entity
data class User(
    @PrimaryKey var userId: Long.var name: String,
    var age: Int
)

@Entity
data class Library(
    @PrimaryKey var libraryId: Long.var userOwnerId: Long
)
Copy the code

To query the list of users and the corresponding song library, you must first establish a one-to-one relationship between the two entities. To do this, create a new data class, UserAndLibrary

data class UserAndLibrary(
   @Embedded varuser: User? .@Relation(parentColumn = "userId",entityColumn = "userOwnerId")
   var library: Library?
)
Copy the code
  • The @relation annotation is added to the Library instance where parentColumn is the primary key name of User and entityColumn is the Library column name of User’s primary key.

Finally, you need to add a method to the Dao class that returns all instances of the data classes that User paired with the Library. Because this method requires Room to run the query twice, you should add the @Transaction annotation to this method to ensure that the entire operation is performed atomically

@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(a): List<UserAndLibrary>
Copy the code

6.3: Define a one-to-many relationship

A one-to-many relationship between two entities is one in which each instance of the parent entity corresponds to zero or more instances of the child entity, but each instance of the child entity corresponds to exactly one instance of the parent entity. For example, each user can create many playlists, and each playlist belongs to only one user. Therefore, there should be a one-to-many relationship between User entities and PlayList entities.

@Entity
data class User(
    @PrimaryKey var userId: Long.var name: String,
    var age: Int
)

@Entity
data class Playlist(
    @PrimaryKey val playlistId: Long.var userCreatorId: Long.var playListName:String
)
Copy the code

To query the User List and the corresponding PlayList, you must first establish a one-to-many relationship between the two entities. To do this, create a new data class UserWithPlaylists, containing a parent entity User, and a child entity List<PlayList>

data class UserWithPlaylists(
   @Embedded varuser: User? .@Relation(parentColumn = "userId",entityColumn = "userCreatorId")
   var playlists: List<Playlist>?
)
Copy the code
  • ParentColumn is the primary key name of the parent entity User, and entityColumn is the column name of the primary key of User in the Playlist of the child entity.

Finally, add a method to the DAO class that returns all instances of the data classes that pair the parent entity with the child entity. This method requires Room to run the query twice, so you should add the @Transaction annotation to this method to ensure that the entire operation is performed atomically.

@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(a): List<UserWithPlaylists>
Copy the code

6.4: Define many-to-many relationships

A many-to-many relationship between two entities refers to a relationship in which each instance of the parent corresponds to zero or more instances of the child, and vice versa. Example: Each playlist can contain multiple songs, and each song can be included in multiple different playlists. Therefore, there should be a many-to-many relationship between Playlist entities and Song entities. First, create a class for each of your entities. Many-to-many relationships differ from other relationship types in that there is usually no reference to the parent entity in the child entity. Therefore, a third class needs to be created to represent the associated entities (the cross-reference table) between the two entities. The cross-reference table must contain primary key columns for each entity in the many-to-many relationship represented in the table

@Entity
data class Playlist(
   @PrimaryKey var playlistId: Long.var playlistName: String
)

@Entity
data class Song(
   @PrimaryKey var songId: Long.var songName: String,
   var artist: String
)

@Entity(primaryKeys = ["playlistId"."songId"])
data class PlaylistSongCrossRef(
   var playlistId: Long.var songId: Long
)
Copy the code

The next step depends on how you want to query these related entities.

  • If you want to query a Playlist and a list of songs contained in each Playlist, you create a new data class that contains a single Playlist object and a list of all Song objects contained in that Playlist.
    data class PlaylistWithSongs(
          @Embedded valplaylist: Playlist? .@Relation(
               parentColumn = "playlistId",
               entityColumn = "songId",
               associateBy = @Junction(PlaylistSongCrossRef::class)
          )
          val songs: List<Song>?
    )
    Copy the code
  • If you want to query a list of songs and each Song’s Playlist, create a new data class that contains a single Song object and a list of all the Playlist objects that contain that Song.
    data class SongWithPlaylists(
          @Embedded val song: Song,
          @Relation(
               parentColumn = "songId",
               entityColumn = "playlistId",
               associateBy = @Junction(PlaylistSongCrossRef::class)
          )
          val playlists: List<Playlist>
     )
    Copy the code

Finally, add a method to the DAO class that provides the query functionality your application needs

  • GetPlaylistsWithSongs: This method queries the database and returns all of the PlaylistWithSongs objects queried.
  • GetSongsWithPlaylists: This method queries the database and returns all SongWithPlaylists objects queried.

Both of these methods require Room to run the query twice, so the @Transaction annotation should be added to both methods to ensure that the entire operation is performed atomically.

@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(a): List<PlaylistWithSongs>

@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(a): List<SongWithPlaylists>
Copy the code

Note: If the @relation annotation does not apply to your particular use case, you may need to manually define the appropriate relationship using the JOIN keyword in your SQL query.