Getting started with Android's most useful Room

Article Directory

1. Introduction and introduction

Andorid officially recommends Room instead of SQlite, so the third party framework greenDao previously used is directly discarded in the new project.

Room consists of three parts and is marked with three annotations:
Entity: This annotation represents the entity class, which represents the table in the database. Each entity class is a table.
Dao: The annotation is an interface. Encapsulated in the interface is the method of operating the database, such as adding, deleting, modifying and checking
database: This annotation marks a data holder, which is an abstract class and holds an abstract method of the interface dao, which also needs to be in this abstract class Add the identifiers of all entities in, and he needs to inherit RoomDatabase. In fact, the principle is that in the compilation stage, according to the annotations, the specific implementation class will be compiled, so it is much simpler for us, because all the code is generated by the compiler according to the rules.
So we need to add in gradle:

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

Need to rely on:

implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
implementation "androidx.room:room-ktx:2.3.0"

The purpose of adding kapt is to compile annotations. kapt is an annotation processing plugin

2. Application

1. Use annotation entity to define entity class

such as:

@Entity(tableName = "student")
data class Student(
    val name: String,
    val age:String,
) {
    @PrimaryKey(autoGenerate = true)
    var id:Long = 0L
}

The Entity annotation marks this class as a table. You can customize the tableName to indicate the tableName. If you don’t specify it, the default is the name of the class. A primary key must be specified in a table. If you do not specify a primary key, a compilation error will be reported. You can use the annotation @PrimaryKey to specify an attribute. The primary key, where autoGenerate means self-increment. If the primary key is not marked on the attribute, you can specify it above @Entity, for example, specify the name as the primary key:

@Entity(tableName = "student", primaryKeys = ["name"])

You can also specify a joint primary key:

@Entity(tableName = "student", primaryKeys = ["name", "id"])

The Entity annotation has several other values, but they are not very commonly used

Index[] indices() default {}; 索引,可以为表添加索引
boolean inheritSuperIndices() default false; 表示的是父类的索引是否可以被当前的类继承
ForeignKey[] foreignKeys() default {}; 需要依赖的外键,现在基本上没人会用外键
,都是根据逻辑关系控制具体的数据
String[] ignoredColumns() default {}; 可以忽略的字段

When the amount of data is large, you can add an index for a column or multiple columns for quick search, but it should not be used in general, mainly because the Android side generally does not store a particularly large amount of data

@Entity(tableName = "student", 
indices = [Index("name"), Index(value = ["name", "age"])])

There is also a foreign key, which is not very useful, and generally no one uses it. Just understand it:

@Entity(tableName = "student",
    foreignKeys = [ForeignKey(entity = ClassRoom::class,parentColumns = ["id"],
        childColumns = ["classRoomId"])])

parentColumns refers to the primary key of the dependent table, childColumns the id of the dependent table in the current table

By default, the attribute values ​​in the Entity-labeled class are all a column in the table. If a column does not need to be stored in the table, it can be ignored with ignoredColumns, but it is generally ignored with a separate annotation, such as:

@Ignore
val classRoomId:String

If the column does not want to use the default attribute name, you can use @ColumnInfo to specify:

@ColumnInfo(name = "age")
val age:String,

There are several attributes available for the annotation of ColumnInfo:

String defaultValue() default VALUE_UNSPECIFIED; 设置默认值
boolean index() default false; 是不是索引列
@Collate int collate() default UNSPECIFIED; 列的排列顺序

Set the default value may be used, the other two are basically not used

2. Define Dao, which is used to manipulate data and perform addition, deletion, modification, and check

The defined dao is an interface, marked with Dao, for example to find all students:

@Dao
interface StudentDao {
    @Query("select * from student")
    fun findAll():List<Student>
}

If you query based on conditions, you can pass the value over and write a where statement such as:

@Query("select * from student where id = :id ")
fun findById(id:Long): Student

: The following parameters are the parameters in the method. You can pass as many as you need.
In fact, just write sql on such a method. It's still very simple. For example, find the total number of data:

@Query("select count(*) from student")
fun findCount():Int

Is it very simple? Of course, if you want to jointly query certain tables and just keep certain fields, then you need to specify the column names and correspond to the attributes of the entity class you return, such as:

@Query("select s.name as studentName, r.class_name as roomName from student as s  left join class_room as r on r.class_id = s.roomId")
fun getAllName():List<StudentRoom>
data class StudentRoom(val studentName:String,val roomName:String) {}

Insert is also very simple, just use @Insert to annotate

@Insert
fun insertStudent(student: Student)

If you want to determine whether the insertion is successful, you can add a return parameter. The id you inserted is returned.
Of course, you can insert in batches. You can use variable parameters or list. The returned result is also a list, which means each insertion. After the successful ID
is actually compiled, you can click in to see the source code:

