Room是Google推出的数据库框架,是一个 ORM (Object Relational Mapping)对象关系映射数据库、其底层还是对SQLite的封装。 使用ORM可以让开发者更加关注业务逻辑,而不是SQL 语句。
国内Android开发常用的数据库有GreenDao,Realm,DBFlow,LitePal.
Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。具体来说,Room 具有以下优势:
所以 ORM ROOM 是当前最值得使用的Android数据库框架。
Room 包含三个主要组件:
将一晚睡眠定义为一个带注解的表示数据库实体的数据类
我们需要记录
@Entity(tableName = "sleep_night")
data class SleepNight(
@PrimaryKey(autoGenerate = true)
var nightId: Long = 0L,
@ColumnInfo(name = "start_time_milli")
val startTimeMilli: Long = System.currentTimeMillis(), // 以毫秒表示的当前时间
@ColumnInfo(name = "end_time_milli")
var endTimeMilli: Long = startTimeMilli, // 初始为开始时间,表示尚未记录任何结束时间。
@ColumnInfo(name = "quality_rating")
var sleepQuality: Int = -1 // 初始化 -1,以表明未收集到质量数据。
)
创建 SleepNight 数据类,参数包括 ID、开始时间、结束时间,以及数字形式的睡眠质量评分。
其它需要注意的地方:
如果主键比较复杂可以加在@Entity注解的后面。
注意:一个Entity只有一个主键,但是主键可以由多个字段来组成联合主键,比如 id 和 deadLine可以组成一个主键,共同作为唯一标识。避免某些场景,仅仅id相同数据就被覆盖的问题。
@Entity(primaryKeys = {"id", "dedaLine"})
public class User {
private String id;
private String deadLine;
public String firstName;
public String lastName;
}
另外:@Ignore: 忽略该字段,加上该注解不会将该字段映射到数据库中去。
定义一个数据访问对象 (DAO),可以将 DAO 视为定义用于访问数据库的自定义接口,DAO 提供了插入、删除和更新数据库的便捷方法。
对于常见的数据库操作,Room 库会提供方便的注解,例如 @Insert、@Delete 和 @Update。
对于所有其他操作,都使用 @Query 注解。您可以编写 SQLite 支持的任何查询。
使用 Room 数据库时,操作数据库的方法会映射到 SQL 查询。可以使用注解在 DAO 中定义这些映射,而 Room 会创建必要的代码。
另一个好处是,当您在 Android Studio 中创建查询时,编译器会检查您的 SQL 查询是否存在语法错误。
对于睡眠之夜的睡眠跟踪器数据库,必须能够执行以下操作:
@Dao // 所有 DAO 都需要使用 @Dao 关键字进行注解。
interface SleepDatabaseDao {
@Insert //调用 insert注解的方法时,Room 将执行 SQL 查询以将该实体插入到数据库中
fun insert(night: SleepNight)
@Update // update的实体是与所传入实体具有相同键的实体。您可以更新该实体的部分或全部属性。
fun update(night: SleepNight)
// 其余功能没有方便使用的注解,因此您必须使用 @Query 注解并提供 SQLite 查询。
// key在查询中使用英文冒号是为了引用该函数中的参数
@Query("SELECT * from sleep_night WHERE nightId = :key")
fun get(key: Long): SleepNight?
// 使用@Query结合sq语句删除数据库中的所有条目,此查询不会删除表本身。
// 注意:
// @Delete 注解会删除一项内容,您可以使用 @Delete 并提供要删除的夜晚列表。这种方法的缺点是,
// 您需要提取或了解表中的内容。
// @Delete 注解非常适合用于删除特定条目,但在清除表中的所有条目方面效率较低。
@Query("DELETE FROM sleep_night")
fun clear()
// 返回的SleepNight可为null,以便函数能够处理表为空的情况。
//(该表在开始时是空的,在数据被清除后也是空的。)
// 用于返回按 nightId 降序排列的结果列表中的第一个元素。使用 LIMIT 1 可仅返回一个元素。
// desc是降序; asc是升序 。语法: order by [列名] [方法]
@Query("SELECT * FROM sleep_night ORDER BY nightId DESC LIMIT 1")
fun getTonight(): SleepNight?
// 查询所有数据,并依降序排序。
// 返回liveDatad对象的可观察查询。当数据库更新时,Room会生成更新LiveData对象所需的所有代码。
// observe此liveData可以获取到最新数据
@Query("SELECT * FROM sleep_night ORDER BY nightId DESC")
fun getAllNights(): LiveData<List<SleepNight>>
}
@Database(entities = [SleepNight::class], version = 1, exportSchema = false)
abstract class SleepDatabase : RoomDatabase() {
// 对于与数据库关联的每个 DAO 类,数据库类必须定义一个具有零参数的抽象方法,并返回 DAO 类的实例。
abstact fun getDao():SleepDatabaseDao
companion object {
@Volatile
private var INSTANCE: SleepDatabase? = null
fun getInstance(context: Context): SleepDatabase {
if (INSTANCE== null) {
synchronized(this) {
if (INSTANCE== null) {
INSTANCE= Room.databaseBuilder(
context.applicationContext,
SleepDatabase::class.java,
"sleep_night_database" // 数据库名
)
// 允许在主线程操作数据库,一般不推荐;设置这个后主线程调用增删改查不会报错,否则会报错
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build()
}
return INSTANCE
}
}
}
}
}
@Insert支持设置冲突策略,**默认为OnConflictStrategy.ABORT即插入相同数据会中止并回滚 **。还可以指定为其他策略:
注意:@Insert修饰的方法的返回值可为空,也可为插入行的ID或ID列表
如:
fun insert(book: Book?)
fun insert(book: Book?): Long?
fun insert(vararg books: Book?): LongArray?
和@Insert一样,支持不返回删除结果。
和@Insert一样支持设置冲突策略和定制返回更新结果。此外需要注意的是@Update操作将匹配参数的主键id去更新字段。
@Query指定不同的SQL语句即可获得相应的查询结果。在编译阶段就将验证语句是否正确,避免错误的查询语句影响到运行阶段。
**常见的Query语句: **
假设定义了一个书本的实体类,如下:
@Entity(tableName = "Book")
class Book{
@PrimaryKey(autoGenerate=true)
var id :Int = 0
@ColumnInfo(name = "book_name")
var bookName:String? = null
@ColumnInfo(name = "author_name")
var authorName :String? = null
@ColumnInfo(name = "post_year")
var postYear:String? = null
@Ignore
var price :Int = 0
}
@Query("SELECT * FROM book")
@Query("SELECT id, book_name FROM book")
// asc(ascend )是升序,这个值按照从小到大进行排序;desc(descend)是降序,这个值按照从大到小进行排序。
@Query("SELECT * FROM book ORDER BY post_year DESC") 比如查询最近发行的书籍列表
@Query("SELECT * FROM book WHERE id = :id")
// 比如查询名称和作者中**匹配**关键字的书籍
@Query("SELECT * FROM book WHERE book_name LIKE :keyWord " + " OR author_name LIKE :keyWord")
// 比如查询名称和作者中**包含**关键字的书籍
@Query("SELECT * FROM book WHERE book_name LIKE '%' || :keyWord || '%' " + " OR author_name LIKE '%' || :keyWord || '%'")
// 比如查询名称匹配关键字的前三部书籍
@Query("SELECT * FROM book WHERE book_name LIKE :keyWord LIMIT 3")
// 比如查询评分大于指定分数的书籍
@Query("SELECT * FROM book WHERE review_score >= :minScore")
// 比如查询介于发行年份区间的书籍
@Query("SELECT * FROM book WHERE post_year BETWEEN :minYear AND :maxYear")
@Query("SELECT * FROM book WHERE book_name IN (:keyWords)")
大多数情况下,在DAO中定义的方法需要接受参数,根据这些参数执行过滤操作。Room 支持在查询中将方法参数用作绑定参数。
例如,以下代码定义了一个返回特定年龄以上的所有用户的方法:
@Dao
interface UserDao {
// 将minAge作为查询参数传递给Sql语句
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
}
某些 DAO 方法可能要求您传入数量不定的参数,参数的数量要到运行时才知道。Room 知道参数何时表示集合,并根据提供的参数数量在运行时自动将其展开。
例如,以下代码定义了一个方法,该方法返回了部分地区的所有用户的相关信息:
@Dao
interface UserDao {
// 将regions集合作为查询参数传递给Sql语句
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
}
有时候部分查询可能需要访问多个数据表才能计算出结果。可以在 SQL 查询中使用 JOIN 子句来引用多个表。
以下代码定义了一种方法将三个表联接在一起,以便将当前已出借的图书返回给特定用户:
@Dao
interface UserBookDao {
@Query(
"SELECT * FROM book " +
"INNER JOIN loan ON loan.book_id = book.id " +
"INNER JOIN user ON user.id = loan.user_id " +
"WHERE user.name LIKE :userName"
)
fun findBooksBorrowedByNameSync(userName: String): List<Book>
}
由于 SQLite 是关系型数据库,因此您可以定义各个实体之间的关系。尽管大多数对象关系映射库都允许实体对象互相引用,但 Room 明确禁止这样做(请自行了解 Room 为何不允许对象引用)。Room 的数据实体类跟数据库表是对应的,所以,有些表关系可以直接通过定义数据实体类之间的关系来实现,这样就可以无需编写 SQL 语句实现插入和查询过操作。 在 Room 中,您可以通过两种方式定义和查询实体之间的关系:
主表和从表的概念:
主表(父表)
在数据库中建立的表格即Table,其中存在主键(primary key)用于与其它表相关联,并且作为在主表中的唯一性标识。
从表(子表)
以主表的主键(primary key)值为外键 (Foreign Key)的表,可以通过外键与主表进行关联查询。从表与主表通过外键进行关联查询。
从表数据依赖于主表,一般最后查询数据时把主表与从表进行关联查询。
@Embedded注解可以将一个Entity作为属性内嵌到另一Entity,我们可以像访问Column一样访问内嵌Entity
@Embedded通过把内嵌对象的属性解包到被宿主中,建立了实体的连接。此外还可以通过@Relation 和 foreignkeys来描述实体之间更加复杂的关系。
从 Room 2.2 的稳定版开始,我们可利用一个 @Relation 注解来支持表之间所有可能出现的关系: 一对一、一对多和多对多。
参考Room中数据库关系
虽然现在大多数人都还是在使用中间数据类实现查询,但还是建议抓紧学习使用全新的关系查询功能。
假设人和狗之间存在一对多的关系
@Entity(tableName = "owner")
data class Owner(
@PrimaryKey(autoGenerate = true)
var ownerId: Long = 0,
var ownerName: String,
)
@Entity(tableName = "dog")
data class Dog(
@PrimaryKey(autoGenerate = true)
var dogId: Long = 0,
var dogOwnerId:Long = 0,
var dogName: String,
)
// DAO查询方法:
// 使用全新关系查询功能
@Query("select * from owner Join dog on owner.ownerId = dog.dogOwnerId")
fun getAllOwnerAndTheirDogs():Map<Owner,List<Dog>>
测试查询:
private fun testMore2More() {
dao = DogAndOwnerDataBase.getInstance(this@MainActivity).getDao()
val owner1 = Owner(1L, "owner1")
val owner2 = Owner(2L, "owner2")
val dog1 = Dog(1L, 1L, "dog1")
val dog2 = Dog(2L, 1L, "dog2")
val dog3 = Dog(3L, 2L, "dog3")
val dog4 = Dog(4L, 2L, "dog4")
GlobalScope.launch(Dispatchers.IO) {
dao.insertOwner(owner1)
dao.insertOwner(owner2)
dao.insertDog(dog1)
dao.insertDog(dog2)
dao.insertDog(dog3)
dao.insertDog(dog4)
Thread.sleep(1000)
val ownerAndTheirDogs = dao.getAllOwnerAndTheirDogs()
ownerAndTheirDogs.forEach { (owner, list) ->
Log.e(TAG, "owner:$owner")
Log.e(TAG, "list:$list")
Log.e(TAG, "------------------")
}
}
}
结果:
我项目暂时也没用过,都是用的中间数据类,所以…至于更复杂得使用,自己百度搜吧
导出db文件后,用DB Browser发现无法打开db文件,读取不到数据库数据。
原因:room的数据生成文件是三个,要三个一起才可以打开db文件 预览数据库。
工具:sqlitestudio-3.3.3
1.Rxjava和Room配合使用
//Rxjava2和Rxjava3 可以选择使用
def room_version = "2.4.0"
// optional - RxJava2 support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - RxJava3 support for Room
implementation "androidx.room:room-rxjava3:$room_version"
// 定义使用Rxjava的方法
@Query("SELECT * FROM book where id = :id")
fun queryFlowable(id : Int): Flowable<Book?>?
2.LiveData和Room配合使用
// Livedata依赖
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// 返回值使用Livedata
@Query("SELECT * FROM book where id = :id")
fun queryLiveData(id : Int): LiveData<Book?>?
// 通dao对象调用查询的方法
bookDao?.queryLiveData(1)?.observe(this, Observer {
Log.i("book-query","${it?.name}")
})
第一次添加观察者的时候会收到一次数据 onChanged 的回调,其次数据库中的 book 表的数据发生变化都会收到 onChanged 的观察回调,但是这里我们需要注意一个细节,如果你的 @insert 注解是使用的 replace 策略(OnConflictStrategy.REPLACE),这样插入重复的数据也会导致事务的产生旧数据被替换插入 onChanged 也因为重复数据的插入频繁回调!
3.Flow和Room配合使用
可观察查询是指在查询引用的任何表发生更改时发出新值的读取操作。您可能需要用到可观察查询的一种情形是,帮助您在向底层数据库中插入项或者更新或移除其中的项时及时更新显示的列表项。下面是可观察查询的一些示例:
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
@Query("SELECT * from user WHERE region IN (:regions)")
fun loadUsersByRegion(regions: List<String>): Flow<List<User>>
}
注意:Room 中的可观察查询有一项重要限制 - 只要对表中的任何行进行更新(无论该行是否在结果集中),查询就会重新运行。通过应用相应库中的 distinctUntilChanged() 运算符 ,可以确保仅在实际查询结果发生更改时通知界面。
当数据库表的版本进行升级时,需要提供自定义的Migration进行处理,如果不提供自定义的Migration,可以调用**fallbackToDestructiveMigration()**允许升级失败破坏性地删除DB。
database = Room.databaseBuilder(MyApplication.instance(),AppRoomDataBase::class.java,DB_NAME)
.fallbackToDestructiveMigration()
.build();
但是并不推荐这种方式,fallbackToDestructiveMigration方法表示Room启动时将检测version是否发生增加,如果有,那么将找到自定义Migration去执行特定的操作。如果没有自定义Migration, 因为设置了fallbackToDestructiveMigration(),将会删除数据库并重建,所有数据h会丢失。
但是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;
}
// 数据库版本发生变更(如升级)会回调migrate函数,我们需要在此回调中编写版本变更的相关代码,例如创建表、添加列等等。
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
}
例如:需要给Book表添加一个评分的字段score,并且将数据库版本从版本2升级到版本3:
在对应的Entity类中添加相同的字段:
@Entity(tableName = "Book")
class Book{
...
var score:String? = null
}
增加数据库version,例如上一个版本是2,增加了字段现在版本变成3:
@Database(entities = [Book::class], version = 3)
abstract class AppRoomDataBase : RoomDatabase() {
...
}
自定义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 ''")
}
}
提供自定义的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(MyApplication.instance(),RoomDaoManager::class.java,DATABASE_NAME)
.allowMainThreadQueries()//允许在主线程操作数据库,一般不推荐;
.addMigrations(MIGRATION_2_3)
.build()
}
return databaseInstance!!
}
}
}
注意:
在用户使用App的过程中数据库的升级并不总是从 version: 1->2,2->3,3->4。例如用户目前App的数据库版本号是1,并且2,3两个版本的app用户并没有更新下载,因此数据库并没有升级,目前最新的App的数据库版本是4,
用户直接下载最新的App进行升级,如果我们定义了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();
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(MyApplication.instance(),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(MyApplication.instance(),RoomDaoManager::class.java,DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_1_4)
.fallbackToDestructiveMigration()// 如果没有匹配到Migration,则直接删除所有的表,重新创建表
.build();
对于 Room 2.4.0 版本之前的数据库迁移,需要实现 Migration 类,并在其中编写大量复杂冗长的 SQL 语句,来处理不同版本之间的迁移。这种手动迁移的形式,非常容易引发各种错误。
Room 2.4.0后 支持了自动迁移,让我们通过两个示例来对比手动迁移和自动迁移
假设有一个包含两个表的数据库,表名分别是 Artist 和 Track,现在想要将表名 Track 改为 Song。
如果使用手动迁移,必须编写和执行 SQL 语句才能更改,需要如下操作:
val MIGRATION_1_2: Migration = Migration(1, 2) {
fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `Track` RENAME TO `Song`")
}
}
如果使用自动迁移,您只需要在定义数据库时添加 @AutoMigration 配置,同时提供两个版本数据库导出的 schema。Auto Migration API 将为您生成并实现 migrate 函数,编写并执行迁移所需的 SQL 语句。代码如下:
@Database(
version = MusicDatabase.LATEST_VERSION
entities = {Song.class, Artist.class}
// 自动迁移
autoMigrations = {
@AutoMigration (from = 1,to = 2)
}
exprotSchema = true
)
现在,演示一个更复杂的场景,假设我们要将 Artist 表中的 singerName 字段修改为 artistName。
虽然这看起来很简单,但是由于 SQLite 并没有提供用于此操作的 API,因此我们需要根据 ALERT TABLE 实现,有如下几步操作:
迁移代码如下:
val MIGRATION_1_2: Migration = Mirgation(1, 2) {
fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `_new_Artist`(`id` INTEGER NOT
NULL, artistName` TEXT, PRIMARY KEY(`id`)"
)
db.execSQL("INSERT INTO `_new_Artist` (id,artistName)
SELECT id, singerName FROM `Artist`"
)
db.execSQL("DROP TABLE `Artist`")
db.execSQL("ALTER TABLE `_new_Artist` RENAME TO `Artist`")
db.execSQL("PRAGMA foreign_key_check(`Artist`)")
}
}
从上面的代码就可以看出,如果使用手动迁移,即使两个版本之间仅有一处更改,也可能需要繁琐的操作,并且这些操作极易出错。
那我们来看看自动迁移该如何使用。在上面的示例中,自动迁移无法直接处理重命名表中的某一列,因为 Room 在进行自动迁移时,会遍历两个版本的数据库 schema,通过比较来检测两者之间的更改。在处理列或者表的重命名时,Room 无法明确发生了什么更改,此时可能有两种情况,是删除后新添加的?还是进行了重命名?处理列或者表的删除操作时也会有同样问题。
所以我们需要给 Room 添加一些配置来说明这些不确定的场景——定义 AutoMigrationSpec。AutoMigrationSpec 是定义自动迁移规范的接口,我们需要实现该类,并在实现类上添加和修改相对应的注解。本例中,我们使用 @RenameColumn 注解,并在注解参数中,提供表名、列的原始名称以及更新后的名称。如果在迁移完成之后,还需要执行其他任务,可以在 AutoMigrationSpec 的 onPostMigrate 函数中进行处理,相关代码如下:
@RenameColumn(
tableName = "Artist",
fromColumnName = "singerName",
toColumnName = "artistName"
)
static class MySpec : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// 迁移工作完成后处理任务的回调
}
}
完成 AutoMigrationSpec 的实现后,还需要将其添加到数据库定义时配置的 @AutoMigation 中,同时提供两个版本的数据库 schema,Auto Migration API 将生成和实现 migrate 函数,配置代码如下:
@Database(
version = MusicDatabase.LATEST_VERSION
entities = {Song.class, Artist.class}
autoMigrations = {
@AutoMigration (from = 1,to = 2,spec = MySpec.class)
}
exprotSchema = true
)
上面的案例提到了 @RenameColumn,相关的变更处理注解有如下几种:
假设在同一迁移中有多个更改需要配置,我们还可以通过这些可复用的注解简化处理。
Room2.4.0后其它一些新功能: