Android官方数据库框架-Room

简介: Android官方数据库框架-Room

前言

Android 开发者使用数据库的时候,最先想到的是 SQLite。如果有对外公开的需求,则需再包装一层 ContentProvider。除此之外,也可以选择开源的数据库框架,比如 GreenDaoDBFlow等。

本文将讲述 Google 推出的数据库框架 Room,和您一起探讨: 如何使用 Room、其实现的大致原理以及它的优势

简介

Room 是房间的意思。房间除了能存放物品,还能带给人温暖和安心的感觉。用 Room 给这个抽象的软件架构命名,增加了人文色彩,很有温度。

先来看一下 Room 框架的基本组件。


1686637392402.pngRoom框架的基本组件.png

使用起来大体就是这几个步骤,很便捷。

1686637410535.png


Room使用步骤.png

使用前需要构筑如下依赖。

dependencies {
  def room_version = "2.2.6"
  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"
  implementation "androidx.room:room-ktx:$room_version"
  testImplementation "androidx.room:room-testing:$room_version"
}

实战

下面将通过一个展示电影列表的 demo 演示 Room 框架的使用。

组件构建

首先构建一个电影表 Movie,有名称、演员、上映年份、评分这么几个字段。

@Entity
class Movie() : BaseObservable() {
    @PrimaryKey(autoGenerate = true)
    var id = 0
    @ColumnInfo(name = "movie_name", defaultValue = "Harry Potter")
    lateinit var name: String
    @ColumnInfo(name = "actor_name", defaultValue = "Jack Daniel")
    lateinit var actor: String
    @ColumnInfo(name = "post_year", defaultValue = "1999")
    var year = 1999
    @ColumnInfo(name = "review_score", defaultValue = "8.0")
    var score = 8.0
}
  • @Entity 表示数据库中的表
  • @PrimaryKey 表示主键,autoGenerate 表示自增
  • @ColumnInfo 表示字段,name 表示字段名称

然后构建一个访问 Movie 表的 DAO 接口。

interface MovieDao {
    @Insert
    fun insert(vararg movies: Movie?): LongArray?
    @Delete
    fun delete(movie: Movie?): Int
    @Update
    fun update(vararg movies: Movie?): Int
    @get:Query("SELECT * FROM movie")
    val allMovies: LiveData<List<Movie?>?>
}
  • @Dao 表示访问 DB 的方法,需要声明为接口或抽象类,编译阶段将生成 _Impl 实现类,此处则将生成 MovieDao_Impl.java 文件
  • @Insert@Delete@Update@Query 分别表示数据库的增删改查方法

最后需要构建 Room 使用的入口 RoomDatabase

@Database(entities = [Movie::class], version = 1)
abstract class MovieDataBase : RoomDatabase() {
    abstract fun movieDao(): MovieDao
    companion object {
        @Volatile
        private var sInstance: MovieDataBase? = null
        private const val DATA_BASE_NAME = "jetpack_movie.db"
        @JvmStatic
        fun getInstance(context: Context): MovieDataBase? {
            if (sInstance == null) {
                synchronized(MovieDataBase::class.java) {
                    if (sInstance == null) {
                        sInstance = createInstance(context)
                    }
                }
            }
            return sInstance
        }
        private fun createInstance(context: Context): MovieDataBase {
            return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
                    ...
                    .build()
        }
    }
}
  • @Database 表示继承自 RoomDatabase 的抽象类,entities 指定表的实现类列表,version 指定了 DB 版本
  • 必须提供获取 DAO 接口的抽象方法,比如上面定义的 movieDao(),Room 将通过这个方法实例化 DAO 接口
  • RoomDatabase 实例的内存开销较大,建议使用单例模式管理
  • 编译时将生成 _Impl 实现类,此处将生成 MovieDataBase_Impl.java 文件

组件调用

本 demo 将结合 ViewModelRoom 进行数据交互,依赖 LiveData 进行异步查询,画面上则采用 Databinding 将数据和视图自动绑定。

