Room是Google推出的数据库框架,是一个 ORM (Object Relational Mapping)对象关系映射数据库、其底层还是对SQLite的封装。 使用ORM可以让开发者更加关注业务逻辑,而不是SQL 语句。在JavaWeb领域也有类似的ORM 数据库框架Hibernate、MyBatis等等。
Android平台数据库框架
在 Android 中常见的数据库框架:
Greendao
Realm
DBFlow
LitePal
Jetpack-Room
Greendao: 是 Room 之前用得最广泛的 ORM 数据库框架,不过官方目前已经不再积极维护(官方在推新品 ObjectBox )在 Room,出来以后据非官方数据统计多种场景下(插入、更新、删除),Room 性能上也只是和 Greendao 不相上下,强得有限,毕竟底层都还是 Android 的 SQLite 只能通过包装层和生成语句去优化。Greendao的缺点是配置复杂,不支持监听数据表/Kotlin/协程等特性。
Realm: 不是基于 SQLite ,它是底层用C语言写的数据库引擎,号称速度比 SQLite 快 10 倍。跨平台,支持多种平台设备(Android/iOS/Mac/Windows),支持RxJava/Kotlin, 但不支持嵌套类而且要求字段指定默认值, 自定义数据库引擎,因此会要求导入JNI库, 会导致apk体积增加, 函数设计比较复杂, 官方图形工具相对简陋但实时更新。
DBFlow: 主要使用函数操作数据库, 学习成本高,原生支持数据库加密,支持监听数据库,支持协程/Kotlin/RxJava, 在国内比较冷门。
LitePal: 是国内Android开发者郭霖开源并维护的,也是对SQLite数据库的再次封装,方便调用。
Room: 是当前的主流数据库框架, 支持SQL语句, 操作数据库需要编写抽象函数, 由Google官方维护,它是JetPack组件中的数据库框架,支持嵌套对象,支持Kotlin 协程/RxJava,具备SQL语句高亮和编译期检查(具备AndroidStudio的支持)。
综合所有的 Android 平台 ORM 数据库来看,Room 有优秀的效率、支持内存映射、支持与 LiveData 绑定 、编译期检查(Room 会在编译的时候验证每个@Query和@Entity等,它不仅检查语法问题,还会检查是否有该表,这就意味着几乎没有任何运行时错误的风险) 并且最重要的是有着Google Android 官方团队去维护,所以 ORM Room 是当前最值得使用的Android数据库框架。
Room主要由3个部分组成:
Entity:
表示数据库中的表,数据实体,对应数据库中的表。
DAO:
包含用于访问数据库的方法,数据访问对象,包含访问数据库的方法,数据访问对象是Room的主要组件,负责定义访问数据库的方法。
Database:
包含数据库持有者,并作为应用程序持久关系数据的基础连接的主要访问点:数据库扩展了RoomDatabase的抽象类。可以通过databaseBuilder或Room.inMemoryDatabaseBuilder获得它的一个实例。
(1)引入Room依赖,在app的build.gradle中添加依赖:
apply plugin: 'kotlin-kapt'//kotlin开启katp
dependencies {
implementation "androidx.room:room-runtime:2.2.5"
//kotlin 使用kapt 替代annotationProcessor
kapt "androidx.room:room-compiler:2.2.5"
//Java 用annotationProcessor
kapt "androidx.room:room-compiler:2.2.5"
// 可选 - Kotlin扩展和协程支持
implementation "androidx.room:room-ktx:2.2.5"
}
(2)配置编译器选项(可选),在app的build.gradle中的defaultConfig标签下添加依赖:
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true"]
}
}
配置编译器解释:
1) "room.schemaLocation":"$projectDir/schemas".toString(): 的作用是将配置并启把据库架构导出json文件到指定目录。
2) "room.incremental":"true":启用 Gradle 增量注解处理器。
3) "room.expandProjection":"true": 配置 Room 以重写查询,使其顶部星形投影在展开后仅包含 DAO 方法返回类型中定义的列。
如果配置了schemaLocation,编译后,会在对应路径生成schemas文件夹,json包含了各个版本的概要,表结构等信息,如下图一:
如果在创建数据库的时候未指定具体位置生成的位置则是在 data/data/包名/database 下,如果需要指定额外的位置则在上文数据库构建的时候传数据库名前面带上你需要指定的路径。
package com.phone.library_common.room
import androidx.room.*
//@Entity(indices = [Index(value = ["bookName"], unique = true)])
@Entity(tableName = "Book")
class Book {
@PrimaryKey(autoGenerate = true)
var id: Int = 0
@ColumnInfo(name = "book_name")
var bookName: String? = null
var anchor: String? = null
@Ignore
var price: Int = 0
}
创建表一般会用到下面几个注解:
@Entity(tableName=“表名称”): @Entity表示定义数据库中的一个表,通常Room会使用类名作为数据库的表名,如果你希望自定义表名可以配置@Entity(tableName = “my_book”),注意:SQLite中,表名是不区分大小写的。
@PrimaryKey(autoGenerate=true): 定义主键 autoGenerate 用于设置主键自增,默认为false。
@ColumnInfo(name = “别名”): Room默认用变量名称作为数据库表的字段名称,如果你希望字段名称和变量名称不一样,则需要给变量添加@ColumnInfo注解。
@Ignore: 忽略该字段,加上该注解不会将该字段映射到数据库中去。
索引和唯一性:根据操作数据的方式可能需要通过索引来提高查询数据库的速度,通过@Entity添加indices属性,有些字段设置唯一性,可以通过@Index注解下设置unique为true。
此外,Room 要求每个数据库序列化字段为public访问权限。
package com.phone.library_common.room
import androidx.room.*
@Dao
abstract class BookDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(book: Book)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(books: List)
@Delete
abstract fun delete(book: Book)
@Update
abstract fun update(book: Book)
@Query("select * from Book where id =:id")
abstract fun queryById(id: Int): Book
@Query("select * from Book")
abstract fun queryAll(): List
@Query("select count(*) from Book")
abstract fun bookCount(): Int
@Query("delete from Book")
abstract fun deleteAll();
}
Room 使用注解处理器映射了数据库的增删改查,即 @Insert、@Delete、@Update、@Query 代表我们常用的插入、删除、更新、查询数据库操作。
@Query非常的强大,你可以编写任意sql语句并得到你想要的结果。
如果在query时返回值类型和查询的表名和返回值类型或查询的表名不相同时,在程序编译会编译失败,这也降低了程序在运行时出现的风险。
使用 @Database 注解的类应满足以下条件:
是一个继承于RoomDatabase 的抽象类。
在注解中包括与数据库相关联的实体列表。
包含一个没有参数的抽象方法并且返回一个带有注解的@Dao。
在运行时,我们可以通过调用 Room.databaseBuilder()或 Room.inMemoryDatabaseBuilder()
来获取 Database 的实例。
RoomDatabase实例的内存开销较大,建议使用单例模式管理:
package com.phone.library_common.room
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.phone.library_common.BaseApplication
@Database(entities = [Book::class], version = 1)
abstract class AppRoomDataBase : RoomDatabase() {
//创建DAO的抽象类
abstract fun bookDao(): BookDao
companion object {
private const val TAG = "AppRoomDataBase"
//DATABASE_NAME名称可以叫simple_app或simple_app.db,正常来说应该叫 //simple_app.db,但是名称叫simple_app也没问题
const val DATABASE_NAME = "simple_app.db"
@Volatile
private var databaseInstance: AppRoomDataBase? = null
@Synchronized
@JvmStatic
fun get(): AppRoomDataBase {
if (databaseInstance == null) {
databaseInstance = Room.databaseBuilder(
BaseApplication.get(),
AppRoomDataBase::class.java,
DATABASE_NAME
)
.allowMainThreadQueries()//允许在主线程操作数据库,一般不推荐;设置这个后主线程调用增删改查不会报错,否则会报错
// .openHelperFactory(factory)
.build()
}
return databaseInstance!!
}
}
}
@Database 表示继承自RoomDatabase的抽象类,entities指定Entities表的实现类,version指定了DB版本。
必须提供获取DAO接口的抽象方法,比如上面定义的bookDao(),RoomDatabase将通过这个方法实例化DAO接口。
RoomDatabase实例的内存开销较大,建议使用单例模式管理。
点击Build编译项目,Room会自动生成对应的_Impl实现类,此处将生成AppRoomDataBase_Impl.java文件。
注意:数据库的实例化是很昂贵的,所以建议我们使用单例模式来初始化,而且也很少情况下需要访问多个实例。
val appRoomDataBase = AppRoomDataBase.get()
val book = Book()
book.bookName = "EnglishXC"
book.anchor = "rommelXC"
appRoomDataBase.bookDao().insert(book)
val book2 = Book()
book2.bookName = "EnglishXC2"
book2.anchor = "rommelXC2"
appRoomDataBase.bookDao().insert(book2)
val bookList = appRoomDataBase.bookDao().queryAll()
for (i in 0..bookList.size - 1) {
LogManager.i(TAG, "book*****" + bookList.get(i).bookName)
}
可以通过Android Studio直接导出应用创建的数据库文件(Root过的手机或者Android Studio自带的模拟器能查看和导出,在/data/data/
Android查看数据库文件
构造数据库之前 build 提供了很多功能的 API 给我们调用,其中包含一些相当重要的 API。我们需要对其了解:
(1)@Insert
@Insert支持设置冲突策略,默认为OnConflictStrategy.ABORT即插入相同数据会中止并回滚。还可以指定为其他策略:
OnConflictStrategy.REPLACE 冲突时替换为新记录。
OnConflictStrategy.IGNORE 忽略冲突(不建议使用)。
OnConflictStrategy.ROLLBACK 废弃了,使用ABORT替代。
OnConflictStrategy.FAIL 废弃了,使用ABORT替代。
@Insert修饰的方法的返回值可为空,也可为插入行的ID或ID列表:
fun insert(book: Book?)
fun insert(book: Book?): Long?
fun insert(vararg books: Book?): LongArray?
(2)@Delete
和@Insert一样,支持不返回删除结果。
(3)@Update
和@Insert一样支持设置冲突策略和定制返回更新结果。此外需要注意的是@Update操作将匹配参数的主键id去更新字段。
(4)@Query
@Query指定不同的SQL语句即可获得相应的查询结果。在编译阶段就将验证语句是否正确,避免错误的查询语句影响到运行阶段。
1)查询所有字段
@Query("SELECT * FROM book")
2)查询指定字段
@Query("SELECT id, book_name, actor_name, post_year, review_score FROM book")
3)排序查询
@Query("SELECT * FROM book ORDER BY post_year DESC") 比如查询最近发行的书籍列表
4)匹配查询
@Query("SELECT * FROM book WHERE id = :id")
5)多字段匹配查询
@Query("SELECT * FROM book WHERE book_name LIKE :keyWord " + " OR author_name LIKE :keyWord") 比如查询名称和作者中**匹配**关键字的书籍
6)模糊查询
@Query("SELECT * FROM book WHERE book_name LIKE '%' || :keyWord || '%' " + " OR author_name LIKE '%' || :keyWord || '%'") 比如查询名称和作者中**包含**关键字的书籍
7)限制行数查询
@Query("SELECT * FROM book WHERE book_name LIKE :keyWord LIMIT 3") 比如查询名称匹配关键字的前三部书籍
8)参数引用查询
@Query("SELECT * FROM book WHERE review_score >= :minScore") 比如查询评分大于指定分数的书籍
9)多参数查询
@Query("SELECT * FROM book WHERE post_year BETWEEN :minYear AND :maxYear") 比如查询介于发行年份区间的书籍
10)不定参数查询
@Query("SELECT * FROM book WHERE book_name IN (:keyWords)")
11)Cursor查询
@Query("SELECT * FROM book WHERE book_name LIKE '%' || :keyWord || '%' LIMIT :limit")
fun searchMoveCursorByLimit(keyWord: String?, limit: Int): Cursor?
注意:Cursor需要保证查询到的字段和取值一一对应,所以不推荐使用。
(1)数据库升级和降级
在@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 for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.根据报错的意思是,当数据库表的版本进行升级时,需要提供自定义的Migration进行处理,如果不提供自定义的Migration,可以调用fallbackToDestructiveMigration()允许升级失败破坏性地删除DB。
(2)删除数据库并重建(不推荐)
不提供自定义Migration,又不想引发crash,那么可以试试这个:
database = Room.databaseBuilder(BaseApplication.get(), AppRoomDataBase::class.java, DB_NAME)
.fallbackToDestructiveMigration()
.build();
fallbackToDestructiveMigration方法表示Room启动时将检测version是否发生增加,如果有,那么将找到自定义Migration去执行特定的操作。如果没有自定义Migration, 因为设置了fallbackToDestructiveMigration(),将会删除数据库并重建,所有数据丢失。
(3)自定义Migration升级版本
但是DB升级后,无论原有数据被删除还是重新初始化都是用户难以接受的。我们可以通过addMigrations()
指定升级之后的迁移处理来达到保留旧数据和增加新字段的情况。 在Room中,数据库迁移使用的是Migration对象,定义如下:
public abstract class Migration {
public final int startVersion;
public final int endVersion;
public Migration(int startVersion, int endVersion) {
this.startVersion = startVersion;
this.endVersion = endVersion;
}
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
}
startVersion是旧版本号,endVersion是新版本号。数据库版本发生变更(如升级)会回调migrate函数,我们需要在此回调中编写版本变更的相关代码,例如创建表、添加列等等。
addMigrations(Migration migrations…):该方法接收的是一个数组,因此可以对多个版本进行迁移处理。
Migration(int startVersion, int endVersion):每次迁移都必须定义初始版本和目标版本。
在重写的migrate方法中执行更新的sql,同时需要在对应的Entity类中添加相同的字段,来保证字段相同。
例如需要给Book表添加一个评分的字段score,并且将数据库版本从版本2升级到版本3:
1)在对应的Entity类中添加相同的字段:
@Entity(tableName = "Book")
class Book{
...
var score:String? = null
}
2)增加数据库version,例如上一个版本是2,增加了字段现在版本变成3:
@Database(entities = [Book::class], version = 3)
abstract class AppRoomDataBase : RoomDatabase() {
...
}
3)自定义Migration,实现迁移逻辑:
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
//对Book表增加一个score字段
database.execSQL("ALTER TABLE Book ADD COLUMN score TEXT NOT NULL DEFAULT ''")
}
}
4)提供自定义的Migration:
@Database(entities = [Book::class], version = 1)
abstract class AppRoomDataBase : RoomDatabase() {
//创建DAO的抽象类
abstract fun bookDao(): BookDao
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
//对Book表增加一个score字段
database.execSQL("ALTER TABLE Book ADD COLUMN score TEXT NOT NULL DEFAULT ''")
}
}
companion object {
private val DATABASE_NAME = "simple_app.db"
@Volatile
private var databaseInstance: AppRoomDataBase? = null
@Synchronized
open fun getInstance(): RoomDaoManager {
if (databaseInstance == null) {
databaseInstance = Room.databaseBuilder(BaseApplication.get(), RoomDaoManager::class.java,DATABASE_NAME)
.allowMainThreadQueries()//允许在主线程操作数据库,一般不推荐;设置这个后主线程调用增删改查不会报错,否则会报错
.addMigrations(MIGRATION_2_3)
.build()
}
return databaseInstance!!
}
}
}
注意:
数据库降级的话则是调用fallbackToDestructiveMigrationOnDowngrade()来指定在降级的时候删除原有DB,当然可以像上述那样提供自定义的Migration来进行迁移处理。
如果想要迁移数据,无论是升级还是降级,必须要给@Database的version指定正确的目标版本。
在用户使用App的过程中数据库的升级并不总是按部就班的从 version: 1->2,2->3,3->4。例如用户目前App的数据库版本号是1,并且2,3两个版本的app用户并没有更新下载,因此数据库并没有升级,目前最新的App的数据库版本是4,
用户直接下载最新的App进行升级。
(1)如果我们定义了migrations:version 1 到 2, version 2 到 3, version 3 到 4, Room 会一个接一个的触发所有 migration。
1)version:1->2
:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//do something
}
};
2)version:2->3
:
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//do something
}
};
3)version:3->4
:
static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//do something
}
};
4)把migration 添加到 Room database builder:
database = Room.databaseBuilder(MyApplication.instance(),RoomDaoManager::class.java,DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.build();
(2)Room 还可以处理大于 1 的版本增量:可以一次性定义一个从1 到4 的 migration
,提升迁移的速度。
1)version:1->4
:
static final Migration MIGRATION_1_4 = new Migration(1, 4) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//do something
}
};
2)接着,我们只需把它添加到 migration
列表中:
database = Room.databaseBuilder(BaseApplication.get(), RoomDaoManager::class.java,DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_1_4)
.build();
3)Room会优先执行MIGRATION_1_4里面的逻辑,其他的一步一步升级的逻辑不会执行。
4)如果没有匹配到对应的升级Migration配置,则app 直接 crash为了防止crash,可添加fallbackToDestructiveMigration方法配置 直接删除所有的表,重新创建表:
database = Room.databaseBuilder(BaseApplication.get(), RoomDaoManager::class.java,DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_1_4)
.fallbackToDestructiveMigration()// 如果没有匹配到Migration,则直接删除所有的表,重新创建表
.build();
@Entity
@Dao
@Database
如对此有疑问,请联系qq1164688204。
推荐Android开源项目
项目功能介绍:RxJava2和Retrofit2项目,添加自动管理token功能,添加RxJava2生命周期管理,使用App架构设计是MVP模式和MVVM模式,同时使用组件化,部分代码使用Kotlin,此项目持续维护中。
项目地址:https://gitee.com/urasaki/RxJava2AndRetrofit2