除了SQLite你一定要试试Room
不舍的春节已过,收起心来,2021咱们一起加油。
Android开发者使用数据库的时候,最先想到的是SQLite。如果有对外公开的需求,则需再包装一层ContentProvider。除此之外,也可以选择开源的数据库框架,比如GreenDao,DBFlow等。
本文将讲述Google推出的数据库框架Room,和你一起探讨:如何使用Room、其实现的大致原理以及它的优势。
1 简介
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"}2 实战
2.1 组件构建
@Entityclass 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接口。
@Daointerface 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文件
2.2 组件调用
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() } }}Database Inspector工具可以看到DB数据创建成功了。※Database Inspector支持实时刷新,查询和修改等DB操作,是DB开发的利器。2.3 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?
@Insert一样支持不返回删除结果或返回删除的函数,不再赘述。@Update
@Insert一样支持设置冲突策略和定制返回更新结果。此外需要注意的是@Update操作将匹配参数的主键id去更新字段。fun update(vararg movies: Movie?): Int
@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进行响应式查询。
数据库升级降级
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.
@Database的version升级为2之后再次运行仍然发生崩溃。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()}addMigrations()指定升级之后的迁移处理来达到保留旧数据和增加新字段的双赢。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()都必不可少
@Transaction注解帮助我们快速实现这个需求,它将确保注解内的方法运行在同一个事务模式。@Daopublic 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)})3 原理浅谈
3.1 RoomDatabase的创建
@Databse注解声明的RoomDatabase实例XXX_Impl。3.2 SupportSQLiteDatabase的创建
SupportSQLiteDatabase是模仿SQLiteDatabase作成的接口,供Room框架内部对DB进行操作。由FrameworkSQLiteDatabase实现,其将通过内部持有的SQLiteDatabase实例,代理DB操作。创建DB文件
创建表
初始化表
升级表
打开表
onOpen()。4 注意
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
https://github.com/ellisonchan/JetpackDemo
参考资料
Room版本
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 访问数据
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