最全面的ROOM数据库框架使用指南

21,692 阅读6分钟

Room属于Google推出的JetPack组件库中的数据库框架

主流数据库框架对比分析

  1. Realm高性能, 比SQLite快十倍
    • 支持RxJava/Kotlin
    • 但不支持嵌套类而且要求字段指定默认值, 嵌套数据类我觉得不可或缺
    • 自定义数据库引擎, 故会要求导入JNI库, 会导致apk体积暴增(所有架构平台整合超过5MB)
    • 函数设计比较复杂
    • 官方图形工具相对简陋但实时更新
    • 跨平台, Android/iOS/Mac/Windows
    • 支持监听数据库
  2. DBFlow
    • 主要使用函数操作数据库, 学习成本高
    • 原生支持数据库加密
    • 支持监听数据库
    • 支持协程/Kotlin/RxJava
    • 冷门, 特别是国内
  3. GreenDao
    • 比较落伍, 配置复杂
    • 不支持监听数据表/Kotlin/协程等特性
    • 已经不再积极维护, 官方目前积极维护旗下另外开源数据库ObjectBox
  4. ObjectBox
    • 号称世界最快嵌入式数据库
    • 不支持嵌套对象
    • 体积小(最小压缩到增加体积1MB)
    • 函数设计简单优雅
    • 支持监听数据库
    • 浏览器查看数据库, 仅查看
    • 根据配置生成的JSON文件自动迁移
    • 跨平台, Android/iOS/Mac/Windows/Go
  5. ROOM
    • 主流
    • 支持SQL语句
    • 操作数据库需要编写抽象函数
    • 官方维护, JetPack组件中的数据库框架
    • 监听数据库
    • 支持嵌套对象
    • 支持Kotlin 协程/RxJava
    • 具备SQL语句高亮和编译期检查(具备AndroidStudio的支持)
    • 使用SQLite便于多个平台的数据库文件传递(例如有些联系人信息就是一个SQLite文件)
    • 由于是SQLite可以通过第三方框架进行数据库加密(ROOM原生不支持)
    • 可以配合AndroidStudio自带的数据库查看工具窗口

总结:

根据体积和易用性我推荐ROOM

我平时项目开发必备框架

  1. Android上最强网络请求 Net
  2. Android上最强列表(包含StateLayout) BRV
  3. Android最强缺省页 StateLayout
  4. JSON和长文本日志打印工具 LogCat
  5. 支持异步和全局自定义的吐司工具 Tooltip
  6. 开发调试窗口工具 DebugKit
  7. 一行代码创建透明状态栏 StatusBar

特性

  1. SQL语句高亮
  2. 简单入门
  3. 功能强大
  4. 数据库监听
  5. 支持Kotlin协程/RxJava/Guava

依赖

dependencies {
  def room_version = "2.2.0-rc01"

  implementation "androidx.room:room-runtime:$room_version"
  annotationProcessor "androidx.room:room-compiler:$room_version" 
  // Kotlin 使用 kapt 替代 annotationProcessor

  // 可选 - Kotlin扩展和协程支持
  implementation "androidx.room:room-ktx:$room_version"

  // 可选 - RxJava 支持
  implementation "androidx.room:room-rxjava2:$room_version"

  // 可选 - Guava 支持, including Optional and ListenableFuture
  implementation "androidx.room:room-guava:$room_version"

  // 测试帮助
  testImplementation "androidx.room:room-testing:$room_version"
}

Gradle配置

android {
  ...
    defaultConfig {
      ...
        javaCompileOptions {
          annotationProcessorOptions {
            arguments = [
              "room.schemaLocation":"$projectDir/schemas".toString(),
              "room.incremental":"true",
              "room.expandProjection":"true"]
          }
        }
    }
}
  • room.expandProjection: 在使用星投影时会根据函数返回类型来重写SQL查询语句
  • room.schemaLocation: 输出数据库概要, 可以查看字段信息, 版本号, 数据库创建语句等
  • room.incremental: 启用 Gradle 增量注释处理器

使用

ROOM会在创建数据库对象时就会创建好所有已注册的数据表结构

  1. 创建数据库
  2. 创建操作接口
  3. 创建数据类: 一般为JSON反序列出的data class
  4. 使用

创建数据库

@Database(entities = [Book::class], version = 1)
abstract class SQLDatabase : RoomDatabase() {
    abstract fun book(): BookDao
}

创建操作接口

@Dao
interface BookDao {

    @Query("select * from Book where")
    fun qeuryAll(): List<Book>

    @Insert
    fun insert(vararg book: Book): List<Long>

    @Delete
    fun delete(book: Book): Int

    @Update
    fun update(book: Book): Int

}

创建数据类

@Entity
data class Book(
    @PrimaryKey(autoGenerate = true)
    var number: Long = 0,
    var title:String
)

使用

val db = Room.databaseBuilder(this, SQLDatabase::class.java, "drake").build() 
// drake为数据库文件名称

val book = Book("活着")
db.book().insert(book)

val books = db.user().qeuryAll()
  • databaseBuilder 创建数据库, 一般情况下应用只应该使用一份实例
  • enableMultiInstanceInvalidation 保证多进程中每个进程存在一个市里

注解

名称描述使用
SkipQueryVerification跳过SQL语句校验Database Dao Dao函数
Ignore忽略某个字段被映射到数据表
Transaction事务, 被注释的函数抛出异常将导致事务失败Dao函数

数据表

@Entity(tableName = "users")
data class User (
  @PrimaryKey val id: Int,
  @ColumnInfo(name = "first_name") val firstName: String?,
  @ColumnInfo(name = "last_name") val lastName: String?
)

Entity

修饰类作为数据表, 数据表名称不区分大小写

public @interface Entity {
    /**
     * 数据表名, 默认以类名为表名
     */
    String tableName() default "";

    /**
     * 索引 示例: @Entity(indices = {@Index("name"), @Index("last_name", "address")})
     */
    Index[] indices() default {};

    /**
     * 是否继承父类索引
     */
    boolean inheritSuperIndices() default false;

    /**
     * 联合主键
     */
    String[] primaryKeys() default {};

    /**
     * 外键数组
     */
    ForeignKey[] foreignKeys() default {};

    /**
     * 忽略字段数组
     */
    String[] ignoredColumns() default {};
}
  • 构建的实体类允许空构造函数或者部分字段构造函数, 但要求每个序列化字段为public访问权限
  • 每个实体类要求至少拥有一个主键
  • 如果主构造函数存在必须参数(但为Ignore)会导致编译失败

ColumnInfo

修饰字段作为数据库中的列(字段)

public @interface ColumnInfo {
    /**
     * 列名, 默认为当前修饰的字段名
     */
    String name() default INHERIT_FIELD_NAME;

    /**
     * 指定当前字段属于Affinity类型, 一般不用
     */
    @SQLiteTypeAffinity int typeAffinity() default UNDEFINED;
  
  	// 以下类型
    int UNDEFINED = 1;
    int TEXT = 2;
    int INTEGER = 3;
    int REAL = 4; //
    int BLOB = 5;

    /**
     * 该字段为索引
     */
    boolean index() default false;

    /**
     * 指定构建数据表时排列 列的顺序
     */
    @Collate int collate() default UNSPECIFIED;
  
    int UNSPECIFIED = 1; // 默认值, 类似于BINARY
    int BINARY = 2; // 区分大小写
    int NOCASE = 3; // 不区分大小写
    int RTRIM = 4; // 区分大小写排列, 忽略尾部空格
		@RequiresApi(21)
    int LOCALIZED = 5; // 按照当前系统默认的顺序
    @RequiresApi(21)
    int UNICODE = 6; // unicode顺序

    /**
     * 当前列的默认值, 这种默认值如果改变要求处理数据库迁移, 该参数支持SQL语句函数
     */
    String defaultValue() default VALUE_UNSPECIFIED;
}
  • 主要使用的参数只有index/name
  • 并不是只能修饰Entity类的字段, 非Entity的类也可以被此注解修饰(例如用于展开投影的POJO类, 后面会提到类型投影).

全文搜索

使用@Fts4修饰实体类可以以创建支持全文搜索的数据表, 要求minSdkVersion高于16

Database

public @interface Database {
    /**
     * 指定数据库初始化时创建数据表
     */
    Class<?>[] entities();

    /**
     * 指定数据库包含哪些视图
     */
    Class<?>[] views() default {};

