阅读 1199

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

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

分析

  1. Realm
    1. 高性能, 比SQLite快十倍
    2. 支持RxJava/Kotlin, 不支持协程
    3. 但不支持嵌套类而且要求指定默认值, 嵌套数据类我觉得不可或缺
    4. 自定义数据库引擎, 故会要求导入JNI库, 会导致apk体积暴增(多个架构平台整合超过5MB)
    5. 无法使用一般的SQLite图形工具查看, 官方图形工具简陋
  2. DBFlow
    1. 主要使用函数操作数据库, 学习成本高
    2. 原生支持数据库加密
    3. 支持监听数据库
    4. 支持协程/Kotlin/RxJava
    5. 冷门, 特别是国内
  3. GreenDao
    1. 比较落伍, 配置复杂
    2. 不支持监听数据表/Kotlin/协程等特性
    3. 已经不再积极维护, 官方目前积极维护旗下另外开源数据库ObjectBox
  4. ObjectBox
    1. 冷门
    2. 高性能, 比SQLite快十倍甚至超越Realm
    3. 自定义引擎导致体积庞大
    4. 无法使用SQLite图形工具查看
    5. 支持配置实体类来自动化的数据库迁移
  5. ROOM
    1. 主流
    2. 学习成本相对低, 支持SQL语句
    3. 官方维护, JetPack组件中的数据库框架
    4. 监听数据表
    5. 支持Kotlin协程/RxJava
    6. 具备SQL语句高亮和编译期检查(具备AndroidStudio的支持)
    7. 使用SQLite便于多个平台的数据库文件传递(例如有些联系人信息就是一个SQLite文件)
    8. 由于是SQLite可以通过第三方框架进行数据库加密(ROOM原生不支持)

总结:

考虑到主流肯定使用ROOM, 考虑到体积肯定不会使用Realm和ObjectBox(ROOM函数也比这两个少很多), 由于目前Kotlin为主以及SQL语句检查/JetPack支持所以我是强烈推荐使用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()

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

val books = db.user().qeuryAll()
复制代码

注解

Entity

@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 {};
}
复制代码

ROOM要求每个数据库序列化字段为public访问权限

Index

@Index

public @interface Index {
    /**
     * 指定索引的字段名称
     */
    String[] value();

    /**
     * 索引字段名称
     * index_${tableName}_${fieldName} 示例: index_Foo_bar_baz
     */
    String name() default "";

    /**
     * 唯一
     */
    boolean unique() default false;
}
复制代码

Ignore

@Ignore

被该注解修饰的字段不会被算在表结构中

Database

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

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

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

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

PrimaryKey

@PrimaryKey

每个数据库要求至少设置一个主键字段, 即使只有一个字段的数据表

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 = "冰火之歌"
)
复制代码

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;
}
复制代码
  1. 主要使用的参数只有index/name
  2. 并不是只能修饰Entity类的字段, 非Entity的类也可以被此注解修饰(例如用于展开投影的POJO类, 后面会提到类型投影).

Index

@Index

增加查询速度

// 需要被添加索引的字段名
String[] value();

// 索引名称
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

@Relation

该注解早期只能修饰集合字段, 现在版本可以修饰任意类型.

下面演示创建一个一对多

创建一本书

@Entity
data class Book(
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0,
    var title:String = "drake"
)
复制代码

创建一个人

@Entity
data class Person(
    @PrimaryKey(autoGenerate = true) var id: Int = 0,
    var name: String
)
复制代码

创建一个用户

data class User(
    @Embedded var person: Person,

    @Relation(entity = Book::class, parentColumn = "id", entityColumn = "id")
    var book: Book // 这里表示一对一查询, 如果是集合List<Book>则表示为一对多
)
复制代码
  1. entity参数一般情况不需要指定可以从返回值类型推断, 如果要定义目标实体则可以使用该参数
  2. User并不是数据表(Entity)
  3. User必须包含Person的所有字段, 所以推荐使用Embedded注解
  4. parentColumn对应User中的字段, entityColumn对应Book中的字段(即一对多中的"多"数据表)

