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

参考资料


相关文章
|
4月前
|
数据库 Android开发
Android使用EditText+Listview实现搜索效果(使用room模糊查询)
本文介绍如何在Android中使用EditText与ListView实现搜索功能,并结合Room数据库完成模糊查询。主要内容包括:Room的模糊查询语句(使用`||`代替`+`号)、布局美化(如去除ListView分割线和EditText下划线)、EditText回车事件监听,以及查询逻辑代码示例。此外,还提供了相关扩展文章链接,帮助读者深入了解ListView优化、动态搜索及Room基础操作。
296 65
|
4月前
|
人工智能 数据挖掘 API
基于neo4j数据库和dify大模型框架的rag模型搭建——后续补充
基于neo4j数据库和dify大模型框架的rag模型搭建——后续补充
452 21
基于neo4j数据库和dify大模型框架的rag模型搭建——后续补充
|
4月前
|
Java 数据库 Docker
基于neo4j数据库和dify大模型框架的rag模型搭建
基于neo4j数据库和dify大模型框架的rag模型搭建
909 35
|
2月前
|
SQL XML Java
配置Spring框架以连接SQL Server数据库
最后,需要集成Spring配置到应用中,这通常在 `main`方法或者Spring Boot的应用配置类中通过加载XML配置或使用注解来实现。
173 0
|
4月前
|
XML 数据库 Android开发
Android数据库的使用(增删改查)
本文介绍了一个简单的数据库操作Demo,包含创建数据库、增删改查功能。通过5个按钮分别实现创建数据库、插入数据、删除数据、更新数据和查询数据的操作。代码结构清晰,适合初学者学习Android SQLite数据库基础操作。
113 5
|
4月前
|
数据库 Android开发
Android外部数据库的引用
简介:本文介绍了在Android项目中引用外部数据库的方法。首先,将现成的数据库文件放入项目的`assets`文件夹中(需手动创建)。其次,在APP引导界面通过代码将数据库拷贝至App目录下,确保数据库可用。最后,对数据库进行增删改查等操作。关键步骤包括判断数据库是否存在、使用`AssetManager`读取数据库文件并写入App私有目录,实现外部数据库的顺利集成与使用。
|
4月前
|
数据库 Android开发 开发者
Android常用的room增删改查语句(外部数据库)
本文分享了将一个原生数据库驱动的单词APP重构为使用Room库的过程及遇到的问题,重点解决了Room中增删改查的常用语句实现。文章通过具体示例(以“forget”表为例),详细展示了如何定义实体类、Dao接口、Database类以及Repository和ViewModel的设计与实现。同时,提供了插入、删除、更新和查询数据的代码示例,包括模糊查询、分页加载等功能。此外,针对外部数据库导入问题,作者建议可通过公众号“计蒙不吃鱼”获取更多支持。此内容适合有一定Room基础的开发者深入学习。
146 0
Android常用的room增删改查语句(外部数据库)
|
5月前
|
SQL 数据库连接 数据库
在C++的QT框架中实现SQLite数据库的连接与操作
以上就是在C++的QT框架中实现SQLite数据库的连接与操作的基本步骤。这些步骤包括创建数据库连接、执行SQL命令、处理查询结果和关闭数据库连接。在实际使用中,你可能需要根据具体的需求来修改这些代码。
256 14
|
7月前
|
前端开发 Java 编译器
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
193 36
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
|
7月前
|
存储 缓存 Java
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
643 3
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