    /**
     * 数据库当前版本号
     */
    int version();

    /**
     * 是否允许到处数据库概要, 默认为true. 要求配置gradle`room.schemaLocation`才有效
     */
    boolean exportSchema() default true;
}

PrimaryKey

@PrimaryKey

  • 每个数据库要求至少设置一个主键字段, 即使只有一个字段的数据表
  • 子类和父类有一个主键即可, 子类会覆盖父类的主键
  • 假设PrimaryKey和Embedded同时使用则内嵌的类所有字段都成为联合主键
  • 使用Entity.primaryKeys()也可以创建联合主键
boolean autoGenerate() default false; // 主键自动增长

如果主键设置自动生成, 则要求必须为Long或者Int类型

ForeignKey

@ForeignKey

public @interface ForeignKey {
  // 引用外键的表的实体
  Class entity();
  
  // 要引用的外键列
  String[] parentColumns();
  
  // 要关联的列
  String[] childColumns();
  
  // 当父类实体(关联的外键表)从数据库中删除时执行的操作
  @Action int onDelete() default NO_ACTION;
  
  // 当父类实体(关联的外键表)更新时执行的操作
  @Action int onUpdate() default NO_ACTION;
  
  // 在事务完成之前,是否应该推迟外键约束
  boolean deferred() default false;
  
  // 给onDelete,onUpdate定义的操作
  int NO_ACTION = 1; // 无动作
  int RESTRICT = 2; // 存在子健记录时禁止删除父键
  int SET_NULL = 3; // 子表删除会导致父键置为NULL 
  int SET_DEFAULT = 4; // 子表删除会导致父键置为默认值 
  int CASCADE = 5; // 父键删除时子表关键的记录全部删除
  
  @IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
  @interface Action {
    }
}

示例

@Entity
@ForeignKey(entity = Person::class, parentColumns = ["personId"],childColumns = ["bookId"], onDelete = ForeignKey.RESTRICT )
data class Book(
    @PrimaryKey(autoGenerate = true)
    var bookId: Int = 0,
    @ColumnInfo(defaultValue = "12") var title: String = "冰火之歌"
)

Index

@Index

  • 增加查询速度, 但降低插入和更新
  • 无法被继承
  • 仅在Entity中使用
// 需要被添加索引的字段名
String[] value();

// 索引名称
// 默认为: index_${tableName}_${fieldName} 示例: index_Foo_bar_baz
String name() default "";

// 表示字段在表中唯一不可重复
boolean unique() default false;

RawQuery

@RawQuery

该注解用于修饰参数为SupportSQLiteQuery的Dao函数, 用于原始查询(编译器不会校验SQL语句), 一般使用@Query

 interface RawDao {
    @RawQuery
    fun queryBook(query:SupportSQLiteQuery): Book
 }

va; book = rawDao.queryBook(SimpleSQLiteQuery("SELECT * FROM song ORDER BY name DESC"));

如果要返回可观察的对象Flow等, 则需要指定注解参数observedEntities

@RawQuery(observedEntities = [Book::class])
fun query(query:SupportSQLiteQuery): Flow<MutableList<Book>>

Embedded

@Embedded

如果数据表实体存在一个字段属于另外一个对象, 该字段使用此注解修饰即可让外部类包含该类所有的字段(在数据表中).

该外部类不要求同为Entity修饰的表结构

String prefix() default  ""; 
// 前缀, 在数据表中的字段名都会被添加此前缀

Relation

  • 注释字段必须public
  • Entity类内的字段不允许使用Relation
  • 要求Relation类中使用Embedded嵌入主表字段
  • Relation只能修饰Entity数据表类型字段(无法嵌套Relation)

下面演示创建一个一对多, 一个用户对应一个库

@Entity
data class User(
  @PrimaryKey val userId: Long,
  val name: String,
  val age: Int
)

@Entity
data class Library(
  @PrimaryKey val libraryId: Long,
  val userOwnerId: Long
)

创建一个关联后的对象

data class UserAndLibrary(
  @Embedded val user: User, // 此处要求必须使用Embedded
  @Relation(
    parentColumn = "userId",
    entityColumn = "userOwnerId"
  )
  val library: Library // 这里表示一对一查询, 如果是集合List<Book>则表示为一对多
)

