This article is first published in the wechat public account “Android development journey”, welcome to follow, get more technical dry goods

Wan-android Jetpack VersionAndroid Jetpack architecture for componentized application developmentWelcome to star

Project address of Flutter wan-AndroidFlutter Wan – AndroidWelcome to star

Room is a member of the Jetpack component library, an ORM library that abstracts Sqlite to make it easier for developers to manipulate the database. Room supports compile-time syntax checking and returns LiveData.

Add the dependent

Add the following dependencies to your app’s build.gradle:

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

If the project is developed in Kotlin, use the kapt keyword when adding room-compiler, and Java uses the annotationProcessor key. Otherwise, an access error may occur.

Important concepts

To use Room, you must understand the three basic concepts:

  • Entity: An Entity class that corresponds to a table structure in a database. The annotation @entity token is required.
  • Dao: Contains access to a set of methods to access a database. The @DAO annotation is required.
  • Database: The Database holder that serves as the primary access point for the underlying connection to application persistence-related data. The annotation @database tag is required. To use the @Database annotation, the following conditions must be met: the class defined must be an abstract class inherited from RoomDatabase. The list of entity classes associated with the database needs to be defined in the annotation. Contains an abstract method with no arguments and returns an annotated @DAO.

The corresponding relationship among the three and their applications is shown as follows:

Entity

Classes defined using the @Entity annotation are mapped to a table in the database. The default entity class has the class name table name and the field name table name. Of course, we can also modify.

@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
)

Copy the code

For example, here we define a User class. @entity, @primaryKey, @columnInfo, and @Ignore are used

In the @entity annotation we pass a parameter tableName to specify the name of the table. The @primarykey annotation is used to annotate the PrimaryKey of the table, and autoGenerate = true is used to specify PrimaryKey self-growth. The @columnInfo annotation is used to annotate information about the corresponding column of a table, such as the table name, default values, and so on. The @ignore annotation, as its name implies, ignores this field, and a field using this annotation will not generate the corresponding column information in the database. You can also specify this using the ignoredColumns parameter in the @entity annotation, which has the same effect.

In addition to the parameter fields used above, annotations have other parameters. Let me take a look at the relevant source code to understand the other parameters.

The Entity annotation:

@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 {};
}

Copy the code

PrimaryKey comments:

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

ColumnInfo comments:

@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]";
}
Copy the code

Ignore comments:

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

Dao

The Dao class is an interface that defines a set of methods for manipulating the database. Usually we operate the database is nothing more than add, delete, change and check. Room also provides annotations for ours, such as @insert, @delete, @update, and @Query.

@query

Let’s look at the @query annotation. It takes a String as an argument, and we can write the SQL statement to execute it, and we can check the syntax at compile time. For example, we can query a user’s information by ID:

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

Arguments passed by SQL statement references are directly referenced using the: symbol.

@Insert

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

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

We can see that the onConflict argument in the direct is the processing logic when the inserted data already exists. There are three types of operation logic: REPLACE, ABORT, and IGNORE. If not specified, ABORT terminates data insertion by default. Here we specify it as REPLACE to REPLACE the original data.

@Delete

If you need to Delete data from a table, use the @delete annotation:

@Delete
fun deleteUserByUser(user: User)
Copy the code

Use the primary key to find the entity to delete.

@Update

If a piece of data needs to be modified, the @update annotation is used, which, like @delete, looks for the entity to be deleted based on the primary key.

@Update
fun updateUserByUser(user: User)
Copy the code

The @query Query takes a string, so we can use the @Query annotation to execute things like delete or update directly using SQL statements. 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)
Copy the code

The effect is exactly the same.

Database

Start by defining an abstract class that inherits the RoomDatabase class and adding the annotation @database to identify it:

@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
        }
    }
}
Copy the code

Remember that the Database needs to meet that requirement? Here is a comparison with the above.

Use entities to map the associated entity class and version to indicate the current database version number. The singleton pattern is used to return the Database to prevent wasting memory by creating too many new instances. Room.databasebuilder (context,klass,name).build() to create a Database, where 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. For the most part, manipulating a database is a time-consuming activity. If you need to call from the main thread, use allowMainThreadQueries.

use

With all of the above defined, it’s time to call. We use:

class RoomActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) val userDao = AppDatabase.getInstance(this).userDao() //insert datafor (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")
        val updateUser = userDao.getUserById(2)
        Log.e("room"."==update==${updateUser.userId}.${updateUser.userName}.${updateUser.userPhone}"Val row = userdao.deleteUserById (10) log.e ("room"."Deleted$rowLine")}}Copy the code

Let’s look at the data

10 insert data room: = = query = = 1, li si, 0110 room: = = query = = 2, li si 1110 1 room: = = query = = 3, li si 2110 2 room: = = query = = 4, li si 3110 3 room: = = query = = 5, 4110 4 room: li si = = query = = 6, li si 5 room: 5110 = = query = = 7, li si 6110 6 room: = = query = = 8, li si, 7110 room: Room: ==update==2, room: ==query==9, room: ==query==10, room: ==query==10Copy the code

Generated by the database in data/data/packageName/databases directory.

The Dao and Database related files will generate UserDao_Impl and AppDatabase_Impl files in the build directory after compilation, so you can check them out for yourself.

Database upgrade and downgrade

When using the database, it is inevitable to update the database. To upgrade or degrade 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()
Copy the code

Migration requires two parameters: startVersion indicates the version to be upgraded, and endVersion indicates the version to be upgraded. You also need to change the version value in the @database annotation to be the same as endVersion.

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

The same is true for database degradation use. Also use addMigrations just startVersion > endVersion.

When a version mismatch occurs during upgrade or downgrade, an exception is thrown by default. Of course we can handle exceptions as well.

The upgrade can add fallbackToDestructiveMigration method, when not matching to the version will be directly delete table and then recreated.

Relegation when adding fallbackToDestructiveMigrationOnDowngrade method, when not matching to the version will be directly delete table and then recreated.

Scan the qr code below to follow the public account for more technical dry goods.

Recommended reading

Still not sure what Android Jetpack is? You went out

Android Jetpack Architecture Component – Lifecycle Into the pit Guide

Jetpack architecture components – LiveData and ViewModel cratering details