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.