查询

@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
  • UserAndLibrary并不会创建数据表(Entity), 只是关联两个表的实体类
  • parentColumn对应User中的字段, entityColumn对应Book中的字段(即一对多中的"多"数据表)
  • 要求使用@Transaction, 因为内部存在两次查询. 要求保持原子方式执行

多对多

@Entity
data class Playlist(
  @PrimaryKey val playlistId: Long,
  val playlistName: String
)

@Entity
data class Song(
  @PrimaryKey val songId: Long,
  val songName: String,
  val artist: String
)

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
  val playlistId: Long,
  val songId: Long
)

直接返回指定列, 默认情况根据类型

data class PlaylistWithSongs(
  @Embedded val playlist: Playlist,
  @Relation(
    parentColumn = "playlistId",
    entityColumn = "songId",
    associateBy = @Junction(PlaylistSongCrossRef::class)
  )
  val songs: List<Song>
)

data class SongWithPlaylists(
  @Embedded val song: Song,
  @Relation(
    parentColumn = "songId",
    entityColumn = "playlistId",
    associateBy = @Junction(PlaylistSongCrossRef::class)
  )
  val playlists: List<Playlist>
)
  • associateBy来指定交叉关系引用的数据表(桥接表)

使用关联表

单独定义一个数据表用来表示两个数据表之间的关系

创建一个桥接表

@Entity(primaryKeys = ["personId", "bookId"])
data class LibraryRelation(
    var personId: Int,
    var bookId: Int
)
  • 桥接用的数据表要求同为主键

通过参数associateBy指定桥接表

data class User(
    @Embedded var person: Person,
    @Relation(
        entity = Book::class,
        parentColumn = "personId",
        entityColumn = "bookId",
        associateBy = Junction(BookCaseRef::class, parentColumn = "pId", entityColumn = "bId")
    )
    var book: List<MyBook>
)
  • Junction中的parentColumn/entityColumn默认值为Relation中的同名参数. 其含义为桥接表中的字段名

  • 使用属性entity指定数据表实体类, 然后会展开投影映射到MyBook中的同名字段(MyBook是个普通的数据类), 如果不使用entity则要求返回类型为数据表实体类.

  • 使用projection属性可以指定类型投影字段

     public class UserAndAllPets {
       @Embedded
       public User user;
       @Relation(parentColumn = "id", entityColumn = "userId", entity = Pet.class,
               projection = {"name"})
       public List<String> petNames;
     }
    

完整的多对多查询

data class User(
    @Embedded var person: Person,
    @Relation(
        entity = Book::class,
        parentColumn = "personId",
        entityColumn = "bookId",
        associateBy = Junction(BookCaseRef::class, parentColumn = "pId", entityColumn = "bId")
    )
    var book: List<Book>
)

data class BookCase(
    @Embedded var book: Book,
    @Relation(
        entity = Person::class,
        parentColumn = "bookId",
        entityColumn = "personId",
        associateBy = Junction(BookCaseRef::class, parentColumn = "bId", entityColumn = "pId")
    )
    var person: List<Person>
)

Transaction

Dao抽象类中可以创建一个带有@Transaction注解的函数, 该函数内的数据库操作在一个事务中 一般情况使用函数runTransaction 设计师

苏打水大

@Dao
abstract class UserDao {
    @Insert
    abstract fun insertPerson(person: Person): Long

    @Query("select * from Person")
    abstract fun findUser(): List<User>

    @Delete
    abstract fun delete(p: Person)

    @Transaction
    open fun multiOperation(deleteId: Int) {
        insertPerson(Person(deleteId, "nathan"))
        insertPerson(Person(deleteId, "nathan")) // 重复插入主键冲突导致事务失败
    }
}
  • Insert/Delete/Update修饰的函数本身就是在事务中
  • ROOM仅允许一个事务运行, 其他事务排队
  • @Tranaction要求修饰的函数不能为final/private/abstract, 但如果该函数同时包含@Query则可以为abstract
  • Query如果查询的包含Relation注解的查询存在多个查询, 使用@Transaction则会多个查询在一个事务中, 避免因为其他的事务导致

