不舍的春节已过,收起心来,2021咱们一起加油。
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
意味着主键自增,默认为false@ColumnInfo
表示字段,name
和defaultValue
,顾名思义代表名称和默认值
然后构建一个访问Movie
表的DAO
接口。
@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
表示访问数据库表的接口,需要声明为接口或抽象类,Room
框架将在编译阶段生成_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
指定Entities
表的实现类列表,version
指定了DB版本- 必须提供获取
DAO
接口的抽象方法,比如上面定义的movieDao()
,RoomDatabase
将通过这个方法实例化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。
class MovieViewModel(application: Application) : AndroidViewModel(application) {
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
。
abstract class MovieDataBase : RoomDatabase() {
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()
}
}
}
运行下demo看下效果。
通过AndroidStudio
的Database Inspector
工具可以看到DB数据创建成功了。
*Database Inspector
支持实时刷新,查询等基本的DB操作,能够满足一般的需求,大家可以试试。
DAO的具体使用
@Insert
@Insert
支持设置冲突策略,默认为OnConflictStrategy.ABORT即中止并回滚。还可以指定为其他策略。
- OnConflictStrategy.REPLACE 冲突时替换为新记录
- OnConflictStrategy.IGNORE 忽略冲突(不建议使用)
- OnConflictStrategy.ROLLBACK 废弃了,使用ABORT替代
- OnConflictStrategy.FAIL 废弃了,使用ABORT替代
@Insert
修饰的方法的返回值可为空,也可为插入行的ID或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
进行响应式查询。
进阶使用
数据库升级降级
在@Entities
类里增加了新字段后,重新运行已创建过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
。
SupportSQLiteDatabase
的创建
SupportSQLiteDatabase
是模仿SQLiteDatabase
作成的接口,供Room
框架内部对DB进行操作。由FrameworkSQLiteDatabase
实现,其将通过持有SQLiteDatabase
实例,代理DB操作。
SupportSQLiteDatabase
的创建由增删改查等DB操作触发,需要经历DB的创建,表的创建,表的初始化,升降级以及打开等过程。
创建DB文件
创建表
初始化表
升级表
如果DB文件已经存在并且版本和目标版本不一致的话,将执行迁移处理。迁移处理Migration
未配置或者执行失败的话将删除DB并执行回调。
打开表
DB的创建或升级都正常完成后将回调onOpen()
。
注意
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实例的初始化发生异常:getDatabase called recursively
。 @Entity
注解类不要提供多个构造函数,使用@Ignore
可以回避Callback#onCreate()
回调即DB数据的初始化并非由RoomDatabase$Builder#build()
触发,而是由具体的增删改查触发,切记
总结
通过上述的实战和原理介绍可以看出,Room
的本质是在SQLite
的基础上进行封装的抽象层,通过一系列注解让用户能够更简便的使用SQLite
。正因为此,它具备了一些优势,值得开发者大胆使用。
- 通过简单的注解完成接口的定义,简单易上手
- 注解在编译时验证SQL语句的合法性,提高开发效率
- 支持使用
RxJava2
,LiveData
以及Kotlin协程
进行异步查询 - 相较其他数据库框架SQL执行效率更高
本文DEMO
https://github.com/ellisonchan/JetpackDemo
参考资料
- 版本历史:https://developer.android.google.cn/jetpack/androidx/releases/room?hl=zh-cn
- Room保存数据:https://developer.android.google.cn/training/data-storage/room?hl=zh-cn
- Room DAO 访问数据:https://developer.android.google.cn/training/data-storage/room/accessing-data?hl=zh-cn#query-rxjava
- 官方示例:https://github.com/android/architecture-components-samples/tree/main/BasicSample