《Android 编程权威指南》学习笔记 : 第11章 数据库与 Room 库
第11章 数据库与 Room 库
资料
引 Room 库
要使用 Room,首先添加项目依赖
- room-runime
- room-complier
在 app/build.gradle 中添加
apply plugin: 'kotlin-kapt' // AS插件:Kotlin注解处理器,
...
dependencies {
...
//Room
implementation 'androidx.room:room-runtime:2.4.2' // Room API:包含创建数据库的需要的类和注解
kapt 'androidx.room:room-compiler:2.4.2' //Room编译器,基于注解自动生成代码
}
添加依赖后,记得 sync now
定义实体
代码清单:com.example.criminalintent/Crime.kt
@Entity
data class Crime(
@PrimaryKey val id:UUID = UUID.randomUUID(),
var title: String = "",
var date: Date = Date(),
var isSolved: Boolean = false
)
- @Entity:表明此类是数据库实体类
- @PrimaryKey:表明主键
创建数据库类
代码清单:app/src/main/java/com.example.criminalintent/database/CrimeDatabase.kt
@Database(entities = [ Crime::class], version = 1)
@TypeConverters(CrimeTypeConverters::class)
abstract class CrimeDatabase : RoomDatabase() {
abstract fun crimeDao(): CrimeDao
}
- 继承 RoomDatabase(),是抽象类
- @Database(entities = [ Crime::class], version = 1):表明实体的集合、数据库版本
- @TypeConverters(CrimeTypeConverters::class) 表明提供的类型转换器
创建类型转换器
Room 能直接在后台SQLite数据库表里存储基本数据类型,但是其它类型,比如:UUID,Date类型是无法识别的,这时就需要类型转换器
代码清单:src/app/main/java/com.example.criminalintent/database/CrimeTypeConverters.kt
class CrimeTypeConverters {
@TypeConverter
fun fromDate(date: Date?): Long? {
return date?.time
}
@TypeConverter
fun toDate(millisSinceEpoch: Long?): Date? {
return millisSinceEpoch?.let {
Date(it)
}
}
@TypeConverter
fun fromUUID(uuid: UUID?): String? {
return uuid?.toString()
}
@TypeConverter
fun toUUID(uuid: String?): UUID? {
return UUID.fromString(uuid)
}
}
- 记得添加注释: @TypeConverter
创建数据访问对象(DAO)
代码清单:src/app/main/java/com.example.criminalintent/database/CrimeDao.kt
定义数据访问对象一个接口
@Dao
interface CrimeDao {
@Query("select * from crime")
fun getCrimes(): List<Crime>
@Query("select * from crime where id=(:id)")
fun getCrime(id: UUID): Crime?
}
- @Query(): 注释SQL语句,Room工具会自动生成 Kotlin 代码
仓储(单例)
推荐使用仓储模式访问数据
代码清单:src/app/main/java/com.example.criminalintent/CrimeRepository.kt
class CrimeRepository private constructor(context: Context) {
private val database: CrimeDatabase = Room.databaseBuilder(
context.applicationContext,
CrimeDatabase::class.java,
DATABASE_NAME
).build()
private val crimeDao = database.crimeDao()
fun getCrimes(): List<Crime> = crimeDao.getCrimes()
fun getCrime(id: UUID): Crime? = crimeDao.getCrime(id)
companion object {
private var INSTANCE: CrimeRepository? = null
fun initialize(context: Context) {
if(INSTANCE == null) {
INSTANCE = CrimeRepository(context)
}
}
fun get(): CrimeRepository {
return INSTANCE ?:
throw IllegalStateException("CrimeRepository must be initialize")
}
}
}
- 使用 Room.databaseBuilder(...,...,...,).build() 创建抽象类 CrimeDatabase::class.java 的具体实现类 val database: CrimeDatabase
- 仓储再定义引用 private val crimeDao = database.crimeDao()
创建 Appication 类
定义 CriminalIntentApplication:Application() 应用程序类,在 onCreate()中初始化 仓储
代码清单:app/src/main/java/com.example.criminalintent/CrimeRepository.kt
class CriminalIntentApplication : Application() {
override fun onCreate() {
super.onCreate()
CrimeRepository.initialize(this)
}
}
登记应用程序类
在 AndroidManifest.xml 登记登记应用程序类 Appication 类
代码清单:app/src/main/AndroidManifest.xml
<application
android:name=".CriminalIntentApplication"
...
/>
ViewModel 中修改数据访问
代码清单:src/app/main/java/com.example.criminalintent/CrimeListViewModel.kt
class CrimeListViewModel : ViewModel() {
// val crimes = mutableListOf<Crime>()
// init {
// for (i in 0 until 100){
// val crime = Crime()
// crime.title = "Crime #$i"
// crime.isSolved = i%2 == 0
// crimes += crime
// }
// }
private val crimeRepository: CrimeRepository = CrimeRepository.get()
var crimes = crimeRepository.getCrimes()
}
上传已存在的数据库
为了测试,上传已经有数据的数据库表,运行模拟器后,在其 【Device File Exploer】中 找到**data/date**目录
,
找到应用程序目录 data/date/com.example.criminalintent,右键菜单选择【Upload...】菜单项,在本电脑中找到随书示例代码章节中找到数据库文件,点击上传
本书随书资料下载:https://www.ituring.com.cn/book/2771
运行崩溃
运行模拟器,抛出异常:
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
程序崩溃原因:主线程(UI)线程中不能访问数据库,访问数据库是个耗时的任务,堵塞主线程。
应用线程
- 主线程:处于一个循环的运行状态,主要响应UI相关事件,故也称UI线程
- 后台线程:主线程外的其它线程,
后台线程
添加后台线程的原则:
- 所有耗时的任务都应该在后台线程上完成
- UI只能在主线程上更新
Android上能让我们在后台线程上执行任务的方法:
- 第24章 异步网络请求
- 第25章 Handler 处理后台小任务
- 第27章 WorkManager 执行周期性的后台任务
- 第12章 Executor 来插入和更新数据库
- 本章节 LiveData
使用 LiveData
LiveData 能在线程间传递数据,满足添加后台线程的原则。
Room 原生支持与 LiveData 协调工作
引用 LiveData 库
代码清单:app/build.grale
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
之前要使用 ViewModel 时已经添加依赖了,这不用再添加了。
修改创建数据访问对象(DAO)
代码清单:src/app/main/java/com.example.criminalintent/database/CrimeDao.kt
@Dao
interface CrimeDao {
@Query("select * from crime")
//fun getCrimes(): List<Crime>
fun getCrimes(): LiveData<List<Crime>>
@Query("select * from crime where id=(:id)")
//fun getCrime(id: UUID): Crime?
fun getCrime(id: UUID): LiveData<Crime?>
}
从DAO 类返回ListData 实例,就是告诉Room 要在后台线程上执行数据库查询。
查询到 crime 数据后,LiveData 对象会把结果发送到主线程并通知 UI观察者。
修改仓储类
修改仓储类的查询函数,返回值为 ListData<T>
类型:
List<List<Crime>>
List<Crime>
代码清单:src/app/main/java/com.example.criminalintent/CrimeRepository.kt
class CrimeRepository private constructor(context: Context) {
...
//fun getCrimes(): List<Crime> = crimeDao.getCrimes()
fun getCrimes(): LiveData<List<Crime>> = crimeDao.getCrimes()
//fun getCrime(id: UUID): Crime? = crimeDao.getCrime(id)
fun getCrime(id: UUID): LiveData<Crime?> = crimeDao.getCrime(id)
...
}
观察 LiveData
重构名字
把 ViewModel中 crimes 的重命名为更贴切的名字: crimeLiveData,
代码清单:src/app/main/java/com.example.criminalintent/CrimeListViewModel.kt
class CrimeListViewModel : ViewModel() {
private val crimeRepository: CrimeRepository = CrimeRepository.get()
//var crimes = crimeRepository.getCrimes()
var crimesLiveData = crimeRepository.getCrimes()
}
修改更新 updateUI 函数
代码清单:src/app/main/java/com.example.criminalintent/CrimeListFragment.kt
private fun updateUI(crimes: List<Crime>) {
adapter = CrimeAdapter(crimes)
crimeRecycleView.adapter = adapter
}
添加 LiveData 观察者
代码清单:src/app/main/java/com.example.criminalintent/CrimeListFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
crimeListViewModel.crimesLiveData.observe(
viewLifecycleOwner, // 保证Observer与fragment生命周期同步
Observer { crimes -> // 在LiveData的数据变更时执行
crimes?.let {
Log.i(TAG, "Got crimes ${crimes.size}")
updateUI(crimes)
}
}
)
}
- 是在 onViewCreated()写代码,是保证 fragment的视图已经创建完成后,才能更新视图UI。
- 调用函数 crimeListViewModel.crimesLiveData.observe(生命周期拥有者,Observer { ... }) 添加 LiveData 的观察者
- viewLifecycleOwner:生命周期拥有者,是Fragment内置成员(注意:是fragment的视图生命周期拥有者,而不是 fragment本身,不过默认这两者也是生命周期一致的),
传入该值参保证Observer与fragment生命周期同步,在fragment被销毁后,解除Observer与LiveData的订阅关系,与其拥有者视图共存亡。 - Observer { ... } : 在LiveData的数据变更时执行
运行结果
数据库的Schema
运行程序后,有条警告
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.
数据库的Schema 就是数据库结果,包含的注意元素:
- 数据库里有哪些表
- 表里有哪些栏位
- 表与表间的关系和约束
Room 支持导出数据库的Schema 到一个文件, 保存在版本控制系统中进行版本的历史控制。
要消除上面的警告,有两个方法:
- 提供文件路径,保存Schema
在 app/build.gradle 文件添加 kapt {}代码块
...
android {
compileSdk 32
buildTypes {
...
}
...
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
}
添加后记得,Sync Now
再次运行模拟器,警告没有了
而这时,项目中多出了一个目录 app/schemas/com.example.criminalintent.database.CrimeDatabase
文件1.json的内容如下:
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "2de443d76b568d6e694b91d2e7d7d3e3",
"entities": [
{
"tableName": "Crime",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `date` INTEGER NOT NULL, `isSolved` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSolved",
"columnName": "isSolved",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"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, '2de443d76b568d6e694b91d2e7d7d3e3')"
]
}
}
- 禁用 schema导出功能,可以将 exportSchema 设置为 false:
代码清单:src/app/main/java/com.example.criminalintent/database/CrimeDatabase.kt
@Database(entities = [ Crime::class], version = 1, exportSchema = false)
@TypeConverters(CrimeTypeConverters::class)
abstract class CrimeDatabase : RoomDatabase() {
abstract fun crimeDao(): CrimeDao
}