Room

SQLite数据库是使用了一些原生的API来进行数据的增删改查操作。这些原生API虽然简单易用,但是如果放到大型项目当中的话,会容易让项目的代码变得混乱,除非进行了很好的封装。为此出现了诸多专门为Android数据库设计的ORM框架。

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

relational [rɪˈleɪʃənl] 相关的;亲属的 mapping [ˈmæpɪŋ] 映射,映现

那么使用ORM框架有什么好处呢?它赋予了我们一个强大的功能,就是可以用面向对象的思维来和数据库进行交互,绝大多数情况下不用再和SQL语句打交道了, 同时也不用担心操作数据库的逻辑会让项目的整体代码变得混乱。

由于许多大型项目中会用到数据库的功能,为了帮助我们编写出更好的代码,Android官方推出 了一个ORM框架,并将它加入了Jetpack当中,就是Room

Room的优点:

  • 使用编译时注解,能够对@Query@Entity里面的SQL语句等进行验证;
  • SQL语句的使用更加贴近,能够降低学习成本;
  • RxJava 2的支持(大部分都Android数据库框架都支持),对LiveData的支持;
  • @Embedded能够减少表的创建;

简单来说:Room是一个基于SQLite的强大数据库框架。

1 使用Room进行增删改查

Room主要由EntityDaoDatabase3部分组成,每个部分都有明确的职责,详细说明如下:

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

entity [ˈentəti] 实体

第一步:添加依赖

要使用Room,需要在app/build.gradle文件中添加 如下的依赖:

plugins {...id 'kotlin-kapt'
}
dependencies {...implementation "androidx.room:room-runtime:2.2.6"implementation "androidx.room:room-ktx:2.2.6"kapt "androidx.room:room-compiler:2.2.6"androidTestImplementation "androidx.room:room-testing:2.2.6"
}

这里新增了一个kotlin-kapt插件,同时在dependencies中添加了两个Room的依赖库。 由于Room会根据我们在项目中声明的注解来动态生成代码,因此这里一定要使用kapt引入Room的编译时注解库,而启用编译时注解功能则一定要先添加kotlin-kapt插件。注意,kapt只能在Kotlin项目中使用,如果是Java项目的话,使用annotationProcessor即可。

第二步:创建实体类(表)

定义@Entity,也就是实体类。 一个良好的数据库编程建议是,给每个实体类都添加一个id字段,并将这个字段设为主键。以下是实体类的声明:

@Entity
data class User(var uname: String,var sex: Int,var age: Int,var city: String
) {@PrimaryKey(autoGenerate = true)var id: Long = 0
}

User的类名上使用@Entity注解,将它声明成了一个实体类,然后在User类中添加了一个id字段,并使用@PrimaryKey注解将它设为了主键,再把autoGenerate参数指定成true,使得主键的值是自动生成的。

这样实体类部分就定义好了。在实际项目当中,可能需要根据具体的业务逻辑定义很多个实体类。当每个实体类定义的方式都是差不多的,最多添加一些实体类之间的关联。

第三步:创建Dao

这部分是Room用法中最关键的地方,因为所有访问数据库的操作都是在这里封装的。 访问数据库的操作无非就是增删改查这4种,但是业务需求却是千变万化的。而Dao要做的事情就是覆盖所有的业务需求,使得业务方永远只需要与Dao层进行交互,而不必和底层的数据库打交道。

下面是一个Dao具体是如何实现的。新建一个UserDao接口,注意必须使用接口,然后在接口中编写如下代码:

@Dao
interface UserDao {@Insertfun insertUser(user: User): Long@Updatefun updateUser(newUser: User)@Query("select * from User")fun queryAllUsers(): List<User>@Query("select * from User where age > :age")fun queryOlderThan(age: Int): List<User>@Deletefun deleteUser(user: User)@Query("delete from User where uname = :uname")fun deleteUserByUName(uname: String): Int}

UserDao接口的上面使用了一个@Dao注解,这样Room才能将它识别成一个DaoUserDao的内部就是根据业务需求对各种数据库操作进行的封装。数据库操作通常有增删改查这4种,因此,Room也提供了@Insert@Delete@Update@Query4种相应的注解。