class DemoActivity : AppCompatActivity() {
    private var movieViewModel: MovieViewModel? = null
    private var binding: ActivityRoomDbBinding? = null
    private var movieList: List<Movie?>? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityRoomDbBinding.inflate(layoutInflater)
        setContentView(binding!!.root)
        binding!!.lifecycleOwner = this
        movieViewModel = ViewModelProvider(this).get(MovieViewModel::class.java)
        movieViewModel?.getMovieList(this, { movieList: List<Movie?>? ->
            if (movieList == null) return@getMovieList
            this.movieList = movieList
            binding?.setMovieList(movieList)
        })
    }
}

ViewModel 通过 MediatorLiveData 担当列表查询的中介,当 DB 初始化结束后再更新 UI。

private val mediatorLiveData = MediatorLiveData<List<Movie?>?>()
    private val db: MovieDataBase?
    private val mContext: Context
    init {
        mContext = application
        db = MovieDataBase.getInstance(mContext)
        if (db != null) {
            mediatorLiveData.addSource(db.movieDao().allMovies) { movieList ->
                if (db.databaseCreated.value != null) {
                    mediatorLiveData.postValue(movieList)
                }
            }
        };
    }
    fun getMovieList(owner: LifecycleOwner?, observer: Observer<List<Movie?>?>?) {
        if (owner != null && observer != null)
            mediatorLiveData.observe(owner, observer)
    }
}

RoomDatabase 创建后异步插入初始化数据,并通知 MediatorLiveData

val databaseCreated = MutableLiveData<Boolean?>()
    ...
    companion object {
        ...
        private fun createInstance(context: Context): MovieDataBase {
            return Room.databaseBuilder(context.applicationContext, ...)
                    ...
                    .addCallback(object : Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            Executors.newFixedThreadPool(5).execute {
                                val dataBase = getInstance(context)
                                val ids = dataBase!!.movieDao().insert(*Utils.initData)
                                dataBase.databaseCreated.postValue(true)
                            }
                        }
                        ...
                    })
                    .build()
        }
    }
}

运行效果

1686637444772.png

Demo效果.png

通过 Database Inspector 工具可以看到 DB 数据创建成功了。Database Inspector 支持实时刷新,查询和修改等 DB 操作,是 DB 开发的利器。

如果不知道如何使用Database Inspector,可参考官方文档使用 Database Inspector 调试数据库

1686637466848.png

查看数据库.png

DAO 的具体使用

@Insert

@Insert 支持设置冲突策略,默认为 OnConflictStrategy.ABORT 即中止并回滚。还可以指定为其他策略。

  • OnConflictStrategy.REPLACE 冲突时替换为新记录
  • OnConflictStrategy.IGNORE 忽略冲突 (不推荐)
  • OnConflictStrategy.ROLLBACK 已废弃,使用 ABORT 替代
  • OnConflictStrategy.FAIL 同上

其声明的方法返回值可为空,也可为插入行的 ID 或列表。

  • fun insertWithOutId(movie: Movie?)
  • fun insert(movie: Movie?): Long?
  • fun insert(vararg movies: Movie?): LongArray?
@Delete

@Insert 一样支持不返回删除结果或返回删除的函数,不再赘述。

@ Update

@Insert 一样支持设置冲突策略和定制返回更新结果。此外需要注意的是 @Update 操作将匹配参数的主键 id 去更新字段。

  • fun update(vararg movies: Movie?): Int
@ Query