之后你可以自由插入Person或者Book, 然后查询的时候返回User

@Dao
interface UserDao {
    @Query("select * from person")
    fun find(): List<User>
}
复制代码

可以看到查询SQL语句查询的是person表, 但是函数返回类型是List<User>. 这个User即包含Peron和与Person对应的Book.

所谓对应的原则即parentColumn/entityColumn这两个属性,parentColumn表示

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

data class User(
    @Embedded var person: Person,

    @Relation(entity = Book::class, parentColumn = "id", entityColumn = "id")
    var book: Book // 这里表示一对一查询, 如果是集合List<Book>则表示为一对多
)
复制代码

桥接表

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

创建一个桥接表

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

通过参数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<Book>
)
复制代码
  1. Junction中的parentColumn/entityColumn默认值为Relation中的同名参数. 其含义为桥接表中的字段名

完整的多对多查询

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")) // 重复插入主键冲突导致事务失败
    }
}
复制代码
  1. Insert/Delete/Update修饰的函数本身就是在事务中
  2. ROOM仅允许一个事务运行, 其他事务排队
  3. @Tranaction要求修饰的函数不能为final/private/abstract, 但如果该函数同时包含@Query则可以为abstract
  4. 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

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

Delete

@Delete

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

Update

@Update

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

返回值可以Int, 表示更新列索引

Query

@Query

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

image-20191129182540753

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

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

@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);
}
复制代码

字段映射

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

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.

可观察

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

  • LiveData

    • 初始化
    • 删除
    • 更新
    • 插入
  • Flowable

    • 插入
    • 更新

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

###视图

@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?
)
复制代码

注册视图到数据库上视图才可以被创建, 之后就可以像查询数据表一样查询视图

@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 "";
}
复制代码

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

abstract void	clearAllTables()
// 清除所有数据表中的行

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

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

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

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

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

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

事务

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

public void runInTransaction(@NonNull Runnable body)

public <V> V runInTransaction(@NonNull Callable<V> body)
复制代码

RoomDatabase构造器

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

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

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

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

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

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

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

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

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

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

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

T	build()
// 创建数据库
复制代码

日志

设置SQLite的日志模式

Builder<T>	setJournalMode(RoomDatabase.JournalMode journalMode)
// 设置日志模式
复制代码

JournalMode

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

生命周期

Builder<T>	addCallback(RoomDatabase.Callback callback)
复制代码

RoomDatabase.Callback

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

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

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可以修饰

  1. 数据库(@Database修饰)
  2. 数据体(@Entity修饰)
  3. 数据体的字段属性, (不支持形参)
  4. 官方文档说可以修饰Dao但是我试验会报错

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

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

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

shejiswuyanzubuxuyaodahsdajhdhjahdjas

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使用, 可以做到自动跟随生命周期以及异常处理

特性

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

预打包的数据库

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

当前应用数据库版本 预打包数据库版本 更新应用数据库版本 迁移策略 描述
2 3 4 破坏性迁移 删除当前应用数据库重建版本4
2 4 4 破坏性迁移 复制预打包数据库文件
2 3 4 手动迁移 复制预打包数据库文件, 且运行手动迁移 3->4

建议使用DataGrip来创建SQLite文件, 后缀.sqlite或者.db本质上没区别, 但是Android上一般使用db

展开投影

当查询出的数据表包含很多个字段, 而我只需要其中两个字段, 我就可以创建一个只包含两个字段的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>
复制代码
  1. 这中间YellowBook缺少的字段会使用默认值(ColumnInfo的defaultValues)来插入, bookId会使用自动生成的主键id.
  2. 这里提到的默认值不是Kotlin参数或者字段默认值, 而是SQLite中的默认值
关注下面的标签,发现更多相似文章
评论