Jetpack: Room super detailed use pit guide!

1, Introduction

ORM(Object Relational Mapping) relational mapping library provides a layer of encapsulation on Sqlite to optimize the convenience of database operation.
The architecture diagram of Room is as follows:

  • Entity: an entity corresponds to a table in the database. Entity class is the mapping of Sqlite table structure to Java class, which can be regarded as a Model class in Java.
  • Dao: Data Access Objects. As the name suggests, we can access objects through it.

An Entity corresponds to a table, and each table needs a Dao object to add, delete, modify and query the table. After the Room object is instantiated, we can get the Dao object (Get Dao) through the database instance, and then operate the data in the table through the Dao object.
rely on

buildscript {
    //android room version
    ext.room_version = '2.3.0'
}
// room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - room kotlin extension
implementation "androidx.room:room-ktx:$room_version"

2, Room usage guide

Room usage

Get familiar with the basic use of Room by creating a simple student database table.

  1. Create an Entity about students, that is, create a student table SimpleStudentEntity.

    The Entity tag is used to map the SimpleStudent class to the data table in the Room. The tableName property can set the table name for the data table. If it is not set, the table name is the same as the class name.

    The PrimaryKey tag is used to specify this field as the primary key of the table. Autogenerate = set to true to let SQLite generate the unique id

    The ColumnInfo tag can be used to set the name of the field stored in the database table and specify the type of the field.

    At the same time, in order to indicate and field related references elsewhere, we declare them as top-level constants (java static constants) for easy reference.

    /**
     * Table names are related and defined uniformly
     */
    const val SIMPLE_STUDENT_TABLE_NAME = "simple_student"
    const val SIMPLE_STUDENT_TABLE_STUDENT_ID = "student_id"
    const val SIMPLE_STUDENT_TABLE_STUDENT_NAME = "student_name"
    const val SIMPLE_STUDENT_TABLE_STUDENT_AGE = "student_age"
    

    The complete SimpleStudentEntity.kt file code is as follows:

    @Entity(tableName = SIMPLE_STUDENT_TABLE_NAME)
    data class SimpleStudentEntity(
    
        @NonNull
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(
            name = SIMPLE_STUDENT_TABLE_STUDENT_ID,
            typeAffinity = ColumnInfo.INTEGER
        ) val id: Int = 0,//This value is set as a self incrementing primary key. By default, it will be self incrementing in the database. Just set a default value here
    
        @NonNull
        @ColumnInfo(name = SIMPLE_STUDENT_TABLE_STUDENT_NAME, typeAffinity = ColumnInfo.TEXT)
        val name: String?,
    
        @NonNull
        @ColumnInfo(name = SIMPLE_STUDENT_TABLE_STUDENT_AGE, typeAffinity = ColumnInfo.TEXT)
        val age: String?
    )
    /**
     * Table names are related and defined uniformly
     */
    const val SIMPLE_STUDENT_TABLE_NAME = "simple_student"
    const val SIMPLE_STUDENT_TABLE_STUDENT_ID = "student_id"
    const val SIMPLE_STUDENT_TABLE_STUDENT_NAME = "student_name"
    const val SIMPLE_STUDENT_TABLE_STUDENT_AGE = "student_age"
    
  2. For the above student Entity, we need to define a Dao interface file to access the Entity. Note that * * Dao * * label needs to be added above the interface file.

    Add, Delete and modify queries are marked with Insert, Delete, Update and Query respectively. You can add a colon (:) before the Query to refer to the Kotlin value in the Query (for example: id in the function parameter)

    The query needs to pass in sql statements. If you don't know sql, you can google.

    @Dao
    interface SimpleStudentDao {
        @Insert
        fun insertStudent(studentEntity: SimpleStudentEntity)
    
        @Insert
        fun insertStudentAll(studentEntity: List<SimpleStudentEntity>)
    
        @Delete
        fun deleteStudent(studentEntity: SimpleStudentEntity)
    
        @Update
        fun updateStudent(studentEntity: SimpleStudentEntity)
    
        @Query("select * from $SIMPLE_STUDENT_TABLE_NAME")
        fun getStudentAll(): List<SimpleStudentEntity>
    
        @Query("select * from $SIMPLE_STUDENT_TABLE_NAME where $SIMPLE_STUDENT_TABLE_STUDENT_ID = :id")
        fun getStudentById(id: Int): List<SimpleStudentEntity>
    }
    
  3. After defining the Entity and Dao, the next step is to create the database.
    The Database tag is used to tell the system that this is a Room Database object.

    The entities attribute is used to specify which tables the database has. If multiple tables need to be created, the table names are separated by commas.

    The version attribute is used to specify the database version number. The subsequent database upgrade is judged according to the version number.

    The database class needs to inherit from RoomDatabase and be created through Room.databaseBuilder() combined with singleton design pattern.

    In addition, Dao objects created earlier are returned in the form of abstract methods, so the customized Database is an abstract class.

    @Database(entities = arrayOf(SimpleStudentEntity::class), version = 1)
    abstract class SimpleMyDataBase : RoomDatabase() {
    
        companion object {
            private const val DATA_NAME = "simple_db"
            
            @Volatile
            private var INSTANCE: SimpleMyDataBase? = null
    		
            /**
             * Double check lock single instance, return database instance
             */
            fun getDataBase(): SimpleMyDataBase = INSTANCE ?: synchronized(this) {
                val instance = INSTANCE ?: Room
                    .databaseBuilder(AppUtil.application, SimpleMyDataBase::class.java, DATA_NAME)
                    .build().also {
                        INSTANCE = it
                    }
                instance
            }
        }
    
        /**
         * Returns the SimpleStudentDao Dao object
         */
        abstract fun simpleStudentDao(): SimpleStudentDao
    
    }
    

