Jetpack系列——Room

850 阅读4分钟

Android采用了SQLite数据库来实现数据库持久化数据,但是使用Android原生提供的API来操作SQLite数据库,代码量大,且编写起来容易出错,于是市面上开源了许多框架,如GreenDAO、ORMLite等等。Google在Jetpack中提供了一种新的数据库组件,即Room,让开发者能够更简单高效地进行数据库开发。

Room中有两个比较重要的概念:

  • Entity:对应数据库中的一张表,在Java中对应一个实体类,实体类中的属性对应数据库中的字段;
  • Dao:数据访问对象,开发者可以通过Dao来进行数据操作。

基本使用

添加依赖:

implementation 'androidx.room:room-runtime:2.2.5'
annotationProcessor 'androidx.room:room-compiler:2.2.5'

// 如果是kotlin还需要添加以下依赖
annotationProcessor "android.arch.persistence.room:compiler:2.2.5"
kapt "android.arch.persistence.room:compiler:1.1.1"

作为示例代码,我们创建一个名为“person”的数据库表,在表中主要是id、name、grade、isMen和extraInfo等几个字段,首先需要创建对应的Entity实体类:

const val tableName = "person"

@Entity(tableName = tableName)
class Person {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    var mId: Int? = null

    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
    var mName: String

    @ColumnInfo(name = "grade", typeAffinity = ColumnInfo.REAL)
    var mGrade: Double

    @ColumnInfo(name = "isMan", typeAffinity = ColumnInfo.INTEGER)
    var mIsMan: Boolean

    @Ignore
    var mExtraInfo: String? = null

    constructor(name: String, grade: Double, isMan: Boolean) {
        mName = name
        mGrade = grade
        mIsMan = isMan
    }

    // Room只能识别一个构造器
    @Ignore
    constructor(
        name: String,
        grade: Double,
        isMan: Boolean,
        extraInfo: String?
    ) {
        mName = name
        mGrade = grade
        mIsMan = isMan
        mExtraInfo = extraInfo
    }

    override fun toString(): String {
        return "Person(mId=$mId, mName='$mName', mGrade=$mGrade, mIsMan=$mIsMan, mExtraInfo=$mExtraInfo)"
    }
}

其中各注解:

  • @Entity:修饰类,将该实体类与数据库表对应起来,其中tableName参数值表示对应的表名;
  • @PrimaryKey:修饰属性:表示将该字段作为表中的主键,autoGenerate = true表示自动生成;
  • @ColumnInfo:修饰属性,将该属性与数据库表中字段对应,name参数表示数据库表中的字段名,typeAffinity表示字段类型;
  • @Ignore:修饰方法和属性,用来告诉Room忽略该字段或方法,由于Room只能识别Entity实体类的一个构造方法,所以如果实体类中需要有多个构造方法的时候,其余的需要使用@Ignore修饰。

有了Entity实体类,接下来还需要创建一个Dao工具类:

@Dao
interface PersonDao {
    @Insert
    fun insertPerson(p: Person?)

    @Delete
    fun deletePerson(p: Person?)

    @Query("DELETE FROM ${tableName} WHERE id = :id")
    fun deletePerson(id: Int)

    @Update
    fun updatePerson(p: Person?)

    @Query("SELECT * FROM ${tableName}")
    fun queryPersons(): List<Person?>?

    @Query("SELECT * FROM ${tableName} WHERE id = :id")
    fun queryPerson(id: Int): Person?
}

定义的Dao类是一个接口,需要使用@Dao注解修饰,将来项目build之后,Room会生成对应的Impl类。

在Dao工具类中定义出所有的数据库操作,比且使用对应的注解修饰:

  • @Insert:插入数据;
  • @Delete:删除数据;
  • @Update:更新数据;
  • @Query:查找数据,此注解需要传入SQL语句作为参数,SQL语句用来限定查询的条件,注意如果是按条件删除数据等操作,也可以使用此注解来修饰。

