Jetpack Room

Room

由于原生的 SQLite 需要编写大量的 SQL 语句和处理逻辑,因此涌现出了许多 ORM 框架,比如 GreenDAO、ORMLite 以及 Litepal 等。而 Jetpack 也提供了 Room 组件,并且 Room 融合了 LiveData 等组件。

Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 强大功能的同时,能够流畅地访问数据库。

ORM(Object Relational Mapping)也叫对象关系映射。简单地讲,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是 ORM 了。

使用 ORM 框架的好处是可以用面向对象的思维来和数据库进行交互,绝大多数情况下不用再和 SQL 语句打交道了,同时也不用担心操作数据库的逻辑会让项目的整体代码变的混乱。

使用 Room 进行增删改查

Room 的基本架构图:

Room 的整体结构。它主要由 Entity、Dao 和 Database 三部分组成:

  • Entity 表示数据库中对应的表,用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
  • Dao 是数据访问对象的意思,提供了访问数据库的方法,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和 Dao 层进行交互即可。
  • Database 是数据库持有者,用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供 Dao 层的访问实例。

对 App 而言,他要使用 Database 获取对应数据库的访问对象 DAO,然后使用 DAO 对 Entity 进行存储操作。

首先需要添加插件和依赖:

1
2
3
4
5
6
7
8
9
apply plugin: 'kotlin-kapt'