verification

Above, the creation of database and table is completed. Let's see how to add / delete / modify / query the database.

It should be noted that these operations cannot be performed directly in the UI thread, and all operations need to be performed in the worker thread.

Write an Activity and test the above code.

Define the ViewModel file. Jetpack: ViewModel user guide, detailed analysis of implementation principle!

class SimpleViewModel(private val simpleStudentDao: SimpleStudentDao) : ViewModel() {

    fun insertStudent(studentEntity: SimpleStudentEntity) {
        viewModelScope.launch(Dispatchers.Default) {
            simpleStudentDao.insertStudent(studentEntity)
        }
    }

    fun insertStudentAll(studentEntity: List<SimpleStudentEntity>) {
        viewModelScope.launch(Dispatchers.Default) {
            simpleStudentDao.insertStudentAll(studentEntity)
        }
    }

    fun deleteStudent(studentEntity: SimpleStudentEntity) {
        viewModelScope.launch(Dispatchers.Default) {
            simpleStudentDao.deleteStudent(studentEntity)
        }
    }

    fun updateStudent(studentEntity: SimpleStudentEntity) {
        viewModelScope.launch(Dispatchers.Default) {
            simpleStudentDao.updateStudent(studentEntity)
        }
    }

    suspend fun getStudentAll(): List<SimpleStudentEntity> {
        //Use master-slave scope
        return supervisorScope {
            val students = async(Dispatchers.IO) {
                simpleStudentDao.getStudentAll()
            }
            students.await()
        }
    }

    suspend fun getStudentById(id: Int): List<SimpleStudentEntity> {
        //Use master-slave scope
        return supervisorScope {
            val students = async(Dispatchers.IO) {
                simpleStudentDao.getStudentById(id)
            }
            students.await()
        }
    }


    override fun onCleared() {
        super.onCleared()

    }
}

/**
 * Custom factory. You can pass in Dao parameters
 */
class MyViewModelFactory(private val dao: SimpleStudentDao) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(SimpleViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return SimpleViewModel(dao) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class.")
    }

}

Activity

class SimpleRoomDemoActivity : AppCompatActivity() {

    /**
     * binding 
     */
    private var _binding: ActivitySimpleUseRoomBinding? = null
    private val binding get() = _binding!!

    /**
     * Database dao
     */
    private val simpleDao: SimpleStudentDao by lazy(LazyThreadSafetyMode.NONE) {
        SimpleMyDataBase.getDataBase().simpleStudentDao()
    }

    /**
     * viewModel
     */
    lateinit var viewModel: SimpleViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivitySimpleUseRoomBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initParam()
        initView()
    }

    private fun initParam() {
        viewModel = ViewModelProvider(
            this,
            //Transfer into your own factory
            MyViewModelFactory(simpleDao)
        )[SimpleViewModel::class.java]
    }

    private fun initView() {
        with(binding) {
            btnInsert.setOnClickListener {
                viewModel.insertStudent(SimpleStudentEntity(0, "zxf", "18"))
            }
            btnInsertAll.setOnClickListener {
                viewModel.insertStudentAll(
                    arrayListOf(
                        SimpleStudentEntity(0, "liSi", "18"),
                        SimpleStudentEntity(0, "wangWu", "18")
                    )
                )
            }
            //What are delete and update based on? The sql generated by the source code is based on the primary key by default
            btnDelete.setOnClickListener {
                viewModel.deleteStudent(SimpleStudentEntity(2, "delete", "99"))
//                viewModel.deleteStudent(SimpleStudentEntity(199,"update","99"))
            }
            btnUpdate.setOnClickListener {
                //Therefore, we can directly write a default id to set it without having to get the query object
//                viewModel.updateStudent(SimpleStudentEntity(1,"update","99"))
                viewModel.updateStudent(SimpleStudentEntity(199, "update", "99"))
            }
            //After looking at the source code generated by the query, the object is directly new, so it will not return null, but the value of the object is null, so it needs to be declared as a nullable type
            btnGetId.setOnClickListener {
                lifecycleScope.launch {
                    displayToTextView(viewModel.getStudentById(5))
                }
            }
            btnGetAll.setOnClickListener {
                lifecycleScope.launch {
                    displayToTextView(viewModel.getStudentAll())
                }
            }
        }
    }

    private fun displayToTextView(students: List<SimpleStudentEntity>) {
        val string = students.joinToString(
            """
            
        """.trimIndent()
        )
        binding.text.text = string
    }

    /**
     * Release binding
     */
    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}