可以看到,insertUser()方法上面使用了@Insert注解,表示会将参数中传入的User对象插 入数据库中,插入完成后还会将自动生成的主键id值返回。updateUser()方法上面使用了@Update注解,表示会将参数中传入的User对象更新到数据库当中。deleteUser()方法上面 使用了@Delete注解,表示会将参数传入的User对象从数据库中删除。以上几种数据库操作都是直接使用注解标识即可,不用编写SQL语句。

但是如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,那么就必须编写SQL语句了。比如说我们在UserDao接口中定义了一个queryAllUsers()方法,用于从数据库中查询所有的用户,如果只使用一个@Query注解,Room将无法知道我们想要查询哪些数据, 因此必须在@Query注解中编写具体的SQL语句才行。我们还可以将方法中传入的参数指定到SQL语句当中,比如queryOlderThan()方法就可以查询所有年龄大于指定参数的用 户。另外,如果是使用非实体类参数来增删改数据,那么也要编写SQL语句才行,而且这个时候不能使用@Insert、@Delete@Update注解,而是都要使用@Query注解才行,如deleteUserByUName()方法的写法。

这样我们就大体定义了添加用户、修改用户数据、查询用户、删除用户这几种数据库操作接口,在实际项目中根据真实的业务需求来进行定义即可。

虽然使用Room需要经常编写SQL语句这一点不太友好,但是SQL语句确实可以实现更加多样化的逻辑,而且Room是支持在编译时动态检查SQL语句语法的。 也就是说,如果我们编写的SQL语句有语法错误,编译的时候就会直接报错,而不会将错误隐藏到运行的时候才发现,也算是大大减少了很多安全隐患吧。

第四步:创建数据库

接下来是定义Database。这部分内容的写法是非常固定的,只需要定义好3个部分的内容:数据库的版本号、包含哪些实体类,以及提供Dao层的访问实例。 新建一个UserDatabase文件,代码如下所示:

@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {abstract fun userDao(): UserDaocompanion object {private var instance: AppDatabase? = null@Synchronizedfun getDatabase(context: Context): AppDatabase {instance?.let { return it }return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").build().apply { instance = this }}}
}

可以看到,这里在AppDatabase类的头部使用了@Database注解,并在注解中声明了数据库的版本号以及包含哪些实体类,多个实体类之间用逗号隔开即可。

另外,AppDatabase类必须继承自RoomDatabase类,并且一定要使用abstract关键字将它声明成抽象类,然后提供相应的抽象方法,用于获取之前编写的Dao的实例, 比如这里提供的userDao()方法。不过我们只需要进行方法声明就可以了,具体的方法实现是由Room在底层自动完成的。

接着,在companion object结构体中编写了一个单例模式,因为原则上全局应该只存在一份AppDatabase的实例。这里使用了instance变量来缓存AppDatabase的实例,然后在getDatabase()方法中判断:如果instance变量不为空就直接返回,否则就调用 Room.databaseBuilder()方法来构建一个AppDatabase的实例。databaseBuilder()方法接收3个参数,注意第一个参数一定要使用applicationContext,而不能使用普通的context,否则容易出现内存泄漏的情况,第二个参数是AppDatabaseClass类型,第三个参数是数据库名,这些都比较简单。最后调用build()方法完成构建,并将创建出来的实例赋值给instance变量,然后返回当前实例即可。

使用:

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_stack)val userDao = AppDatabase.getDatabase(this).userDao()val user1 = User("萧峰", 1, 31, "南京")val user2 = User("段誉", 1, 24, "大理")val user3 = User("慕容复", 1, 24, "大理")addDataBtn.setOnClickListener {thread {user1.id = userDao.insertUser(user1)user2.id = userDao.insertUser(user2)user3.id = userDao.insertUser(user3)}}updateDataBtn.setOnClickListener {thread {user1.age = 32userDao.updateUser(user1)}}deleteDataBtn.setOnClickListener {thread {userDao.deleteUserByUName("慕容复")}}queryDataBtn.setOnClickListener {thread {for (user in userDao.queryAllUsers()) {Log.e("CAH", "queryAllUsers: ${user.toString()}")}}}}}

这段代码的逻辑很简单的。首先获取了UserDao的实例,并创建三个User对象。然后在Add Data按钮的点击事件中,调用了UserDaoinsertUser()方法,将这三个User对象插入数据库中,并将insertUser()方法返回的主键id值赋值给原来的User对象。

