Jetpack room framework usage and source code analysis

1, Introduction

1. What is Room

The room framework is one of many component libraries of Android Jetpack. The emergence of Jetpack unifies the Android development ecology, various tripartite libraries are gradually replaced by official components, and room gradually replaces competitive products as the most mainstream database ORM framework. Room is an abstraction of SQLite database, which enables users to enjoy a more robust database access mechanism while making full use of the powerful functions of SQLite.

2. Why use Room

Compared with traditional methods such as SQLiteOpenHelper, using Room to operate SQLite has the following advantages:
1. SQL syntax check at compile time
2. Develop efficiently and avoid a large amount of template code
3. API design is friendly and easy to understand
4. It can be associated with LiveData and has the capability of LiveData Lifecycle

2, Basic usage of Room

The use of Room mainly involves the following three components
1. Database: access to the underlying database
2. Entity: represents a table in the database, usually annotated
3. DAO (Data Access Object): database accessor

The concepts of these three components also appear in other ORM frameworks. The DAO is obtained through the Database, and then the entities are queried and obtained through the DAO. Finally, the data in the Database table is read and written through the entities. Users only need to face the DAO.
Use a Room frame through a chestnut

1. Introduce dependency

    def room_version = "2.3.0"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"

2. Three roles of Room

Entity

An Entity corresponds to a table. To create a User table, you only need to add @ Entity annotation on the class. Primary keys and column names can be defined through annotation. Note that variables cannot be defined as val or private, otherwise exceptions will be thrown~

@Entity(tableName = "User") // tableName is the table name. If it is not set, it is the same as the class name
class User() {

    constructor(s: String, a: Int) : this() {
        name = s
        age = a
    }

    constructor(i: Int, s: String, a: Int) : this() {
        uid = i
        name = s
        age = a
    }

    // The primary key is annotated with @ PrimaryKey. Is autoGenerate self growing
    @PrimaryKey(autoGenerate = true)
    var uid: Int? = null

    // The column name is annotated with @ ColumnInfo. Name is an alias. If it is not set, the name in the table is the same as the field name
    @ColumnInfo(name = "name")
    var name: String? = null

    @ColumnInfo(name = "age")
    var age: Int? = null

    override fun toString(): String {
        return "User(uid=$uid, name=$name, age=$age)"
    }

}

DAO

The DAO layer is defined as an interface class, so that when the user calls, the actual call is the implementation class, and the implementation class is automatically generated by Room for us. Add, delete, change and query methods are defined in the interface class, in which we still need to write SQL statements ourselves.

@Dao
interface UserDao {

    @Insert
    fun insert(vararg users: User)

    @Delete
    fun delete(vararg users: User?)

    @Update
    fun update(user: User)

    @Query("select * from User")
    fun getAll(): MutableList<User>

    @Query("select * from User where name like :name")
    fun findByName(name: String): MutableList<User>

}

Database

Create an abstract class to inherit RoomDatabase. The purpose of creating an abstract class is also to let Room generate subclasses to realize functions. Define an abstract method to expose DAO.
@The Database annotation takes three parameters. entities are all Entity objects, that is, all tables. Version is the Database version and exportSchema=false is the export mode. It must be written. This is the specification code

@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDataBase : RoomDatabase() {
    abstract fun userDao(): UserDao?
}

3. Call database

Use the following code in the Activity to test the use of the Room database

val myDB = Room.databaseBuilder(applicationContext, AppDataBase::class.java, 
            "myDB").build()
val dao = myDB.userDao()
//Database operations are generally performed in child threads
thread {
    val user1 = User("Zhang San", 12)
    val user2 = User("Li Si", 22)
    val user3 = User("Wang Wu", 18)
    val user4 = User("Kuzi VI", 23)

    dao?.insert(user1, user2, user3, user4)

    var allUser = dao?.getAll()
    Log.d(TAG, allUser.toString())

    val user = dao?.findByName("Zhang San")
    Log.d(TAG, user.toString())
    dao?.delete(user?.get(0))
    allUser = dao?.getAll()
    Log.d(TAG, allUser.toString())
}

