Room属于Google推出的JetPack组件库中的数据库框架.java
分析android
总结:git
考虑到主流确定使用ROOM, 考虑到体积确定不会使用Realm和ObjectBox(ROOM函数也比这两个少不少), 因为目前Kotlin为主以及SQL语句检查/JetPack支持因此我是强烈推荐使用ROOM.github
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配置sql
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会在建立数据库对象时就会建立好全部已注册的数据表结构数据库
建立数据库数组
@Database(entities = [Book::class], version = 1)
abstract class SQLDatabase : RoomDatabase() {
abstract fun book(): BookDao
}
复制代码
建立操做接口bash
@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
修饰类做为数据表, 数据表名称不区分大小写
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
public @interface Index {
/** * 指定索引的字段名称 */
String[] value();
/** * 索引字段名称 * index_${tableName}_${fieldName} 示例: index_Foo_bar_baz */
String name() default "";
/** * 惟一 */
boolean unique() default false;
}
复制代码
@Ignore
被该注解修饰的字段不会被算在表结构中
public @interface Database {
/** * 指定数据库初始化时建立数据表 */
Class<?>[] entities();
/** * 指定数据库包含哪些视图 */
Class<?>[] views() default {};
/** * 数据库当前版本号 */
int version();
/** * 是否容许处处数据库概要, 默认为true. 要求配置gradle`room.schemaLocation`才有效 */
boolean exportSchema() default true;
}
复制代码
@PrimaryKey
每一个数据库要求至少设置一个主键字段, 即便只有一个字段的数据表
boolean autoGenerate() default false; // 主键自动增加
复制代码
若是主键设置自动生成, 则要求必须为Long或者Int类型.
@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 = "冰火之歌"
)
复制代码
修饰字段做为数据库中的列(字段)
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
@Index
增长查询速度
// 须要被添加索引的字段名
String[] value();
// 索引名称
String name() default "";
// 表示字段在表中惟一不可重复
boolean unique() default false;
复制代码
@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
若是数据表实体存在一个字段属于另一个对象, 该字段使用此注解修饰便可让外部类包含该类全部的字段(在数据表中).
该外部类不要求同为Entity修饰的表结构
String prefix() default "";
// 前缀, 在数据表中的字段名都会被添加此前缀
复制代码
@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>则表示为一对多
)
复制代码
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
)
复制代码
经过参数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>
)
复制代码
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>
)
复制代码
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")) // 重复插入主键冲突致使事务失败
}
}
复制代码
final/private/abstract
, 但若是该函数同时包含@Query则能够为abstract@Transaction
则会多个查询在一个事务中, 避免由于其余的事务致使全部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
}
复制代码
@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;
复制代码
@Delete
修饰的函数返回类型必须是Int: 表示删除行索引, 从1开始
@Update
根据主键匹配来更新数据行
返回值能够Int, 表示更新列索引
@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);
}
复制代码
字段映射
数据表的你可能只须要几个字段, 那么能够建立一个新的对象用于接收查询的部分字段结果
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();
}
复制代码
查询参数
查询的参数可使用集合
@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 "";
}
复制代码
建立数据库访问对象
应该遵照单例模式, 不须要访问多个数据库实例对象
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不推荐使用, 特别是在列表划动中查询数据库内容.
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.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
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能够修饰
根据修饰不一样所做用域也不一样
修饰数据库中则整个数据库操做中Date都会通过转换器
@Database(entities = {User.java}, version = 1)
@TypeConverters({DateConvert.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
复制代码
shejiswuyanzubuxuyaodahsdajhdhjahdjas
ROOM支持查询函数返回四种类型
Single/Mabye/Completable/Observable/Flowable 等RxJava的被观察者
LiveData: JetPack库中的活跃观察者
Flow: Kotlin协程中的流
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") }
}
复制代码
前面提到每次变更数据表都会致使Flow再次执行, 这里咱们可使用函数distinctUntilChanged
过滤掉重复数据行为(采用==
判断是否属于相同数据, data class 默认支持, 其余成员属性须要本身从新equals
函数)
GlobalScope.launch {
bookFlow.distinctUntilChanged().collect {
Log.d("日志", "result = $it")
}
}
复制代码
建议配合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>
复制代码
defaultValues
)来插入, bookId
会使用自动生成的主键id.