oynix

于无声处听惊雷,于无色处见繁花

Room的使用

Room是一个数据库,基于SQLite的抽象层,也可以直接使用SQLite,但强烈建议使用Room。

官方文档:Android Room

导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
// ..... 其他依赖 .....

// Room
def room_version = "2.3.0"

implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"

// To use Kotlin annotation processing tool (kapt)
kapt "androidx.room:room-compiler:$room_version"
}

创建表。

Room里的表,需要用@Entry注解标注,这里用个简单的学生表

1
2
3
4
5
6
7
8
// table_name就是表的名字,indecies是可选项,用来生成表的索引
@Entity(tableName = "student", indices = [Index(value = ["num"], unique = true)])
data class Student(
@PrimaryKey val id: Int,
val num: Int,
val name: String,
val age: Int
)

创建数据表访问类。

这里我们只需要声明接口,并加以@Dao注解标注,具体实现交给Room来做,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这里只定义CRUD,增删改查,也可根据需要定义其他接口,如查询分数大于某个数值的所有学生
@Dao
interface StudentDao {
@Insert
fun insert(student: Student): Long

@Delete
fun delete(student: Student): Int

@Update
fun update(student: Student): Int

@Query("SELECT * FROM student WHERE num = :num")
fun query(num: Int): Student?
}

表有了,访问方式也有了,下面创建数据库。

1
2
3
4
5
6
// 继承自RoomDatabase,其中已经实现了绝大数功能,我们只需要额外声明几个接口,用来提供Dao数据类
// 方法的具体实现交给Room来做
@Database(entities = [Student::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun studentDao(): StudentDao
}

万事俱备,现在可以用了。在Activity里调用下试试看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 通过Room提供的Builder创建数据库的实例
// 第一个参数是个context,第二个参数是数据库的Class,第三个参数是数据库文件的名字
val db = Room.databaseBuilder(this, AppDatabase::class.java, "app.db").build()

// 获得一个Dao
val dao = db.studentDao()

// 添加一条记录
dao.insert(Student(1, 1, "john", 98))
}

添加完成,运行一下。

然后你会发现,崩溃了。。。日志如下:

1
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.oynix.room.sample/com.oynix.room.sample.MainActivity}: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.

这句话的意思是说,不能在main thread,也就是主线程访问数据库,因为这样有可能会把页面卡住一段很长的时间。这样的检测机制算是合理的,毕竟这属于IO操作,而IO操作都应该放到单独的线程去跑。
但是也可以去掉这种检测,在创建数据库的时候额外传入个配置:

1
2
3
4
5
6
7
8
9
10
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val db = Room.databaseBuilder(this, AppDatabase::class.java, "app.db")
.allowMainThreadQueries() // 允许在主线程操作
.build()
val dao = db.studentDao()
dao.insert(Student(1, 1, "john", 98))
}

这个时候再运行一下,就会发现顺利执行

数据库文件

这个时候,在Android Studio的Device File Explorer中,可以在data/data/{包名}/database目录下发现3个文件,app.db、app.db-shm和app.db-wal,其中.db是数据库文件,另外两个是临时文件。
把这三个文件导出到电脑桌面,然后用能打开db文件的软件将app.db打开,如SQLite Professional、SQLite Studio等,会看到我们创建的student表,以及表中刚刚插入的那条数据。

增加一张表

这个时候,业务发生调整,我们需要增加一张教师表,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity(tableName = "teacher", indices = [Index(value = ["num"], unique = true)])
data class Teacher(
@PrimaryKey val id: Int,
val num: Int, // 教师编号
val name: String,
val course: String
)

@Dao
interface TeacherDao {
@Insert
fun insert(teacher: Teacher): Long

@Delete
fun delete(teacher: Teacher): Int

@Update
fun update(teacher: Teacher): Int

@Query("SELECT * FROM teacher WHERE num = :num")
fun query(num: Int): Teacher?
}

同时,也要修改数据库类,增加新的教师表实体

1
2
3
4
5
6

@Database(entities = [Student::class, Teacher::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun studentDao(): StudentDao
abstract fun teacherDao(): TeacherDao
}

增加一条教师的记录。张老师,他很厉害,会教数学

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val db = Room.databaseBuilder(this, AppDatabase::class.java, "app.db")
.allowMainThreadQueries()
.build()
// John,学号:1
val dao = db.studentDao()
dao.insert(Student(1, 1, "John", 14))

// 张老师,教数学
val teacherDao = db.teacherDao()
teacherDao.insert(Teacher(1, 1001, "Miss Zhang", "Math"))
}

再一运行,发现又报错了。。。

1
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.oynix.room.sample/com.oynix.room.sample.MainActivity}: java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.

这句话的意思是说,数据库的schema,也就是结构发生了改变,但是版本号没有更新,Room不知道怎么处理这些schema的修改,然后就报了个错。

数据库升级