result

3, Room stepping on the pit

1. Why not perform database operations on the main thread? What happens if you have to use it?

At first, I guessed that it should be the same as LiveData. When the method is executed, I first judge the thread. For example, the setVaule method of LiveData is judged in the first step. If it is not the main thread, an exception will be thrown. This should be similar. Jetpack: LiveData user guide, detailed analysis of implementation principle!

Let's look for the code in the generated Dao implementation class. For example, our Dao interface above is called SimpleStudentDao, and SimpleStudentDao is generated through the annotation processor_ Impl class, just find a method, such as insert. Take a look at the source code:

public void insertStudent(final SimpleStudentEntity studentEntity) {
	__db.assertNotSuspendingTransaction();
	__db.beginTransaction();
	try {
		__insertionAdapterOfSimpleStudentEntity.insert(studentEntity);
		__db.setTransactionSuccessful();
	} finally {
    __db.endTransaction();
	}
}

The first assertNotSuspendingTransaction verifies whether the blocking function is in the correct scope (the transaction problem caused by kotlin coroutine + room, which will be introduced in the following article, continue to pay attention!). Without this verification, it will cause deadlock.

Look what the second beginthransaction did

public void beginTransaction() {
	assertNotMainThread();
    ...
}
public void assertNotMainThread() {
    ...
    if (isMainThread()) {
        throw new IllegalStateException("Cannot access database on the main thread 			since it may potentially lock the UI for a long period of time.");
    }
}

Therefore, it is impossible to operate the database in the main thread, otherwise an exception will be thrown directly 😥.

2. Look at the query method. If no data is found, what will be returned? Null pointer? Or what, what should we pay attention to in the declaration field?
The above codes are written in kotlin. We know that kotlin has two types: non empty and nullable. If * * Room returns null, you need to declare the declared field or return type as nullable type. Otherwise, assigning null to the non empty type of kotlin will throw an exception** So we need to know what will be returned if there is no query result. Let's continue to look at the source code and the query method.

public List<SimpleStudentEntity> getStudentAll() {
    final String _sql = "select * from simple_student";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    __db.assertNotSuspendingTransaction();
    final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
    try {
      final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "student_id");
      final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "student_name");
      final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "student_age");
      final List<SimpleStudentEntity> _result = new ArrayList<SimpleStudentEntity>(_cursor.getCount());
      while(_cursor.moveToNext()) {
        final SimpleStudentEntity _item;
        final int _tmpId;
        _tmpId = _cursor.getInt(_cursorIndexOfId);
        final String _tmpName;
        if (_cursor.isNull(_cursorIndexOfName)) {
          _tmpName = null;
        } else {
          _tmpName = _cursor.getString(_cursorIndexOfName);
        }
        final String _tmpAge;
        if (_cursor.isNull(_cursorIndexOfAge)) {
          _tmpAge = null;
        } else {
          _tmpAge = _cursor.getString(_cursorIndexOfAge);
        }
        _item = new SimpleStudentEntity(_tmpId,_tmpName,_tmpAge);
        _result.add(_item);
      }
      return _result;
    } finally {
      _cursor.close();
      _statement.release();
    }
  }

Obviously, even if a data cannot be found, a List object will be returned, so the returned type does not need to be declared as an nullable type. For Entity classes, it is also an object created in advance to assign the found data, * * but if the reference type is not found, the field will be assigned as empty. Therefore, when creating an Entity, you need to declare the field as an nullable type** As shown in the Entity class declaration above:

3. The update and delete methods are judged by which field. The interface code in Dao says that an Entity class needs to be passed in. Do you need to query first before deleting and updating?

Similarly, look at the source code. Just look at delete

  public void deleteStudent(final SimpleStudentEntity studentEntity) {
    ...
    try {
      __deletionAdapterOfSimpleStudentEntity.handle(studentEntity);
      __db.setTransactionSuccessful();
    }
    ...
  }
    this.__deletionAdapterOfSimpleStudentEntity = new EntityDeletionOrUpdateAdapter<SimpleStudentEntity>(__db) {
      @Override
      public String createQuery() {
        return "DELETE FROM `simple_student` WHERE `student_id` = ?";
      }

      @Override
      public void bind(SupportSQLiteStatement stmt, SimpleStudentEntity value) {
        stmt.bindLong(1, value.getId());
      }
    };

Obviously, you can see an sql statement "DELETE FROM simple_student WHERE student_id =?";

Update in the same place, the sql statement is "update or abort simple_student set student_id =?, student_name =?, student_age =? Where student_id =?".

Therefore, by default, the judgment is based on the primary key (the PrimaryKey set above). For delete and update, you only need to know the corresponding primary key. There is no need to query the corresponding object first and then perform the corresponding operation (previously seen in other blogs) 🤣), For example, the above code directly writes the dead id and operates on the newly created object!

Tags: Android SQLite kotlin jetpack room

Posted on Sat, 20 Nov 2021 07:10:04 -0500 by neo0506