Android Jetpack architecture component - Room in pit details

Keywords: Android Database Java SQL

This article starts with WeChat public's "Android development tour". Welcome to pay more attention to get more dry goods.

Room is a member of Jetpack component library, belonging to ORM library. It mainly makes a layer of abstraction for Sqlite, so as to simplify the operation of database by developers. Room supports syntax checking at compile time and supports returning LiveData.

Add dependency

Add the following dependencies to build.gradle of app:

def room_version = "2.2.0-rc01"
    
implementation "androidx.room:room-runtime:$room_version"
// For kotlin use kapt instead of annotation processor
kapt "androidx.room:room-compiler:$room_version"

If the project is developed in Kotlin language, kapt keyword is used when adding room compiler, and annotation processor is used for java language development. Failure to do so will result in access errors.

Important concepts

To use Room, you must understand three basic concepts:

  • Entity: entity class, which corresponds to a table structure of the database. Annotation @ entity tag is required.
  • Dao: contains a series of methods to access the database. Annotation @ Dao tag is required.
  • Database: database holder, as the main access point for the underlying connection of application persistence related data. Annotation @ database tag is required.
    Using @ Database annotation requires the following conditions:
    The defined class must be an abstract class that inherits from RoomDatabase.
    In the annotation, you need to define the list of entity classes associated with the database.
    Contains an abstract method without parameters and returns @ Dao with annotations.

The corresponding relationship between the three and the application is as follows:

Entity

Classes defined with the @ Entity annotation are mapped to a table in the database. The class name of the default Entity class is table name, and the field name is table name. Of course, we can also modify it.

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true) var userId: Long,
    @ColumnInfo(name = "user_name")var userName: String,
    @ColumnInfo(defaultValue = "china") var address: String,
    @Ignore var sex: Boolean
)

For example, here we define a User class. Here @ Entity, @ PrimaryKey, @ ColumnInfo and @ Ignore are used

In the @ Entity annotation, we passed in a parameter tableName to specify the name of the table. @The PrimaryKey annotation is used to annotate the primary key of the table, and autoGenerate = true is used to specify the self growth of the primary key. @The ColumnInfo annotation is used to mark the information of the corresponding columns of the table, such as table name, default value, etc. @Ignore annotation is to ignore this field as the name implies. Fields using this annotation will not generate corresponding column information in the database. You can also use the ignoredColumns parameter in the @ Entity annotation to specify. The effect is the same.

In addition to the parameter fields used above, there are other parameters for annotation. Next, I'll take a look at the relevant source code to understand other parameters.

Entity comment:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Entity {

    String tableName() default "";

    Index[] indices() default {};

    boolean inheritSuperIndices() default false;

    String[] primaryKeys() default {};

    String[] ignoredColumns() default {};
}

PrimaryKey note:

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface PrimaryKey {
  
    boolean autoGenerate() default false;
}

ColumnInfo comment:

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface ColumnInfo {
   
    String name() default INHERIT_FIELD_NAME;

    @SuppressWarnings("unused") @SQLiteTypeAffinity int typeAffinity() default UNDEFINED;

    boolean index() default false;

    @Collate int collate() default UNSPECIFIED;

    String defaultValue() default VALUE_UNSPECIFIED;
   
    String INHERIT_FIELD_NAME = "[field-name]";
 
    int UNDEFINED = 1;
    int TEXT = 2;
    int INTEGER = 3;
    int REAL = 4;
   
    int BLOB = 5;

    @IntDef({UNDEFINED, TEXT, INTEGER, REAL, BLOB})
    @Retention(RetentionPolicy.CLASS)
    @interface SQLiteTypeAffinity {
    }

    int UNSPECIFIED = 1;
    int BINARY = 2;
    int NOCASE = 3;
    int RTRIM = 4;
       
    @RequiresApi(21)
    int LOCALIZED = 5;
    
    @RequiresApi(21)
    int UNICODE = 6;

    @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM, LOCALIZED, UNICODE})
    @Retention(RetentionPolicy.CLASS)
    @interface Collate {
    }

    String VALUE_UNSPECIFIED = "[value-unspecified]";
}

Ignore note:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface Ignore {
}

Dao

The Dao class is an interface that defines a series of methods for manipulating the database. Usually we operate the database just by adding, deleting, modifying and checking. Room also provides related annotations for us, including @ Insert, @ Delete, @ Update and @ Query.

@query

Let's first look at @ Query query Query annotation. Its parameters are of String type. We write SQL statements directly for execution, and we can check the syntax when compiling. For example, we can Query the information of a user according to the ID:

@Query("select * from user where userId = :id")
fun getUserById(id: Long): User

The parameters passed by SQL statement reference are directly referenced by: symbol.

@Insert

If we need to Insert a piece of data into the table, we can directly define a method and annotate it with @ Insert annotation:

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addUser(user: User)