dependencies {
// 由于 Room 会根据项目中声明的注解来动态生成代码,因此这里一定要使用 kapt 引入 Room 的编译时注解库,
// 而启用编译时注解功能则一定要先添加 kotlin-kapt 插件。
//(Java 项目使用 annotationProcessor("androidx.room:room-compiler:$roomVersion"))
implementation "androidx.room:room-runtime:2.2.5"
kapt "androidx.room:room-compiler:2.2.5"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 定义 Entity
* @Entity 注解声明它是一个实体类
*/
@Entity
data class User(var firstName:String,var lastName:String,var age:Int){

/**
* 一个良好的数据库编程建议是:
* 给每个实体类都添加一个 id 字段,并将这个字段设为主键。
* @PrimaryKey 设为主键
* autoGenerate = true 使得主键的值是自动生成的
*/
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Dao 必须使用接口,使用 @Dao 注解声明这是一个 Dao
* 内部则根据业务需求对各种数据库操作进行封装
*
* 使用非实体类参数来增删改查数据,必须编写 SQL 语句。
*/
@Dao
interface UserDao {

@Insert
fun insertUser(user: User):Long

@Update
fun updateUser(newUser:User)

@Query("select * from User")
fun loadAllUsers():List<User>

@Query("select * from User where age>:age")
fun loadUsersOlderThan(age: Int):List<User>

@Delete
fun deleteUser(user:User)

@Query("delete from User where lastName = :lastName")
fun deleteUserByLastName(lastName:String):Int
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 写法是非常固定的,只需定义好 3 个部分的内容:
* 数据库版本号,包含哪些实体类,以及提供 Dao 层的访问实例。
*
* 使用 @Database 注解,并在其中声明了数据库的版本号以及包含哪些实体类。(多个实体类用逗号隔开)
*
* 必须继承自 RoomDatabase() 类,并且要使用 abstract 关键字声明为抽象类,
* 然后提供相应的抽象方法,用户获取 Dao 实例,只需提供方法就可以,具体实现由 Room 在底层自动完成。
*/
@Database(version = 1,entities = [User::class])
abstract class AppDatabase :RoomDatabase(){

abstract fun userDao():UserDao

/**
* 在结构体中编写了一个单例模式
* 使用 instance 变量缓存 AppDatabase 的实例
*/
companion object{
private var instance:AppDatabase? =null

@Synchronized
fun getDatabase(context: Context):AppDatabase{

/**
* 如果 instance 变量不为空就直接返回,
* 否则调用 Room.databaseBuilder() 来构建一个 AppDatabase 实例。
*/
instance?.let {
return it
}

/**
* 接收三个参数:
* 第一个一定要使用 context.applicationContext,避免内存泄漏的问题。
* 第二个参数是 AppDatabase 的 class 类型
* 第三个参数是数据库名
*/
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java,"app_database").build().apply{
instance = this
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MainActivity : AppCompatActivity() {

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

val userDao = AppDatabase.getDatabase(this).userDao()
var user1 = User("Tom","Brady",40)
var user2 = User("Tom","Hanks",63)

addDataBtn.setOnClickListener{
// 由于数据库操作属于耗时操作,Room 默认是不允许在主线程中进行数据库操作的。
// 但为了测试方便,Room 还提供了一个更加简单的方法:
// Room.databaseBuilder(context.applicationContext,
// AppDatabase::class.java,"app_database")
// .allowMainThreadQueries()
// .build()
// 这样 Room 就允许在主线程中操作数据库了,但建议只在测试环境下使用。
thread {
// 将 User 对象插入到数据库中,并将 insertUser() 返回的主键 id 值赋值给原来的 User 对象
// 因为使用更新和删除注解去操作数据时都是基于这个 id 值来操作的。
user1.id = userDao.insertUser(user1)
user2.id = userDao.insertUser(user2)
}
}
updateDataBtn.setOnClickListener{
thread {
user1.age = 42
userDao.updateUser(user1)
}
}
deleteDateBtn.setOnClickListener{
thread {
userDao.deleteUserByLastName("Hanks")
}
}
queryDateBtn.setOnClickListener{
thread {
for (user in userDao.loadAllUsers()){
Log.e("MainActivity",user.toString())
}
}
}
}
}

Room 的数据库升级

测试阶段使用的方法:

1
2
3
4
5
6
// 构建 AppDatabase 时,加入 fallbackToDestructiveMigration() 方法,
// 这样只要数据库进行了升级,Room 就会将当前数据库销毁,然后再重新创建。副作用就是之前数据库中的所有数据全部丢失。
Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java,"app_database")
.fallbackToDestructiveMigration()
.build()

正规写法:

1
2
3
4
5
6
@Entity
data class Book(var name:String,var pages:Int) {

@PrimaryKey(autoGenerate = true)
var id : Long = 0
}
1
2
3
4
5
6
7
8
9
@Dao
interface BookDao {

@Insert
fun insertBook(book: Book):Long

@Query("select * from Book")
fun loadAllBooks():List<Book>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 将版本升级为 2,并添加实体类。
*/
@Database(version = 2,entities = [User::class,Book::class])
abstract class AppDatabase :RoomDatabase(){

abstract fun userDao():UserDao
abstract fun bookDao():BookDao

companion object{

/**
* 实现匿名类 Migration,传入的参数代表当数据库版本从 1 升级到 2 时就执行这个匿名类中的升级逻辑。
*/
val MIGRATION_1_2 = object : Migration(1,2){

override fun migrate(database: SupportSQLiteDatabase) {
// 由于要新增一张 Book 表,所以需要编写相应的建表语句。
// 并且建表语句必须和 Book 实体类中声明的结构完全一致。
database.execSQL("create table Book (id integer primary key autoincrement not null, name text not null,pages integer not null)")
}
}

private var instance:AppDatabase? =null

@Synchronized
fun getDatabase(context: Context):AppDatabase{

instance?.let {
return it
}

/**
* 添加 addMigrations(MIGRATION_1_2) 方法
*/
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java,"app_database")
.fallbackToDestructiveMigration()
.addMigrations(MIGRATION_1_2)
.build()
.apply{
instance = this
}
}
}
}

如果只是向现有的表中添加新的列,那只需要使用 alert 语句修改表结构就可以。

1
2
3
4
5
6
@Entity
data class Book(var name:String,var pages:Int,var author:String) {

@PrimaryKey(autoGenerate = true)
var id : Long = 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Database(version = 3,entities = [User::class,Book::class])
abstract class AppDatabase :RoomDatabase(){

...
companion object{

...
val MIGRATION_2_3 = object : Migration(2,3){

override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("alter table Book add column text not null default 'unknown'")
}
}

private var instance:AppDatabase? =null

@Synchronized
fun getDatabase(context: Context):AppDatabase{

...

/**
* 添加 addMigrations(MIGRATION_2_3) 方法
*/
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java,"app_database")
.fallbackToDestructiveMigration()
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build().apply{
instance = this
}
}
}
}

原理解析

从 DataBase 的创建来看,程序通过 Room.databaseBuilder ().build 创建了 database 对象,build 方法:

RoomDatabase.java
1
2
3
4
5
6
7
8
9
10
@SuppressLint("RestrictedApi")
@NonNull
public T build() {
...
DatabaseConfiguration configuration =
new DatabaseConfiguration(...);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
}

先查看 getGeneratedImplementation 方法:

getGeneratedImplementation 方法通过反射方法为 AccountDataBase 创建了实例,命名规则为 类型 +”_Impl”,也就是 AccountDataBase_Impl 类。

Room.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static <T, C> T getGeneratedImplementation(@NonNull Class<C> klass,
@NonNull String suffix) {
final String fullPackage = klass.getPackage().getName();
String name = klass.getCanonicalName();
final String postPackageName = fullPackage.isEmpty()
? name
: name.substring(fullPackage.length() + 1);
final String implName = postPackageName.replace('.', '_') + suffix;
//noinspection TryWithIdenticalCatches
try {

final String fullClassName = fullPackage.isEmpty()
? implName
: fullPackage + "." + implName;
@SuppressWarnings("unchecked")
final Class<T> aClass = (Class<T>) Class.forName(
fullClassName, true, klass.getClassLoader());
return aClass.newInstance();
} ...
}

接下来查看 db.init (configuration),他会调用 createOpenHelper 方法,查看 AccountDataBase_Impl 实现类中的 createOpenHelper 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class AccountDatabase_Impl extends AccountDatabase {
private volatile AccountDao _accountDao;

@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(2) {
@Override
public void createAllTables(SupportSQLiteDatabase _db) {
_db.execSQL("CREATE TABLE IF NOT EXISTS `Account` (`accountId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `_loginAccount` TEXT NOT NULL, `_loginPassword` TEXT NOT NULL, `_loginIpAddress` TEXT NOT NULL)");
_db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
_db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14a6d25a90b176163fb0e4b68c523836')");
}

...
}

...

}

createOpenHelper 方法通过 SupportSQLiteOpenHelper 类实现了创建数据表、删除数据表等方法。数据插入等操作采用的是同样的实现方式。


其他

1、可能会遇到的问题:

build.gradle(:app)
1
2
3
4
5
6
7
8
9
10
11
12
13
android {
...
defaultConfig {
...
// 指定room.schemaLocation生成的文件路径 处理Room 警告 Schema export Error
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
...
}

2、修改数据库版本号后报错:

1
java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

解决方法 1:设置 fallbackToDestructiveMigration ()(不推荐)

解决方法 2:使用 Migration 升级策略

3、AndroidStudio 4.1 及更高版本为开发者提供了 DataBase Inspector 工具,用来直接查看生成的数据库文件。运行程序后(有线连接)从菜单栏中依次选择 View > Tool Windows > App Inspection -> 选择 Database Inspector 标签页 -> 从下拉菜单中选择正在运行的应用进程就可以看到生成的数据库文件了。


备注

参考资料

Room

第一行代码 (第 3 版)

《Android Jetpack 开发 原理解析与应用实战》

相关文章

Android 账号登录

欢迎关注微信公众号:非也缘也