@Override
public long insertStudent(final Student student) {
  __db.assertNotSuspendingTransaction();
  __db.beginTransaction();
  try {
    long _result = __insertionAdapterOfStudent.insertAndReturnId(student);
    __db.setTransactionSuccessful();
    return _result;
  } finally {
    __db.endTransaction();
  }
}

Obviously return ID
can also use variable parameters, kotlin is vararg for variable parameters

To update and delete, you can do as follows:

@Update
fun updateStudent(student: Student)
@Delete
fun deleteStudent(student: Student)

They are all updated and deleted based on the primary key, but there is a problem here. If you update, all the data of this primary key row will be updated, so sometimes if you only need to update a certain field, you need to use another method. Use sql statement For
example, I just want to update the student's name:

@Query("update student set name = :name where id = :id")
fun updateNameForStudent(name:String,id:Long)

In the same way, delete can actually write SQL statements

3. Define the database

With tables and operation methods, there is no database, so we need to define a database, specify which tables and operation methods in the database are in the database, need an abstract method, and inherit RoomDatabase, as follows:

@Database(entities = [Student::class, ClassRoom::class], version = 1)
abstract class AppDatabase : RoomDatabase(){

    abstract fun studentDao(): StudentDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase?= null
        fun getInstance(context: Context): AppDatabase =
            INSTANCE?: synchronized(this){
                INSTANCE?:buildDatabase(context).also {
                    INSTANCE = it
                }
            }

        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
                .allowMainThreadQueries()
                .build()
    }
}

Entities specify all entity classes, version specifies the version of the current database, and there is an abstract method in it, the return value is your dao, that's it, the rest of the compiled plugin will be implemented for you automatically. The next step is to create a database. The method to create a book database is this:

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
                .allowMainThreadQueries()
                .build()

Specify your database abstract class and the name of the database, and create it. You can execute this method wherever the database is used, but usually we will write it in the database and transform it into a singleton mode. This is to avoid multiple Data reference, to avoid waste of resources, here is basically the end, the rest is to call, .allowMainThreadQueries() This method is called in the main thread, if it is not added, an error will be reported when called in the main thread. For example, insert a student:

AppDatabase.getInstance(this).studentDao()
.insertStudent(Student("Marry","24",1))

4. Upgrade and downgrade of database

Table increase and table modification are inevitable, so table upgrade is needed at this time. For
example, if I add an address table, I
should add the entity class address and upgrade the version number.

@Database(entities = [Student::class, ClassRoom::class, Address::class],
 version = 2)

If this is the case, then the app will hang, and some other processing needs to be added. Sometimes when upgrading, it may be necessary to delete some data and migrate the data, etc. There is also a method:

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
    .addMigrations(MIGRATION_1_2)
    .allowMainThreadQueries()
    .build()
    
     private val MIGRATION_1_2 = object : Migration(1,2){
            override fun migrate(database: SupportSQLiteDatabase) {
            }
        }

Every time you upgrade, you need to add this method, such as 2 to 3

  private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                .allowMainThreadQueries()
                .build()


        private val MIGRATION_1_2 = object : Migration(1,2){
            override fun migrate(database: SupportSQLiteDatabase) {
             
            }
        }

        private val MIGRATION_2_3 = object : Migration(2,3){
            override fun migrate(database: SupportSQLiteDatabase) {
            }
        }

The upgrade rule is that there must be a corresponding method for each level up. If you directly upgrade the first version to the third version, the default will first upgrade to 2 and then upgrade to 3.

Another issue that needs special attention is that if you add a new table, you not only need to specify the entities in the database class, but you also need to write SQL to create the table when you upgrade, otherwise an error will be reported.
For example, I want to add an address table,

@Database(entities = [Student::class,
 ClassRoom::class, Address::class], version = 2)
 private val MIGRATION_1_2 = object : Migration(1,2){
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE IF NOT EXISTS `address` (`addressId` INTEGER NOT NULL, `addressName` TEXT NOT NULL, PRIMARY KEY(`addressId`))")
    }
}

In this way, don't write this sql statement yourself, go to the copy of the created statement that you automatically generated, otherwise, if you write it by yourself that is inconsistent with the automatically generated create statement, you will get an error.
The create statement generated by default is in your database_impl class