就像刚刚那样,在版本的迭代更新中,常常会有修改数据库结构的情况。在Room中,数据库是通过version号,来管理数据库版本的,每做一次修改,version都要增加,一般增加1,你每次加2也没人能拿你怎么样。我们在创建数据库的同时,还要告诉Room版本更新做了哪些操作,这些需要通过addMigrations来添加。
只修改schema,而不增加version,程序会崩溃。
只增加version,而不提供Migration,程序会删除并重建数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val db = Room.databaseBuilder(this, AppDatabase::class.java, "app.db")
.allowMainThreadQueries()
.addMigrations(object : Migration(1, 2){
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `teacher` (`id` INTEGER NOT NULL, `num` INTEGER NOT NULL, `name` TEXT NOT NULL, `course` TEXT NOT NULL, PRIMARY KEY(`id`))")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_teacher_num` ON `teacher` (`num`)")
}
})
.build()
// 这里要注掉,因为表里已经有了一个id为1的记录了
// val dao = db.studentDao()
// dao.insert(Student(1, 1, "John", 14))

val teacherDao = db.teacherDao()
teacherDao.insert(Teacher(1, 1001, "Miss Zhang", "Math"))
}

如上,从1到2后,我们新加了一个teacher表,并在表上加了一个索引,那么这么写就可以了。
如果说,不想写这么长的SQL语句怎么办?也好办,全局搜AppDatabase_Impl.java文件,这里面就是Room已经写好的。
再次运行,发现没有再崩溃,打开app.db,里面多了一张表,表里有一条刚加的张老师的记录。

addMigrations

这里要单独说一说这个方法,程序运行后,如果本地数据库版本和代码里的版本不一致,这个时候这个方法才会派上用场,如果一致则无用。
先帖下文档里的说明:

1
2
3
4
5
6
7
8
9
10
11
12
/**
Adds a migration to the builder.
Each Migration has a start and end versions and Room runs these migrations to bring the database to the latest version.
If a migration item is missing between current version and the latest version, Room will clear the database and recreate so even if you have no changes between 2 versions, you should still provide a Migration object to the builder.
A migration can handle more than 1 version (e.g. if you have a faster path to choose when going version 3 to 5 without going to version 4). If Room opens a database at version 3 and latest version is >= 5, Room will use the migration object that can migrate from 3 to 5 instead of 3 to 4 and 4 to 5.
Params:
migrations – The migration object that can modify the database and to the necessary changes.
Returns:
This RoomDatabase.Builder instance.
*/
@NonNull
public Builder<T> addMigrations(@NonNull Migration... migrations)

它接收可变参数,即可同时接收多个Migration。每个Migration都有个startVersion,以及一个endVersion,Room将会运行这些Migration,将本地数据库的版本一步步升级到代码里的最新版本。如果缺失当前版本到最新版本的Migration,Room将会清空数据库并重建。所以,即便在两个版本之间没有变化,仍然需要提供一个Migration给builder。
一个Migration可以处理多个版本,例如,当前是版本3,最新是版本5,你提供了3到4的Migration、4到5的Migration以及3到5的Migration,那么Room就会选择更快的Migration,即3到5,而不是由3到4再到5。

数据加密

虽然Android高版本在数据安全这一块已经提升了很多,未root的手机基本看不到其他应用独立存储空间里的内容,但为了以防万一,可以进一步将数据库文件,也就是app.db,进行加密。
当前有很多种实现思路,如每次写之前,将数据加密,读之后,将数据解密,等。
这里介绍个第三方加密库,使用很方便,当然也有缺点,就是包体会变大,因为用到了native库,增大6M左右

  • 引入依赖
    1
    2
    3
    4
    5
     // room cypher maven
    maven { url "https://s3.amazonaws.com/repo.commonsware.com" }

    // room cypher
    implementation "com.commonsware.cwac:saferoom.x:1.2.1"
  • 增加Factory,openHeloperFactory,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val db = Room.databaseBuilder(this, AppDatabase::class.java, "app.db")
    .allowMainThreadQueries()
    .openHelperFactory(SafeHelperFactory("your_database_password".toCharArray()))
    .addMigrations(object : Migration(1, 2){
    override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("CREATE TABLE IF NOT EXISTS `teacher` (`id` INTEGER NOT NULL, `num` INTEGER NOT NULL, `name` TEXT NOT NULL, `course` TEXT NOT NULL, PRIMARY KEY(`id`))")
    database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_teacher_num` ON `teacher` (`num`)")
    }
    })
    .build()
    // val dao = db.studentDao()
    // dao.insert(Student(1, 1, "John", 14))

    // val teacherDao = db.teacherDao()
    // teacherDao.insert(Teacher(1, 1001, "Miss Zhang", "Math"))
    }
  • 加密之后,再导出来的app.db就无法打开了

总结

以上,Room的简单使用就这些了,相比于SQLite,简单、方便了不少,本地化数据可以多考虑使用。
另外还有些更高级的用法,比如和ViewModel的结合、和Hilt的结合、异步操作数据流,等等,这里就就不做说明了。

附上Github地址:https://github.com/oynix/RoomSample

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2021/08/da5376c38dfd/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道