接下来就是Database类了,此类主要是用来创建数据库:

@Database(entities = [Person::class], version = 1)
abstract class PersonDatabase : RoomDatabase() {
    abstract val personDao: PersonDao?

    companion object {
        private const val DATABASE_NAME = "person.db"
        private var mPersonDatabase: PersonDatabase? = null

        fun getInstance(context: Context): PersonDatabase {
            if (mPersonDatabase == null) {
                mPersonDatabase = Room.databaseBuilder(
                    context.applicationContext,
                    PersonDatabase::class.java,
                    DATABASE_NAME
                ).build()
            }
            return mPersonDatabase!!
        }
    }
}

PersonDatabase是一个抽象类,其中核心有两个方法,第一个就是获取PersonDatabase实例,一般情况下,整个项目中有一个PersonDatabase实例即可,所以我们通常使用单例模式。第二个就是获取其对应的Dao工具类。

创建Database类的时候,需要继承自RoomDatabase类,并使用@Database注解,其中entities参数值是一数组,对应此数据库中的所有表,version表示数据库版本号。

使用Room.databaseBuilder初始化Database对象,需要注意传入的Context是整个应用程序的Context。

接下来就可以使用Room进行数据库增删改查了,由于数据库操作是耗时操作,所以Room强制要求所有的增删改查不能在主线程中执行,这里我们使用Kotlin的协程来实现:

class RoomActivity : AppCompatActivity() {

    var i = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_room)

        var p: Person? = null

        insert.setOnClickListener {
            MainScope().launch(Dispatchers.IO) {
                try {
                    p = Person("p$i", 10.1 + i, i % 2 == 0)
                    i++
                    PersonDatabase.getInstance(this@RoomActivity).personDao?.insertPerson(p)
                } catch (e: Exception) {
                    Log.e("jia", "onCreate: " + e.toString())
                }
            }
        }

        delete_by_id.setOnClickListener {
            MainScope().launch(Dispatchers.IO) {
                PersonDatabase.getInstance(this@RoomActivity).personDao?.deletePerson(i)
            }
        }

        delete.setOnClickListener {
            MainScope().launch(Dispatchers.IO) {
                PersonDatabase.getInstance(this@RoomActivity).personDao?.deletePerson(p)
            }
        }

        update.setOnClickListener {
            MainScope().launch(Dispatchers.IO) {
                PersonDatabase.getInstance(this@RoomActivity).personDao?.updatePerson(p)
            }
        }

        query.setOnClickListener {
            MainScope().launch(Dispatchers.IO) {
                val p = Person("new p$i", 10.1 + i, i % 2 == 0)
                val persons =
                    PersonDatabase.getInstance(this@RoomActivity).personDao?.queryPersons()
                if (persons != null && !persons.isEmpty()) {
                    for (pp in persons) {
                        Log.e("jia", "onCreate: " + pp.toString())
                    }
                }
            }
        }

        query_by_id.setOnClickListener {
            MainScope().launch(Dispatchers.IO) {
                val person = PersonDatabase.getInstance(this@RoomActivity).personDao?.queryPerson(i)
                if (person != null) {
                    Log.e("jia", "onCreate: " + person.toString())
                }
            }
        }
    }
}

Room+ViewModel、LiveData

ViewModel+LiveData的组合,可以使得数据不受UI组件生命周期影响的同时,还能时时监测到数据值的变化,那么如果再结合Room使用,就可以当数据库内容发生变化的时候,主动通知到UI组件,就不需要每次都主动地去执行一些命令。

从上一小节的介绍中得知,创建RoomDatabase实例的时候,需要用到Application的Context,如果使用普通的ViewModel的话,就需要个ViewModel传入Context,由于ViewModel的特性,传入Context会造成内存泄漏,所以我们可以使用ViewModel的子类AndroidViewModel,其正好提供了Application的Context。