DML

  1. 增删改查全部以主键为准, 即数据的其他属性可以对应不上数据表中记录也可以根据主键删除
  2. ROOM中DML全部由被注解修饰的抽象函数来执行
  3. DML函数中Insert可以返回Long类型, 其他Update/Delete返回Int类型. 或全部返回Unit.
  4. 当参数是可变时, 返回值应也是可变类型, 否则只会返回第一条记录的值
  5. 可变类型包括 List/MutableList/Array
  6. 当进行DML进行多个数据体的操作时(例如插入多个用户), 只要有一个不符合就全部丢弃提交
  7. 除Transaction其他DML注解都要求为抽象/公开

所有DML操作都要求在被@Dao修饰的接口中定义抽象函数

@Dao
interface BookDao {

    @Query("select * from Book")
    fun find(): Flow<Book>

    @Insert
     fun insert(vararg book: Book): List<Long>

    @Delete
    fun delete(book: Book): Int

    @Update
    fun update(book: Book): Int

}
  1. Dao可以是抽象类或者接口

Insert

@Insert
fun insert(book: Book): Long

@Insert
fun insert(vararg book: Book): List<Long>
// 多行插入
@Insert
fun insert(book: List<Book>): List<Long>
// 多行插入

@Insert

/**
* 在插入列时出现冲突如何处理
* Use {@link OnConflictStrategy#ABORT} (默认) 回滚事务
* Use {@link OnConflictStrategy#REPLACE} 替换已存在的列
* Use {@link OnConflictStrategy#IGNORE} 保持已存在的列
*/
@OnConflictStrategy
int onConflict() default OnConflictStrategy.ABORT;
  1. 修饰的函数返回值必须是Long/List<Long>: 表示插入的记录主键值, 如果主键不是Long或Int则返回值表示索引(1开始). 如果参数不是集合则返回值不允许为集合只能是Long
  2. 自动产生的主键要求主键是Long类型, 同时值必须是0才会自动生成, 如果手动指定主键且重复会抛出SQLiteConstraintException

Delete

@Delete

修饰的函数返回类型必须是Int类型: 表示删除的行数, 从1开始

Update

@Update

根据主键匹配来更新数据行

修饰的函数返回类型必须是Int类型: 表示删除的行数, 从1开始

Query

@Query

该注解只接受一个字符串参数, 该字符串属于SQL查询语句, 会被编译器校验规则和代码高亮以及自动补全(这里很强大).

返回值和查询列是否匹配会被编译器校验

想要引用函数参数使用:{参数名称}

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

    @Query("SELECT * FROM user WHERE first_name LIKE :search "
           + "OR last_name LIKE :search")
    public List<User> findUserWithName(String search);
}
  1. 编写SQL语句时, 数据表严格对照类名(区分大小写), 否则影响重命名类名时无法照顾到SQL语句中的表名

字段映射

数据表的你可能只需要几个字段, 那么可以创建一个新的对象用于接收查询的部分字段结果

user这张数据表包含的字段很多

public class NameTuple {
    @ColumnInfo(name="first_name")
    public String firstName;

    @ColumnInfo(name="last_name")
    public String lastName;
}

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName();
}
  • NameTuple对象可以非Entity注解修饰
  • 名称对应数据表中的字段名(或者使用ColumnInfo注解)

查询参数

查询的参数可以使用集合

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

查询结果

  • 实体对象: 查询集合的第一个对象
  • 数组: 无结果空数组
  • 集合: 无结果空集合

实体对象未查询到返回NULL, 集合未查询到返回空的List非NULL

查询类型协程RxJava生命周期
可观察读取Flow<T>Flowable<T>Publisher<T>Observable<T>LiveData<T>
单次读取suspend funSingle<T>Maybe<T>
单次写入suspend funSingle<T>Maybe<T>Completable<T>

可观察

查询函数可以通过以下返回类型来注册观察者

  • LiveData

    • 初始化

    • 删除

    • 更新

    • 插入

  • Flowable

    • 插入

    • 更新

查询语句中的数据表中的行更新后会通知观察者.

执行SQL语句

@Query注解实际上可以执行任何SQL语句

@Query("delete from book where bookId = :bookId")
fun deleteFromId(bookId: Int): Int // 通过Id来删除行, 返回值为Int时表示影响表行数

模糊查询

@Query(
        "select * from conversation where case when conversationType = 1 " +
                "then (select name from chat_group where subjectId = id)" +
                "else (select nickName from person where subjectId = userId) end like '%' || :keyword || '%' " +
                "order by updateTs desc"
    )

视图

@DatabaseView

数据库视图表示创建一个虚拟的数据表, 其表结构可能是其他数据表的部分列. 主要是为了复用数据表

@DatabaseView("SELECT user.id, user.name, user.departmentId," +
        "department.name AS departmentName FROM user " +
        "INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(
    var id: Long,
    var name: String?,
    var departmentId: Long,
    var departmentName: String?
)

字段名和实体类的字段名同名即可自动映射到

注册视图到数据库上视图才可以被创建, 之后就可以像查询数据表一样查询视图; view数组就是所有的视图字节码

@Database(entities = [User::class], views = [MovieView::class], version = 1)
abstract class SQLDatabase : RoomDatabase() {
    abstract fun user(): UserDao
}

注解参数

public @interface DatabaseView {
    /**
     * 查询语句
     */
    String value() default "";

    /**
     * 视图名称, 默认为类名
     */
    String viewName() default "";
}

事务

在接口回调中的所有数据库操作都属于事务, 只要失败全部回滚

public void runInTransaction(@NonNull Runnable body)

public <V> V runInTransaction(@NonNull Callable<V> body)

在Dao中创建事务

@Dao
abstract class UsersDao {
  @Transaction
  open suspend fun setLoggedInUser(loggedInUser: User) {
    deleteUser(loggedInUser)
    insertUser(loggedInUser)
  }

  @Query("DELETE FROM users")
  abstract fun deleteUser(user: User)

  @Insert
  abstract suspend fun insertUser(user: User)
}

避免在事务中执行额外的耗时操作, 会导致阻塞数据库(因为事务按顺序每次执行一次)

ROOM

创建数据库访问对象

应该遵守单例模式, 不需要访问多个数据库实例对象

static <T extends RoomDatabase> Builder<T>	databaseBuilder(Context context,  Class<T> klass, String name)
// 创建一个序列化的数据库

static <T extends RoomDatabase> Builder<T>	inMemoryDatabaseBuilder(Context context, Class<T> klass)
// 在内存中创建一个数据库, 应用销毁以后会被清除

ROOM默认不允许在主线程访问数据库, 除非使用函数allowMainThreadQueries, 但是会导致锁住UI不推荐使用, 特别是在列表划动中查询数据库内容.

RoomDatabase

public abstract void clearAllTables()
// 清除所有数据表中的行, 会在关闭数据库以后删除, 如要求立即删除还需要执行`VACUUM`SQL

public boolean	isOpen()
// 如果数据库连接已经初始化打开则返回false

public void close()
// 关闭数据库(如果已经打开)

public InvalidationTracker	getInvalidationTracker()
public Returns the invalidation tracker for this database.

public SupportSQLiteOpenHelper	getOpenHelper()
// 返回使用这个数据库的SQLiteOpenHelper对象

public Executor getQueryExecutor()
public Executor getTransactionExecutor()
  
public boolean	inTransaction()
// 如果当前线程是在事务中返回true

public Cursor query(String query, Object[] args)
// 使用参数查询数据库的快捷函数

构造器

RoomDatabase.Builder 该构造器负责构建一个数据库实例对象

public Builder<T>	addMigrations(Migration... migrations)
// 添加迁移

public Builder<T>	allowMainThreadQueries()
// 允许主线程查询

public Builder<T>	createFromAsset(String databaseFilePath)
// 配置room创建和打开一个预打包的数据库, 在'assets/'目录中

public Builder<T>	createFromFile(File databaseFile)
// 配置room创建和打开一个预打包的数据库

public Builder<T>	fallbackToDestructiveMigration()
// 如果未找到迁移, 则允许进行破坏性的数据库重建

public Builder<T>	fallbackToDestructiveMigrationFrom(int... startVersions)
// 只允许指定的开始版本进行破坏性的重建数据库
  
public Builder<T>	fallbackToDestructiveMigrationOnDowngrade()
// 如果降级旧版本时迁移不可用则允许进行破坏性的迁移