之所以要这么做,是因为使用@Update@Delete注解去更新和删除数据时都是基于这个id值来操作的。

然后在Update Data按钮的点击事件中,将user1的年龄修改成了32岁,并调用UserDaoupdateUser()方法来更新数据库中的数据。在Delete Data按钮的点击事件中,调用了UserDaodeleteUserByUName()方法,删除uname慕容复的用户。在Query Data按钮的点击事件中,调用了UserDaoqueryAllUsers()方法,查询并打印数据库中所有的用户。

另外,由于数据库操作属于耗时操作,Room默认是不允许在主线程中进行数据库操作的,因此 上述代码中我们将增删改查的功能都放到了子线程中。不过为了方便测试,Room还提供了一个 更加简单的方法,如下所示:

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").allowMainThreadQueries().build()

在构建AppDatabase实例的时候,加入一个allowMainThreadQueries()方法,这样Roo 就允许在主线程中进行数据库操作了,这个方法建议只在测试环境下使用。

运行,发现出现以下问题:

Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide room.schemaLocation annotation processor argument OR set exportSchema to false.

这是因为,Room会将数据库的架构信息导出为JSON文件(默认exportSchema = true导出架构)。导出架构,需要在build.gradle文件中设置room.schemaLocation注释处理器属性(设置将JSON存放的位置)。如果没有设置exportSchema = false不导出架构或者没有设置架构导出的位置,所以构建错误。
解决方法:

  1. build gradle中添加(推荐)
android {...defaultConfig {...javaCompileOptions {annotationProcessorOptions {arguments = ["room.schemaLocation":"$projectDir/schemas".toString()]}}}
}
  1. 在数据库注解中添加exportSchema = false(不推荐)
@Database(entities = {entity.class}, version = 4, exportSchema = false)

运行程序:

然后点击Add Data按钮,再点击Query Data按钮,查看Logcat中的打印日志,如图所示:

// CAH: queryAllUsers: User(uname=萧峰, sex=1, age=31, city=南京)
// CAH: queryAllUsers: User(uname=段誉, sex=1, age=24, city=大理)
// CAH: queryAllUsers: User(uname=慕容复, sex=1, age=24, city=大理)

由此可以证明,三条用户数据都已经被成功插入数据库当中了。

接下来点击Update Data按钮,再重新点击Query Data按钮,Logcat中的打印日志如图所示:

// CAH: queryAllUsers: User(uname=萧峰, sex=1, age=32, city=南京)
// CAH: queryAllUsers: User(uname=段誉, sex=1, age=24, city=大理)
// CAH: queryAllUsers: User(uname=慕容复, sex=1, age=24, city=大理)

可以看到,第一条数据中用户的年龄被成功修改成了32岁。

最后点击Delete Data按钮,再次点击Query Data按钮,Logcat中的打印日志如图所示:

// CAH: queryAllUsers: User(uname=萧峰, sex=1, age=32, city=南京)
// CAH: queryAllUsers: User(uname=段誉, sex=1, age=24, city=大理)

可以看到,现在只剩下一条用户数据了。

以上就是Room的用法。

2 Room的数据库升级

数据库结构不可能在设计好了之后就永远一成不变,随着需求和版本的变更,数据库也是需要升级的。不过遗憾的是,Room在数据库升级方面设计得非常烦琐,基本上没有比使用原生的SQLiteDatabase简单到哪儿去,每一次升级都需要手动编写升级逻辑才行。

不过,如果如果只是在开发测试阶段,不想编写那么烦琐的数据库升级逻辑,Room有一个简单粗暴的方法,如下所示:

Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").fallbackToDestructiveMigration().build()

在构建AppDatabase实例的时候,加入一个fallbackToDestructiveMigration()方法。这样只要数据库进行了升级,Room就会将当前的数据库销毁,然后再重新创建,随之而来的副作用就是之前数据库中的所有数据就全部丢失了。

假如产品还在开发和测试阶段,这个方法是可以使用的,但是一旦产品对外发布之后,如果造成了用户数据丢失,那可是严重的事故。因此接下来学习一下在Room中升级数据库的正规写法。

随着业务逻辑的升级,现在打算在数据库中添加一张Course表,那么首先要做的就是创建一 个Course的实体类,如下所示:

