
Android 开发者使用数据库的时候,最先想到的是 SQLite。如果有对外公开的需求,则需再包装一层 ContentProvider。除此之外,也可以选择开源的数据库框架,比如 GreenDao,DBFlow等。
本文将讲述 Google 推出的数据库框架 Room,和您一起探讨: 如何使用 Room、其实现的大致原理以及它的优势。
Room 是房间的意思。房间除了能存放物品,还能带给人温暖和安心的感觉。用 Room 给这个抽象的软件架构命名,增加了人文色彩,很有温度。
先来看一下 Room 框架的基本组件。

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

使用前需要构筑如下依赖。
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 将结合 ViewModel 和 Room 进行数据交互,依赖 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()
}
}
}

通过 Database Inspector 工具可以看到 DB 数据创建成功了。Database Inspector 支持实时刷新,查询和修改等 DB 操作,是 DB 开发的利器。
如果不知道如何使用Database Inspector,可参考官方文档使用 Database Inspector 调试数据库

@Insert 支持设置冲突策略,默认为 OnConflictStrategy.ABORT 即中止并回滚。还可以指定为其他策略。
其声明的方法返回值可为空,也可为插入行的 ID 或列表。
和 @Insert 一样支持不返回删除结果或返回删除的函数,不再赘述。
和 @Insert 一样支持设置冲突策略和定制返回更新结果。此外需要注意的是 @Update 操作将匹配参数的主键 id 去更新字段。
查询操作主要依赖 @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()
}
注意:
当我们的 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$Builder 的 build() 调用后便通过反射创建了 @Databse 注解声明的 RoomDatabase 实例 XXX_Impl。

SupportSQLiteDatabase 是模仿 SQLiteDatabase 作成的接口,供 Room 框架内部对 DB 进行操作。由 FrameworkSQLiteDatabase 实现,其将通过内部持有的 SQLiteDatabase 实例,代理 DB 操作。
SupportSQLiteDatabase 的创建由增删改查等 DB 操作触发,需要经历 DB 的创建,表的创建,表的初始化,升降级以及打开等过程。



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

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

Room 框架的使用过程中遇到了些容易出错的地方,需要格外留意。
通过上述的实战和原理介绍可以看出,Room 的本质是在 SQLite 的基础上进行封装的抽象层,通过一系列注解让用户能够更简便的使用 SQLite。正因为此,它具备了一些优势,值得开发者大胆使用。
声明注解便能完成接口的定义,易上手
编译阶段将验证注解里声明的 SQL 语句,提高了开发效率
支持使用 RxJava2,LiveData 以及 Flow 进行异步查询
相较其他数据库框架 SQL 执行效率更高
Demo参见Github-JetpackDemo
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co
我正在尝试在Rails上安装ruby,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手
前言一般来说,前端根据后台返回code码展示对应内容只需要在前台判断code值展示对应的内容即可,但要是匹配的code码比较多或者多个页面用到时,为了便于后期维护,后台就会使用字典表让前端匹配,下面我将在微信小程序中通过wxs的方法实现这个操作。为什么要使用wxs?{{method(a,b)}}可以看到,上述代码是一个调用方法传值的操作,在vue中很常见,多用于数据之间的转换,但由于微信小程序诸多限制的原因,你并不能优雅的这样操作,可能有人会说,为什么不用if判断实现呢?但是if判断的局限性在于如果存在数据量过大时,大量重复性操作和if判断会让你的代码显得异常冗余。wxswxs相当于是一个独立