We can see that there is a parameter onConflict directly, which represents the processing logic when the inserted data already exists. There are three operation logic: REPLACE, ABORT and IGNORE. If not specified, ABORT terminates inserting data by default. Here we specify it as REPLACE to REPLACE the original data.

@Delete

If you need to Delete the data of the table, use the @ Delete annotation:

@Delete
fun deleteUserByUser(user: User)

Use the primary key to find the entity to delete.

@Update

If you need to modify a piece of data, use the @ Update annotation. Like @ Delete, you can also find the entity to be deleted according to the primary key.

@Update
fun updateUserByUser(user: User)

The parameters accepted by the above @ Query query are strings, so we can also use the @ Query annotation to use SQL statements for direct execution like deletion or update. For example, Query a user by userid or update a user's name by userid:

@Query("delete  from user where userId = :id ")
fun deleteUserById(id:Long)

@Query("update  user set userName = :updateName where userID =  :id")
fun update(id: Long, updateName: String)

The effect is exactly the same.

Database

First, define an abstract class to inherit the RoomDatabase class, and add the annotation @ Database to identify:

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

    abstract fun userDao(): UserDao

    companion object {
        private var instance: AppDatabase? = null
        fun getInstance(context: Context): AppDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "user.db" //Database name
                ).allowMainThreadQueries().build()
            }
            return instance as AppDatabase
        }
    }
}

Do you remember that Database needs to meet that requirement? This can be compared with the above.

Use entities to map related entity classes, and version to indicate the version number of the current Database. The singleton mode is used to return to the Database to prevent the memory waste caused by too many new instances. Room.databaseBuilder(context,klass,name).build() is used to create a Database. The first parameter is the context, the second parameter is the class bytecode file of the current Database, and the third parameter is the Database name.

Database cannot be called in the main thread by default. Because in most cases, operating the database is a relatively time-consuming action. Use allowMainThreadQueries for instructions if you need to call on the main thread.

Use

After all the above are defined, it is how to call. We use:

class RoomActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val userDao = AppDatabase.getInstance(this).userDao()

        //insert data
        for (i in (0 until 10)) {
            val user = User(userName = "Li Si $i", userPhone = "110$i")
            userDao.addUser(user)
        }

        //query all data
        userDao.getAllUsers().forEach {
            Log.e("room", "==query==${it.userId},${it.userName},${it.userPhone}")
        }

        //update data
        userDao.updateUserById(2, "Zhang San")
        val updateUser = userDao.getUserById(2)
        Log.e("room", "==update==${updateUser.userId},${updateUser.userName},${updateUser.userPhone}")
        
        //Delete data
        val row = userDao.deleteUserById(10)
        Log.e("room", "Deleted $row That's ok")

    }
}

Let's take a look at the data

10 data inserted
 room: ==query==1, Li Si 01100
 room: ==query==2, Li Si 11101
 room: ==query==3, Li Si 21102
 room: ==query==4, Li Si 31103
 room: ==query==5, Li Si 41104
 room: ==query==6, Li Si 51105
 room: ==query==7, Li Si 61106
 room: ==query==8, Li Si 71107
 room: ==query==9, Li Si 81108
 room: ==query==10, Li Si 91109

Updated the data with id 2
 room: ==update==2, Zhang San, 1101

Deleted data with id 10
 room: 1 Row Deleted

The generated database is in the data/data/packageName/databases directory.

The files related to Dao and Database will be generated into userdao and appdatabase impl files in the build directory after compilation. If you are interested, you can look at them yourself.

Database upgrade and demotion

When using the database, it is inevitable to update the database. To upgrade or downgrade a database, use the addMigrations method:

instance = Room.databaseBuilder(
            context.applicationContext,
            AppDatabase::class.java,
            "user.db"
        ).addMigrations(object :Migration(1,2){
            override fun migrate(database: SupportSQLiteDatabase) {
               database.execSQL("ALTER TABLE user ADD age INTEGER Default 0 not null ")
            }

        })allowMainThreadQueries().build()

Two parameters are required for Migration. startVersion indicates the version to be upgraded, and endVersion indicates the version to be upgraded. At the same time, you need to change the value of version in the @ Database annotation to be the same as endVersion.

For example, the above code upgrades the database from version 1 to version 2, and adds the column age to version 2:

The same is true for database degradation. Also using addMigrations is just startversion > endversion.

When the version does not match during upgrade or downgrade, exceptions will be thrown directly by default. Of course, we can also handle exceptions.

When upgrading, you can add the fallbackToDestructiveMigration method. When the version is not matched, the table will be directly deleted and recreated.

The fallbackToDestructiveMigrationOnDowngrade method is added during degradation. When it does not match the version, the table will be directly deleted and recreated.

Recommended reading

Don't know what Android Jetpack is yet? You are out.

Android Jetpack architecture component - Lifecycle in pit Guide

Android Jetpack architecture component - details of LiveData and ViewModel

Scan below the two-dimensional code to pay attention to the public number and get more dry goods.

Posted by lordtrini on Wed, 27 Nov 2019 04:14:07 -0800