• 《Android 编程权威指南》学习笔记 : 第11章 数据库与 Room 库


    第11章 数据库与 Room 库

    资料

    使用 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 到一个文件, 保存在版本控制系统中进行版本的历史控制。

    要消除上面的警告,有两个方法:

    1. 提供文件路径,保存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')"
        ]
      }
    }
    
    1. 禁用 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
    }
    
  • 相关阅读:
    [LintCode] 最长上升子序列
    [LintCode] 最长公共前缀
    [LintCode] A + B 问题
    [hihoCoder] 拓扑排序·一
    [LintCode] 拓扑排序
    [LintCode] 第k大元素
    [LintCode] 最小路径和
    [LeetCode] Factorial Trailing Zeros
    [LintCode] 尾部的零
    [LeetCode] Length of Last Word
  • 原文地址:https://www.cnblogs.com/easy5weikai/p/16323154.html
Copyright © 2020-2023  润新知