查询操作主要依赖 @Update 的 value,指定不同的 SQL 语句即可获得相应的查询结果。在编译阶段就将验证语句是否正确,避免错误的查询语句影响到运行阶段。

  • 查询所有字段
    @get:Query(“SELECT * FROM movie”)
  • 查询指定字段
    @get:Query(“SELECT id, movie_name, actor_name, post_year, review_score FROM movie”)
  • 排序查询
    @get:Query(“SELECT * FROM movie ORDER BY post_year DESC”) 比如查询最近发行的电影列表
  • 匹配查询
    @Query(“SELECT * FROM movie WHERE id = :id”)
  • 多字段匹配查询
    @Query(“SELECT * FROM movie WHERE movie_name LIKE :keyWord " + " OR actor_name LIKE :keyWord”) 比如查询名称和演员中匹配关键字的电影
  • 模糊查询
    @Query(“SELECT * FROM movie WHERE movie_name LIKE ‘%’ || :keyWord || ‘%’ " + " OR actor_name LIKE ‘%’ || :keyWord || ‘%’”) 比如查询名称和演员中包含关键字的电影
  • 限制行数查询
    @Query(“SELECT * FROM movie WHERE movie_name LIKE :keyWord LIMIT 3”) 比如查询名称匹配关键字的前三部电影
  • 参数引用查询
    @Query(“SELECT * FROM movie WHERE review_score >= :minScore”) 比如查询评分大于指定分数的电影
  • 多参数查询
    @Query(“SELECT * FROM movie WHERE post_year BETWEEN :minYear AND :maxYear”) 比如查询介于发行年份区间的电影
  • 不定参数查询
    @Query(“SELECT * FROM movie WHERE movie_name IN (:keyWords)”)
  • Cursor 查询
    @Query(“SELECT * FROM movie WHERE movie_name LIKE ‘%’ || :keyWord || ‘%’ LIMIT :limit”)
    fun searchMoveCursorByLimit(keyWord: String?, limit: Int): Cursor?
    注意: Cursor 需要保证查询到的字段和取值一一对应,所以不推荐使用
  • 响应式查询
    demo 采用的 LiveData 进行的观察式查询,还可以配合 RxJava2,Kotlin 的 Flow 进行响应式查询。

进阶使用

数据库升级降级

在 Movie 类里增加了新字段后,重新运行已创建过 DB 的 demo 会发生崩溃。

Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.

@Database 的 version 升级为 2 之后再次运行仍然发生崩溃。

A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow fallback of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

提醒我们调用 fallbackToDestructiveMigration() 以允许升级失败时破坏性地删除 DB。

如果照做的话,将能避免发生崩溃,并且 onDestructiveMigration() 将被回调。在这个回调里可以试着重新初始化 DB。

private fun createInstance(context: Context): MovieDataBase {
    return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
            .fallbackToDestructiveMigration()
            .addCallback(object : Callback() {
                override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
                    super.onDestructiveMigration(db)
                    // Init DB again after db removed.
                    Executors.newFixedThreadPool(5).execute {
                        val dataBase = getInstance(context)
                        val ids = dataBase!!.movieDao().insert(*Utils.initData)
                        dataBase.databaseCreated.postValue(true)
                    }
                }
            })
            .build()
}

但是 DB 升级后,无论原有数据被删除还是重新初始化都是用户难以接受的。

我们可以通过 addMigrations() 指定升级之后的迁移处理来达到保留旧数据和增加新字段的双赢。

比如如下展示的从版本 1 升级到版本 2,并增加一个默认值为 8.0 的评分列的迁移处理。

private fun createInstance(context: Context): MovieDataBase {
    return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
            // .fallbackToDestructiveMigration()
            .addMigrations(object : Migration(1, 2) {
                override fun migrate(database: SupportSQLiteDatabase) {
                    database.execSQL("ALTER TABLE movie "
                            + " ADD COLUMN review_score INTEGER NOT NULL DEFAULT 8.0")
                }
            })
            ...
            })
            .build()
}

注意:

  • 降级则调用:
    fallbackToDestructiveMigrationOnDowngrade() 来指定在降级时删除 DB,也可以像上述那样指定 drop column 来进行数据迁移。
  • 如果想要迁移数据,无论是升级还是降级,必须要给 @Database 的 version 指定正确的目标版本。Migration 迁移处理的起始版本以及实际的迁移处理 migrate() 都必不可少。
事务处理