The printed results are as follows, and the function is realized normally

D/RoomActivity: [User(uid=1, name=Zhang San, age=12), User(uid=2, name=Li Si, age=22), User(uid=3, name=Wang Wu, age=18), User(uid=4, name=Kuzi VI, age=23)]
D/RoomActivity: [User(uid=1, name=Zhang San, age=12)]
D/RoomActivity: [User(uid=2, name=Li Si, age=22), User(uid=3, name=Wang Wu, age=18), User(uid=4, name=Kuzi VI, age=23)]

3, Room is used with LiveData

Add a new method in the DAO to return the LiveData type and wrap the original type. Call this method when using and add an observer. The most basic usage of Room is introduced here. In practical application, it is generally used in combination with ViewModel, and an additional layer of Repository is encapsulated.

@Dao
interface UserDao {

    @Query("select * from User")
    fun getAllLiveData(): LiveData<MutableList<User>>
    
}
val myDB = Room.databaseBuilder(applicationContext, AppDataBase::class.java,
             "myDB").build()
val dao = myDB.userDao()
// The data in the observer database will be printed as long as they dare to change
dao?.getAllLiveData()?.observe(this, {
    Log.d(TAG, it.toString())
})

//Database operations are generally performed in child threads
thread {

    val user1 = User("Zhang San", 12)
    val user2 = User("Li Si", 22)
    val user3 = User("Wang Wu", 18)
    val user4 = User("Kuzi VI", 23)

    dao?.insert(user1, user2, user3, user4)

    val user = dao?.findByName("Zhang San")
    Log.d(TAG, user.toString())

    // The simulation data is modified. Once the database is modified, the data will drive the UI to change
    for (i in 0..50) {
        Thread.sleep(3000)
        dao?.update(User(2, "Sun Qi $i", i))
        val allUser = dao?.getAll()
        Log.d(TAG, allUser.toString())
    }

}

4, Room source code analysis

Room makes extensive use of APT annotation processor to generate code and SQL statements at run time through annotation, so as to simplify development. In fact, most ORM frameworks do this. Room generates two specific implementation classes, appdatabase, at run time_ Impl and UserDao_Impl.

1,AppDataBase_Impl

First, we create a database from the build() method to analyze

val myDB = Room.databaseBuilder(applicationContext, AppDataBase::class.java, 
            "myDB").build()

Enter the build() method and set various parameters in RoomDatabase. First, we only focus on the main process. We can see that the init() method is called and db is returned.

public T build() {
    ...
    db.init(configuration);
    return db;
}

Continue to follow the init() method

public void init(@NonNull DatabaseConfiguration configuration) {
    mOpenHelper = createOpenHelper(configuration);
    ...
}

The createOpenHelper method is called, and the createOpenHelper method is implemented in appdatabase_ In impl, RoomOpenHelper is created. RoomOpenHelper inherits SupportSQLiteOpenHelper.Callback. It can be seen that Room is the encapsulation of native SQLite, including initialization and database upgrade operations.

 final SupportSQLiteOpenHelper.Callback _openCallback = new 
         RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1) {
     ...
 }

At the same time, AppDataBase_Impl also exposes a UserDao for us, which is why it needs to be defined as an abstract method. The specific operation to implement new has to be done by the code generated by APT.

 @Override
  public UserDao userDao() {
    if (_userDao != null) {
      return _userDao;
    } else {
      synchronized(this) {
        if(_userDao == null) {
          _userDao = new UserDao_Impl(this);
        }
        return _userDao;
      }
    }
  }

2,UserDao_Impl