public T	build()
// 创建数据库

不常用的函数

public Builder<T>	enableMultiInstanceInvalidation()
// 设置当一个数据库实例中数据表无效应该通知和同步其他相同的数据库实例, 必须两个实例都启用才有效.
// 这不适用内存数据库, 只是针对不同数据库文件的数据库实例
// 默认未启用

public Builder<T>	openHelperFactory(SupportSQLiteOpenHelper.Factory factory)
// 设置数据库工厂

public Builder<T>	setQueryExecutor(Executor executor)
// 设置异步查询时候的线程执行器, 一般不使用该函数默认就好, 直接使用协程就好了

public Builder<T>	setTransactionExecutor(Executor executor)
// 设置异步事务时的线程执行器

日志

设置SQLite的日志模式

public Builder<T>	setJournalMode(RoomDatabase.JournalMode journalMode)
// 设置日志模式

JournalMode

  • TRUNCATE 无日志
  • WRITE_AHEAD_LOGGING 输出日志
  • AUTOMATIC 默认行为, RAM低或者API16以下则无日志

生命周期

Builder<T>	addCallback(RoomDatabase.Callback callback)

RoomDatabase.Callback

public void	onCreate(SupportSQLiteDatabase db)
// 首次创建数据库的时候

public void	onDestructiveMigration(SupportSQLiteDatabase db)
// 破坏性迁移后

public void	onOpen(SupportSQLiteDatabase db)
// 打开数据库时

类型转换

查询语句中默认只允许使用基本类型及其装箱类, 如果我们想使用其他类型作为查询条件以及字段类型则需要定义类型转换器.

使用@TypeConverter可以在自定义对象和数据库序列化之间进行内容转换

class  DateConvert {

    @TypeConverter
    fun fromDate(date: Date): Long {
        return date.time
    }

    @TypeConverter
    fun toDate(date: Long): Date {
        return Date(date)
    }
}

TypeConverters可以修饰

  • 数据库(Database)
  • 数据表(Entity)
  • POJO字段
  • Dao/Dao函数/Dao函数参数

根据修饰不同所作用域也不同

修饰数据库中则整个数据库操作中Date都会经过转换器