当我们的 DB 操作需要保持一致性,或者查询关联性结果的时候需要保证事务处理。Room 提供了 @Transaction 注解帮助我们快速实现这个需求,它将确保注解内的方法运行在同一个事务模式

@Dao
public interface MovieDao {
    @Transaction
    default void insetNewAndDeleteOld(Movie newMovie, Movie oldMovie) {
        insert(newMovie);
        delete(oldMovie);
    }
}

需要注意的是,事务处理比较占用性能,避免在事务处理的方法内执行耗时逻辑

另外,@Inset@Delete@Update 的处理自动在事务模式进行处理,无需增加 @Transaction 注解。

public long[] insert(final Movie... movies) {
    __db.assertNotSuspendingTransaction();
    __db.beginTransaction();
    try {
        long[] _result = __insertionAdapterOfMovie.insertAndReturnIdsArray(movies);
        __db.setTransactionSuccessful();
        return _result;
    } finally {
        __db.endTransaction();
    }
}

上面的源码也启发我们可以手动执行事务处理,一般来说不需要,取决于具体情况。

RoomDatabase 的 beginTransaction() 和 endTransaction() 不推荐外部使用了,可以采用封装好的 runInTransaction() 实现。

db.runInTransaction(Runnable {
    val database = db.getOpenHelper().getWritableDatabase();
    val contentValues = ContentValues()
    contentValues.put("movie_name", newMovie.getName())
    contentValues.put("actor_name", newMovie.getActor())
    contentValues.put("post_year", newMovie.getYear())
    contentValues.put("review_score", newMovie.getScore())
    database.insert("movie", SQLiteDatabase.CONFLICT_ABORT, contentValues)
    database.delete("movie", "id = " + oldMovie.getId(), null)
})

原理浅谈

简要介绍下 Room 的部分实现原理。因篇幅有限只展示关键流程,感兴趣者可自行探究具体代码。

RoomDatabase 的创建

RoomDatabase$Builder 的 build() 调用后便通过反射创建了 @Databse 注解声明的 RoomDatabase 实例 XXX_Impl。

1686637494490.png

SupportSQLiteDatabase 的创建

SupportSQLiteDatabase 是模仿 SQLiteDatabase 作成的接口,供 Room 框架内部对 DB 进行操作。由 FrameworkSQLiteDatabase 实现,其将通过内部持有的 SQLiteDatabase 实例,代理 DB 操作。

SupportSQLiteDatabase 的创建由增删改查等 DB 操作触发,需要经历 DB 的创建,表的创建,表的初始化,升降级以及打开等过程。

创建 DB 文件


1686637523119.png

创建表

1686637544962.png


创建表.png

初始化表

1686637563826.png

升级表

DB 文件已经存在并且版本和目标版本不一致的话,将执行数据迁移。但如果迁移处理未配置或者执行失败了便将删除 DB 并执行相应的回调。

1686637579431.png

打开表

DB 的创建或升级都正常完成后将回调 onOpen()

1686637599887.png

打开表.png

注意

Room 框架的使用过程中遇到了些容易出错的地方,需要格外留意。

  • RoomDatabase 的实例建议采用单例模式管理
  • 不要在 UI 线程执行 DB 操作,否则发生异常:
    Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
    通过调用:
    allowMainThreadQueries() 可以回避,但不推荐。
  • 不要在 Callback#onCreate() 里同步执行 insert 等 DB 处理,否则将阻塞 DB 实例的初始化并发生异常: getDatabase called recursively
  • @Entity 注解类不要提供多个构造函数,使用 @Ignore 可以回避。
  • Callback#onCreate() 并非由 RoomDatabase$Builder#build() 触发,而是由具体的增删改查操作触发,切记。

结语

通过上述的实战和原理介绍可以看出,Room 的本质是在 SQLite 的基础上进行封装的抽象层,通过一系列注解让用户能够更简便的使用 SQLite。正因为此,它具备了一些优势,值得开发者大胆使用。

  • 声明注解便能完成接口的定义,易上手
  • 编译阶段将验证注解里声明的 SQL 语句,提高了开发效率
  • 支持使用 RxJava2,LiveData 以及 Flow 进行异步查询
  • 相较其他数据库框架 SQL 执行效率更高