@Entity
data class Course(var subject: String, var teacher: String, var time: Long) {@PrimaryKey(autoGenerate = true)var id: Long = 0
}

可以看到,Course类中包含了主键id、学科、老师,时间这几个字段,并且还使用@Entity注解将它声明成了一个实体类。

然后创建一个CourseDao接口,并在其中随意定义一些API

@Dao
interface CourseDao {@Insertfun insertCourse(course: Course): Long@Query("select * from Course")fun queryAllCourses(): List<Course>
}

接下来修改AppDatabase中的代码,在里面编写数据库升级的逻辑,如下所示:

@Database(version = 2, entities = [User::class, Course::class])
abstract class AppDatabase : RoomDatabase() {abstract fun userDao(): UserDaoabstract fun courseDao(): CourseDaocompanion object {val MIGRATION_1_2 = object : Migration(1, 2) {override fun migrate(database: SupportSQLiteDatabase) {database.execSQL("create table Course (id integer primary key autoincrement not null, subject text not null, teacher text not null, time integer not null)")}}private var instance: AppDatabase? = null@Synchronizedfun getDatabase(context: Context): AppDatabase {instance?.let { return it }return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").addMigrations(MIGRATION_1_2).build().apply { instance = this }}}
}

观察一下这里的几处变化。首先在@Database注解中,将版本号升级成了2,并将Course类添加到了实体类声明中,然后又提供了一个courseDao()方法用于获取CourseDao的实例。

接下来就是关键的地方了,在companion object结构体中,实现了一个Migration的匿名类,并传入了12这两个参数,表示当数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑。匿名类实例的变量命名也比较有讲究,这里命名成MIGRATION_1_2,可读性更高。由于我们要新增一张Course表,所以需要在migrate()方法中编写相应的建表语句。另外必须注意的是,Course表的建表语句必须和Course实体类中声明的结构完全一致,否则Room就会抛出异常。

最后在构建AppDatabase实例的时候,加入一个addMigrations()方法,并把MIGRATION_1_2传入即可。

现在当我们进行任何数据库操作时,Room就会自动根据当前数据库的版本号执行这些升级逻辑,从而让数据库始终保证是最新的版本。

不过,每次数据库升级并不一定都要新增一张表,也有可能是向现有的表中添加新的列。这种情况只需要使用alter语句修改表结构就可以了,下面是具体的操作过程。

现在Course的实体类中只有id、学科、老师、时间这几个字段,如果想要再添加一个班级字段,代码如下所示:

@Entity
data class Course(var subject: String, var teacher: String, var time: Long, var className: String) {@PrimaryKey(autoGenerate = true)var id: Long = 0
}

既然实体类的字段发生了变动,那么对应的数据库表也必须升级了,所以这里修改AppDatabase中的代码,如下所示:

@Database(version = 3, entities = [User::class, Course::class])
abstract class AppDatabase : RoomDatabase() {abstract fun userDao(): UserDaoabstract fun courseDao(): CourseDaocompanion object {val MIGRATION_1_2 = object : Migration(1, 2) {override fun migrate(database: SupportSQLiteDatabase) {database.execSQL("create table Course (id integer primary key autoincrement not null, subject text not null, teacher text not null, time integer not null)")}}val MIGRATION_2_3 = object : Migration(2, 3) {override fun migrate(database: SupportSQLiteDatabase) {database.execSQL("alter table Course add column className text not null default 'unknown'")}}private var instance: AppDatabase? = null@Synchronizedfun getDatabase(context: Context): AppDatabase {instance?.let { return it }return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database").addMigrations(MIGRATION_1_2, MIGRATION_2_3).build().apply { instance = this }}}
}

升级步骤和之前是差不多的,这里先将版本号升级成了3,然后编写一个MIGRATION_2_3的升级逻辑并添加到addMigrations()方法中即可。比较有难度的地方就是每次在migrate()方法中编写的SQL语句,不过即使写错了也没关系,因为程序运行之后在你首次操作数据库的时候就会直接触发崩溃,并且告诉你具体的错误原因,对照着错误原因来改正你的SQL语句即可。

Jetpack(七)—— Room相关推荐

  1. Andriod --- JetPack (七):Room + ViewModel + LiveData 增删改查实例

    1.Andriod - JetPack (一):初识 JetPack 2.Andriod - JetPack (二):LifeCycle 的诞生 3.Andriod - JetPack (三):Vie ...