@Database(entities = {User.java}, version = 1)
@TypeConverters({DateConvert.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

DQL

ROOM支持查询函数返回四种类型

  1. Single/Mabye/Completable/Observable/Flowable 等RxJava的被观察者

  2. LiveData: JetPack库中的活跃观察者

  3. Flow: Kotlin协程中的流

  4. Cursor: SQLite在Android中最原始的查询结果集, 此返回对象无法监听数据库变化

我不再推荐在项目中使用RxJava, 因为无法方便实现并发并且容易产生回调地域. 这里建议使用协程, 如果觉得无法完全替换RxJava, 推荐使用我的开源项目Net

除Cursor之外我列举的所有返回类型都支持在回调中监听数据库查询结果变化. 当你查询的数据表发生变化后就会触发观察者(即使该变化不符合查询结果)然后重新查询. 并且当你的查询涉及到的所有的数据表都会在变更时收到通知.

@Query("select * from Book")
fun find(): Flow<Array<Book>>

@Query("select * from Book")
fun find(): Observable<Array<Book>>

@Query("select * from Book")
fun find(): LiveData<Array<Book>>

@Query("select * from Book")
fun find(): LiveData<List<Book>> // List 或者 Array都是可以的

@Query("select * from Book")
fun find(): Flow<Array<Book>>

@Query("select * from Book")
fun find(): Cursor

示例查询Flow

val result = db.book().find()

GlobalScope.launch {
  result.collect { Log.d("日志", "result = $it") }
}
  1. 前面提到每次变动数据表都会导致Flow再次执行, 这里我们可以使用函数distinctUntilChanged过滤掉重复数据行为(采用==判断是否属于相同数据, data class 默认支持, 其他成员属性需要自己重新equals函数)

    GlobalScope.launch {
      bookFlow.distinctUntilChanged().collect {
        Log.d("日志", "result = $it")
      }
    }
    
  2. 建议配合Net使用, 可以做到自动跟随生命周期以及异常处理

FTS3

Match

特性

我会不断更新文章, 介绍跟随版本更新的新特性

预打包的数据库

可以将数据库db文件放到一个路径(File)或者asset资产目录下, 然后在满足迁移数据库条件下ROOM通过复制预打包的数据库进行重建.

当前应用数据库版本预打包数据库版本更新应用数据库版本迁移策略描述
234破坏性迁移删除当前应用数据库重建版本 4
244破坏性迁移复制预打包数据库文件
234手动迁移复制预打包数据库文件, 且运行手动迁移 3->4
  1. 这里提及的破坏性迁移即创建ROOM时调用fallbackToDestructiveMigration函数, 手动迁移即没有调用
  2. 建议使用DataGrip来创建SQLite文件, 后缀.sqlite或者.db本质上没区别, 但是Android上一般使用db
  3. 启用预打包数据库的同时必须启用破坏性迁移, 否则抛出

预打包数据库也仅是一种回退方式, 仍然无法完整保留用户数据, 需要自己手动迁移

展开投影

当查询出的数据表包含很多个字段, 而我只需要其中两个字段, 我就可以创建一个只包含两个字段的POJO(非Entity修饰)替代之前Entity类.

ROOM查询返回对象不要求一定为Entity修饰的数据表, 只要字段名对应上就可以投影到

既然介绍到展开投影, 在此强调下用于展开投影的POJO也可以使用某些注解, 例如ColumnInfo,PrimaryKey, Index

数据表和用于简化的POJO类

@Entity
data class Book(
    @PrimaryKey(autoGenerate = true)
    var bookId: Int = 0,
    var title: String = "drake"
)

// 假设我只关注书名, 而并不想去获取多余的id
data class YellowBook(
    @ColumnInfo(name = "title")
    var virtual: String = "drake"
)

原始的查询和使用展开投影后的查询

// 这是原本
@Query("select * from Book")
abstract fun findBook(): List<Book>

// 这是展开投影
@Query("select * from Book")
abstract fun findBook(): List<YellowBook>

目标实体

DAO 注释 @Insert@Update@Delete 现在具有一个新属性entity

和上面介绍的展开投影类似, 依然沿用YellowBook来讲解

// 原本
@Insert
fun insert(vararg book: Book): List<Long>

// 使用目标实体
@Insert(entity = Book::class)
fun insert(vararg book: YellowBook): List<Long>
  • 这中间YellowBook缺少的字段会使用默认值(ColumnInfo的defaultValues)来插入, bookId会使用自动生成的主键id.
  • 这里提到的默认值不是Kotlin参数或者字段默认值, 而是SQLite中的默认值

自动迁移

@Database(
-   version = 1,
+   version = 2,
    entities = [ Doggos.class ],
+   autoMigrations = [
+         AutoMigration (from = 1, to = 2)
+     ]
  )
abstract class DoggosDatabase : RoomDatabase { }

针对在 @Database schema 中声明的实体,如添加新列或表,更新主键、外键或索引,或更改列的默认值,Room 会自动检测出这些变化,不需要额外介入。

从实现层面来说,Room 的自动迁移依赖于所生成的数据库 schema,因此在使用 autoMigrations 时,请确保 @Database 中的 exportSchema 选项为 true。否则将导致错误: Cannot create auto-migrations when export schema is OFF

某些数据库改变无法检测到, 需要一些帮助

  • @DeleteTable(tableName)
  • @RenameTable(fromTableName, toTableName)
  • @DeleteColumn(tableName, columnName)
  • @RenameColumn(tableName, fromColumnName, toColumnName)
@Database(
   version = 2,
   entities = [ GoodDoggos.class ],
   autoMigrations = [
        AutoMigration (
            from = 1, 
            to = 2,
            spec = DoggosDatabase.DoggosAutoMigration::class
       )
    ]
)
abstract class DoggosDatabase : RoomDatabase {   
  @RenameTable(fromTableName = "Doggos", toTableName = "GoodDoggos")
    class DoggosAutoMigration: AutoMigrationSpec {   }
}

手动迁移

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        ...
    }
}

Room.databaseBuilder(applicationContext, DoggosDatabase::class.java, "doggos-database")
    .addMigrations(MIGRATION_1_2,)
    .build()

如果您在同一个版本上同时定义了 Migration 和自动迁移,那么只有 Migration 会生效