public void createAllTables(SupportSQLiteDatabase _db) {
  _db.execSQL("CREATE TABLE IF NOT EXISTS `student` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `age` TEXT NOT NULL, `roomId` INTEGER NOT NULL)");
  _db.execSQL("CREATE TABLE IF NOT EXISTS `class_room` (`class_id` INTEGER NOT NULL, `class_name` TEXT NOT NULL, PRIMARY KEY(`class_id`))");
  _db.execSQL("CREATE TABLE IF NOT EXISTS `address` (`addressId` INTEGER NOT NULL, `addressName` TEXT NOT NULL, PRIMARY KEY(`addressId`))");
  _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
  _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7cbdd6263025181ec070edd36e1118eb')");

In addition, when upgrading and adding fields, do not forget to add default values, otherwise there will be problems.

If the database needs to be downgraded, you need to add this.fallbackToDestructiveMigration(), by default all tables are deleted and re-created, of course all data is gone

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hello_wold.db")
    .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
    .fallbackToDestructiveMigration()
    .allowMainThreadQueries()
    .build()

5. Table Association

Room is also a relational database, so there can be a one-to-one relationship, one-to-many relationship, and many-to-many relationship between tables and tables. One-to-one relationship,
for
example, a student corresponds to a classroom: according to the student to find out the corresponding classroom:

Student table:

@Entity(tableName = "student")
data class Student(
    val name: String,
    val age:String,
    val roomId:Long
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id:Long = 0L
}

Classroom table:

@Entity(tableName = "class_room")
data class ClassRoom(@PrimaryKey val class_id:Long, var class_name:String) {}

Query the corresponding relational table:

data class StudentRoom(
    @Embedded val student: Student,
    @Relation(
       parentColumn = "roomId",
        entityColumn = "class_id"
    )
    val classRoom: ClassRoom
) {}

Corresponding query statement:

@Query("select * from student")
fun getAllStudent():List<StudentRoom>

In many-to-one terms, one classroom corresponds to multiple students, which is relatively simple:

data class StudentRoom(
    @Embedded val student: Student,
    @Relation(
       parentColumn = "roomId",
        entityColumn = "class_id"
    )
    val classRoom: ClassRoom
) {}

Check for phrases:

@Query("select * from class_room")
fun getAllRoom():List<RoomStudent>

There is also a many-to-many relationship. I won’t look at it for the time being and I won’t need it.

Three. Other techniques that may be used

TypeConverter

Sometimes some data in the database cannot be stored. Compared to my Student, every student has some friends and many friends. I just want to store his name, which is a list:

data class Student(
    val name: String,
    val age:String,
    val roomId:Long,
    val friend:List<String>
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id:Long = 0L
}

An error will definitely be reported at this time, because the database does not understand what your list is, so we can convert it, for example, to the string type, so there is an annotation TypeConverter:

class MyConverters {

    @TypeConverter
    fun listToString(value:List<String>):String{
        val sb = StringBuilder()
        value.forEach {
            sb.append(",").append(it)
        }
        return sb.toString().substring(1)
    }

    @TypeConverter
    fun stringToList(value:String):List<String>{
        return value.split(",")
    }
}

Define a class. There are two methods in the class. One is to convert to string and the other is to convert to list. These two must appear in pairs for automatic conversion, and then mark the entity class:

@Entity(tableName = "student")
@TypeConverters(MyConverters::class)
data class Student(
    val name: String,
    val age:String,
    val roomId:Long,
    val friend:List<String>
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id:Long = 0L
}

The system will automatically convert list and string, such as the code generated in the insert statement:

_tmp = __myConverters.listToString(value.getFriend());
 public void bind(SupportSQLiteStatement stmt, Student value) {
    stmt.bindLong(1, value.getId());
    if (value.getName() == null) {
      stmt.bindNull(2);
    } else {
      stmt.bindString(2, value.getName());
    }
    if (value.getAge() == null) {
      stmt.bindNull(3);
    } else {
      stmt.bindString(3, value.getAge());
    }
    stmt.bindLong(4, value.getRoomId());
    final String _tmp;
    _tmp = __myConverters.listToString(value.getFriend());
    if (_tmp == null) {
      stmt.bindNull(5);
    } else {
      stmt.bindString(5, _tmp);
    }
  }
};

Embedded

There is also an annotation that may also use @Embedded, which means nested objects. For
example, in Student, I use @Embedded to nest an entity class, which is a normal class.

@Entity(tableName = "student")
data class Student(
    val name: String,
    val age:String,
    val roomId:Long,
    @Embedded val test: Test
) {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    var id:Long = 0L
}

data class Test(val testName:String) {
}

The fields of the table in test will be created when the student table is created, so the column of testName will be added to the student.

rxjava2

You can also use rxjava to perform asynchronous operations, and you need to introduce dependencies

implementation "androidx.room:room-rxjava2:2.3.0"

such as:

@Query("select * from student")
fun getAllStudent():Observable<List<StudentRoom>>

supplement

The contentProvider in Andorid is one of the four main components that are very commonly used to provide data. After we use the room, how do we correspond to the query, delete, and modify in the contentProvider?

 AppDatabase.getInstance(context!!.applicationContext).openHelper
            .writableDatabase.query(SupportSQLiteQueryBuilder
                .builder("student")
                .selection(selection, selectionArgs)
                .columns(projection).orderBy(sortOrder).create())

The SupportSQLiteQueryBuilder is used to create how to delete and modify statements to receive the passed parameters. Isn’t it simple?
Another example is update:

 AppDatabase.getInstance(context!!.applicationContext).openHelper.writableDatabase.update("student", SQLiteDatabase.CONFLICT_ROLLBACK,values, selection, selectionArgs )