  2. Andriod --- JetPack (六):Room 增删改查

    1.Andriod - JetPack (一):初识 JetPack 2.Andriod - JetPack (二):LifeCycle 的诞生 3.Andriod - JetPack (三):Vie ...

  3. Andriod --- JetPack (四):BaseObservable 与 ObservableField 双向绑定

    1.Andriod - JetPack (一):初识 JetPack 2.Andriod - JetPack (二):LifeCycle 的诞生 3.Andriod - JetPack (三):Vie ...

  4. Andriod --- JetPack (五):DataBinding + LiveData +ViewModel 简单实例

    1.Andriod - JetPack (一):初识 JetPack 2.Andriod - JetPack (二):LifeCycle 的诞生 3.Andriod - JetPack (三):Vie ...

  5. Andriod --- JetPack (三):ViewModel 的诞生

    1.Andriod - JetPack (一):初识 JetPack 2.Andriod - JetPack (二):LifeCycle 的诞生 3.Andriod - JetPack (三):Vie ...

  6. Andriod --- JetPack (二):LifeCycle 的诞生

    1.Andriod - JetPack (一):初识 JetPack 2.Andriod - JetPack (二):LifeCycle 的诞生 3.Andriod - JetPack (三):Vie ...

  7. Andriod --- JetPack (一):初识 JetPack

    1. Andriod --- JetPack (一):初识 JetPack 2. Andriod --- JetPack (二):LifeCycle 的诞生 3. Andriod --- JetPac ...

  8. Android面试宝典2022-(停止更新,请看面试专栏)

    Android面试宝典2020-持续更新 一.Java基础 1.java基本数据类型和引用类型 2.object equals和==的区别 equals和hashcode的关系? 3.static关键 ...

  9. 深入探索Android 启动优化(七) - JetPack App Startup 使用及源码浅析

    本文首发我的微信公众号:徐公,想成为一名优秀的 Android 开发者,需要一份完备的 知识体系,在这里,让我们一起成长,变得更好~. 前言 前一阵子,写了几篇 Android 启动优化的文章,主要是 ...

最新文章

  1. 阿里云弹性公网IP(EIP)的使用限制
  2. 【opencv】26.图像水平边缘和竖直边缘的算子数学分析
  3. TFS2010迁移后Web工作项访问提示:error HRESULT E_FAIL has been returned from a call to a COM component....
  4. 谈Servlet与JSP
  5. 配置ssh信任(不通过密码验证ssh直接访问目标机器)
  6. python输出输入的指定位数的密码_用python生成指定位数的密码
  7. 四面轻松突围!我是如何斩获高级运维开发一职的?
  8. coreseek mysql_coreseek,php,mysql全文检索部署(一)
  9. 刚办的电信卡显示无服务器,刚刚买的电信卡怎么不能用说什么只限紧急呼叫
  10. 『原创』老范的来电防火墙v1.0发布了(图文)
  11. 转载《一个射频工程师的职场日记》
  12. 服务器出现漏洞如何处理
  13. Alsa 调试下篇:应用篇
  14. 12306怎样才能防止抢票?
  15. 平面设计中立体表现技法
  16. L1-020. 帅到没朋友(2016)
  17. 判断并求出两个圆的交点(平面几何)
  18. 使用potplayer播放器看直播
  19. Socket 系统调用深入研究(TCP协议的整个通信过程)
  20. Pragmatic eBook 介绍 :Test-Driving JavaScript Applications

热门文章

  1. LeetCode第9题 回文数(Palindrome Number)
  2. 三星性能测试软件,三星T7 性能测试
  3. 云麦体脂秤华为体脂秤_荣耀体脂秤和小米体脂秤对比哪个好 荣耀/小米体脂秤评测...
  4. 爱的五种语言:创造完美的两性沟通
  5. Linux关闭/禁用触摸屏,deepin关闭/禁用触摸屏方法
  6. 连英文都不懂怎么学python_不懂英文能学Python吗?
  7. 微信公众号的八大价值
  8. android中录音断点播放,Android实现暂停--继续录音(AudioRecord)
  9. 1024 程序员节,带你走进程序员的世界——
  10. 计算机课情感态度与价值观,浅谈信息技术课中情感态度价值观的培养