AppDataBase_ After reading impl, let's take a look at UserDao_Impl class, enter userdao_ After impl, you can see that UserDao_Impl implements all annotated methods defined in DAO, and helps us splice and generate corresponding SQL statements

  @Override
  public void insert(final User... users) {
    __db.assertNotSuspendingTransaction();
    __db.beginTransaction();
    try {
      __insertionAdapterOfUser.insert(users);
      __db.setTransactionSuccessful();
    } finally {
      __db.endTransaction();
    }
  }
  @Override
  public List<User> getAll() {
    final String _sql = "select * from User";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    __db.assertNotSuspendingTransaction();
    final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
    try {
      final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
      final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
      final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "age");
      final List<User> _result = new ArrayList<User>(_cursor.getCount());
      while(_cursor.moveToNext()) {
        final User _item;
        _item = new User();
        final Integer _tmpUid;
        if (_cursor.isNull(_cursorIndexOfUid)) {
          _tmpUid = null;
        } else {
          _tmpUid = _cursor.getInt(_cursorIndexOfUid);
        }
        _item.setUid(_tmpUid);
        final String _tmpName;
        if (_cursor.isNull(_cursorIndexOfName)) {
          _tmpName = null;
        } else {
          _tmpName = _cursor.getString(_cursorIndexOfName);
        }
        _item.setName(_tmpName);
        final Integer _tmpAge;
        if (_cursor.isNull(_cursorIndexOfAge)) {
          _tmpAge = null;
        } else {
          _tmpAge = _cursor.getInt(_cursorIndexOfAge);
        }
        _item.setAge(_tmpAge);
        _result.add(_item);
      }
      return _result;
    } finally {
      _cursor.close();
      _statement.release();
    }
  }

Here, Room can be used as a standard ORM framework. All additions, deletions, modifications and queries are marked by annotations, and APT dynamically generates implementation classes. Each annotated method generates and executes the corresponding SQL.

3. Room works with LiveData

As mentioned earlier, the Room framework can give full play to its advantages only in combination with Jetpack such as LiveData. When we define a LiveData to receive database query results, when the database data changes, we can perceive the data changes. How do we do this? Let's follow up the previously defined getAllLiveData() method, Defined in UserDao, defined in UserDao_ Corresponding implementation found in impl

  @Override
  public LiveData<List<User>> getAllLiveData() {
    final String _sql = "select * from User";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    return __db.getInvalidationTracker().createLiveData(new String[]{"User"},
     false, new Callable<List<User>>() {
      @Override
      public List<User> call() throws Exception {
      ...
      }
      ...
    });
  }

As you can see, the createLiveData() method is called and its callback is monitored. As you can see in the call() callback method, it is to perform database operations and return results.
The createLiveData() method will eventually call RoomTrackingLiveData, and an observer mObserver is initialized in the construction method

    RoomTrackingLiveData(
            ...
            Callable<T> computeFunction,
            String[] tableNames) {
        ...
        mComputeFunction = computeFunction;
        mContainer = container;
        mObserver = new InvalidationTracker.Observer(tableNames) {
            @Override
            public void onInvalidated(@NonNull Set<String> tables) {
                ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
            }
        };
    }

mRefreshRunnable is executed in onActive() method, and the Active state here is the Active state in Lifecycle

    protected void onActive() {
        super.onActive();
        mContainer.onActive(this);
        getQueryExecutor().execute(mRefreshRunnable);
    }

You can see these key codes in mRefreshRunnable

while (mInvalid.compareAndSet(true, false)) {
    computed = true;
    try {
        value = mComputeFunction.call();
    } catch (Exception e) {
        throw new RuntimeException("Exception while computing database"+ 
            " live data.", e);
    }
}
if (computed) {
    postValue(value);
}

Value refers to the value queried in the database through the callback mentioned earlier, and finally calls postValue() to update LiveData. So far, the association between Room framework and LiveData has been established.

Tags: Android Database kotlin jetpack

Posted on Thu, 25 Nov 2021 22:10:17 -0500 by tazdevil