在Dao中,定义查询接口的时候,返回值使用LiveData封装一下:

@Dao
interface PersonDao {

    @Query("SELECT * FROM ${tableName}")
    fun queryPersons(): LiveData<List<Person?>>?
    
}

创建ViewModel:

class RoomViewModel(application: Application) : AndroidViewModel(application) {

    val mDatabase: PersonDatabase
    val mLiveData: LiveData<List<Person?>>?

    init {
        mDatabase = PersonDatabase.getInstance(application)
        mLiveData = mDatabase.personDao!!.queryPersons()
    }

}

在UI组件中通过Dao工具获取到LiveData对象,对其进行observe操作即可。

val personLiveData = PersonDatabase.getInstance(this@RoomActivity).personDao?.queryPersons()
personLiveData?.observe(this@RoomActivity, Observer<List<Person?>> { t -> Log.e("jia", "onChanged: " + t?.size) })

这样操作后,当我们对数据库进行任何数据改动时,都会监测到,时时刷新UI。

数据库升级

在需求迭代过程中,可能会对数据库表结构加一些新的字段,遇到这种情况,就需要将数据库版本号进行升级,Room提供了Migration类,用以实现数据库升级。

Migration类的构造方法接收两个参数:startVersion、endVersion,startVersion表示当前数据库版本(旧版本),endVersion为新版本,创建Migration对象:

    private var MIGRATION_1_2 = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            TODO("Not yet implemented")
        }
    }

如上述代码,如果从版本1升级到版本2,就在创建Migration对象的时候两个参数分别传入1和2。

在migrate方法中,可以通过database的execSQL()方法,传入对应的SQL语句,即可对数据库进行升级管理,如复制一个完全一样的表等等。

在初始化Database对象的时候,调用addMigratioon()方法设置给Database:

 mPersonDatabase = Room.databaseBuilder(
	context.applicationContext,
    PersonDatabase::class.java,
    DATABASE_NAME
)
	.addMigrations(MIGRATION_1_2)
    .build()

如果数据库版本号从1升级到3,系统会优先匹配1-3的Margation,如果没有的话就会依次执行 1-2,2-3。

如果升级数据库但没有匹配到对应的Margation,程序就会奔溃,我们可以在初始化Database对象的时候,加入fallbackToDestructiveMigration()语句:

 mPersonDatabase = Room.databaseBuilder(
   context.applicationContext,
   PersonDatabase::class.java,
   DATABASE_NAME
)
   .fallbackToDestructiveMigration()
   .addMigrations(MIGRATION_1_2)
   .build()

schema文件

如果想查看数据库内容,Room提供了Schema文件,其中包含了数据库的所有基本信息,我们需要在项目的build.gradle文件中进行如下配置,为其指定文件的导出位置:

android {

    defaultConfig {

        javaCompileOptions{
            annotationProcessorOptions{
                arguments=["room.schemaLocation":"$projectDir/ schemas".toString()]
            }
        }
    }
 }

最终生成的文件如下:其中包含了数据库版本号,所有的表信息等等。

{
  "formatVersion": 1,
  "database": {
    "version": 1,
    "identityHash": "70c4ab471b3ce6b4a92e075b494c7ad5",
    "entities": [
      {
        "tableName": "person",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `grade` REAL NOT NULL, `isMan` INTEGER NOT NULL)",
        "fields": [
          {
            "fieldPath": "mId",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "mName",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": true
          },
          {
            "fieldPath": "mGrade",
            "columnName": "grade",
            "affinity": "REAL",
            "notNull": true
          },
          {
            "fieldPath": "mIsMan",
            "columnName": "isMan",
            "affinity": "INTEGER",
            "notNull": true
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [],
        "foreignKeys": []
      }
    ],
    "setupQueries": [
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"70c4ab471b3ce6b4a92e075b494c7ad5\")"
    ]
  }
}