Demo参见Github-JetpackDemo

参考资料


相关文章
|
12天前
|
前端开发 Java 编译器
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
70 36
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
|
5天前
|
存储 缓存 Java
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
20 3
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
|
6月前
|
物联网 区块链 vr&ar
未来已来:探索区块链、物联网与虚拟现实技术的融合与应用安卓与iOS开发中的跨平台框架选择
【8月更文挑战第30天】在科技的巨轮下,新技术不断涌现,引领着社会进步。本文将聚焦于当前最前沿的技术——区块链、物联网和虚拟现实,探讨它们各自的发展趋势及其在未来可能的应用场景。我们将从这些技术的基本定义出发,逐步深入到它们的相互作用和集成应用,最后展望它们如何共同塑造一个全新的数字生态系统。
|
3月前
|
算法 JavaScript Android开发
|
4月前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
196 1
|
3月前
|
开发框架 Dart Android开发
安卓与iOS的跨平台开发:Flutter框架深度解析
在移动应用开发的海洋中,Flutter作为一艘灵活的帆船,正引领着开发者们驶向跨平台开发的新纪元。本文将揭开Flutter神秘的面纱,从其架构到核心特性,再到实际应用案例,我们将一同探索这个由谷歌打造的开源UI工具包如何让安卓与iOS应用开发变得更加高效而统一。你将看到,借助Flutter,打造精美、高性能的应用不再是难题,而是变成了一场创造性的旅程。
|
5月前
|
前端开发 Java 数据库
💡Android开发者必看!掌握这5大框架,轻松打造爆款应用不是梦!🏆
在Android开发领域,框架犹如指路明灯,助力开发者加速应用开发并提升品质。本文将介绍五大必备框架:Retrofit简化网络请求,Room优化数据库访问,MVVM架构提高代码可维护性,Dagger 2管理依赖注入,Jetpack Compose革新UI开发。掌握这些框架,助你在竞争激烈的市场中脱颖而出,打造爆款应用。
531 3
|
5月前
|
编译器 Android开发 开发者
带你了解Android Jetpack库中的依赖注入框架:Hilt
本文介绍了Hilt,这是Google为Android开发的依赖注入框架,基于Dagger构建,旨在简化依赖注入过程。Hilt通过自动化的组件和注解减少了DI的样板代码,提高了应用的可测试性和可维护性。文章详细讲解了Hilt的主要概念、基本用法及原理,帮助开发者更好地理解和应用Hilt。
138 8
|
6月前
|
设计模式 Java Android开发
探索安卓应用开发:从新手到专家的旅程探索iOS开发中的SwiftUI框架
【8月更文挑战第29天】本文旨在通过一个易于理解的旅程比喻,带领读者深入探讨安卓应用开发的各个方面。我们将从基础概念入手,逐步过渡到高级技术,最后讨论如何维护和推广你的应用。无论你是编程新手还是有经验的开发者,这篇文章都将为你提供有价值的见解和实用的代码示例。让我们一起开始这段激动人心的旅程吧!
|
6月前
|
Android开发
基于Amlogic 安卓9.0, 驱动简说(三):使用misc框架,让驱动更简单
如何使用Amlogic T972安卓9.0系统上的misc框架来简化驱动程序开发,通过misc框架自动分配设备号并创建设备文件,从而减少代码量并避免设备号冲突。
80 0
基于Amlogic 安卓9.0, 驱动简说(三):使用misc框架,让驱动更简单

热门文章

最新文章

  • 1
    如何修复 Android 和 Windows 不支持视频编解码器的问题?
  • 2
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 3
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 4
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 5
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 6
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 7
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
  • 8
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 9
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
  • 10
    向量数据库 milvus 快速入门