Room是Google官方出品的ORM(Object-relational mapping) 框架。當前咱們也知道當前還有不少的ORM框架,例如GreenDao、OrmLite、Litepal等。目前並無深刻了解這些框架,沒辦法比較各個框架的優缺點,可是相對而言,Room比較官方出品,且可以更好的與LiveData及RxJava等框架結合使用,仍是推薦各位學習和使用Room框架做爲數據存儲的基礎框架的。官方文檔: https://developer.android.com/training/data-storage/room/java
Room由三個重要的組件組成:Database、Entity、DAO。react
Database 是數據庫的持有者,是應用持久關聯數據的底層鏈接的主要訪問點。並且Database對應的類編寫時必須知足下面幾個條件:android
1. 必須是abstract類並且的extends RoomDatabase。數據庫
2. 必須在類頭的註釋中包含與數據庫關聯的實體列表(Entity對應的類)。json
3. 包含一個具備0個參數的抽象方法,並返回用@Dao註解的類。數組
在運行時,你能夠經過Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()獲取Database實例。app
Entity表明數據庫中某個表的實體類。框架
DAO封裝用於訪問數據庫的方法。ide
在build.gradle中添加以下配置:函數
// add for room
implementation "android.arch.persistence.room:runtime:1.1.1"
// room 配合 RxJava
implementation "android.arch.persistence.room:rxjava2:1.1.1"
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
// RxJava
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.1.3'
每一個Entity表明數據庫中某個表的實體類。默認狀況下Room會把Entity裏面全部的字段對應到表上的每一列。
若是須要制定某個字段不做爲表中的一列須要添加@Ignore註解,示例以下:
@Entity public class User { @PrimaryKey public int id; public String firstName; public String lastName; @Ignore Bitmap picture; }
示例中的picture字段代碼由於使用了@Ignore因此該字段不會映射到User表中。
Entity的實體類都須要添加@Entity註解,並且Entity類中須要映射到表中的字段須要保證外部能訪問到這些字段,這裏建議把字段設置爲public或者實現字段的getter和setter方法。
@Entity註解包含的屬性有:
默認狀況下Entity類的名字就是表的名字(不區分大小寫)。可是咱們也能夠經過@Entity的tableName屬性來自定義表名字。
以下代碼所示users表對應的實體類。
@Entity(tableName = "users") public class User { ... }
以下代碼users表中first_name列對應firstName字段,last_name列對應lastName字段。
@Entity(tableName = "users") public class User { @PrimaryKey public int id; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; }
每一個Entity都須要至少一個字段設置爲主鍵。即便這個Entity只有一個字段也須要設置爲主鍵。Entity設置主鍵的方式有兩種:
@Entity(primaryKeys = {"firstName", "lastName"}) public class User { public String firstName; public String lastName; }
@Entity public class User { @PrimaryKey public String firstName; @PrimaryKey public String lastName; }
若是但願Room給entity設置一個自增的字段,能夠設置@PrimaryKey的autoGenerate屬性。
數據庫索引用於提升數據庫表的數據訪問速度的。數據庫裏面的索引有單列索引和組合索引。Room裏面能夠經過@Entity的indices屬性來給表格添加索引。
@Entity(indices = {@Index("firstName"), @Index(value = {"last_name", "address"})}) public class User { @PrimaryKey public int id; public String firstName; public String address; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; }
索引也是分兩種的惟一索引和非惟一索引。惟一索引就想主鍵同樣重複會報錯的。能夠經過的@Index的unique數學來設置是否惟一索引。
@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)}) public class User { @PrimaryKey public int id; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; }
由於SQLite是關係型數據庫,表和表之間是有關係的。這也就是咱們數據庫中常說的外鍵約束(FOREIGN KEY約束)。Room裏面能夠經過@Entity的foreignKeys屬性來設置外鍵。正常狀況下,數據庫裏面的外鍵約束。子表外鍵於父表。當父表中某條記錄子表有依賴的時候父表這條記錄是不能刪除的,刪除會報錯。通常大型的項目不多會採用外鍵的形式。通常都會經過程序依賴業務邏輯來保證的。
下面咱們舉一個具體的例子來講明一下:
@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)}) public class User { @PrimaryKey public int id; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; } @Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "id", childColumns = "user_id")) public class Book { @PrimaryKey public int bookId; public String title; @ColumnInfo(name = "user_id") public int userId; }
上述代碼中,foreignKeys修飾後的Book表中的userId來源於User表中的id。
@ForeignKey屬性介紹:
entity:parent實體類(引用外鍵的表的實體)。
parentColumns:parent外鍵列(要引用的外鍵列)。
childColumns:child外鍵列(要關聯的列)。
onDelete:默認NO_ACTION,當parent裏面有刪除操做的時候,child表能夠作的Action動做有:
1. NO_ACTION:當parent中的key有變化的時候child不作任何動做。
2. RESTRICT:當parent中的key有依賴的時候禁止對parent作動做,作動做就會報錯。
3. SET_NULL:當paren中的key有變化的時候child中依賴的key會設置爲NULL。
4. SET_DEFAULT:當parent中的key有變化的時候child中依賴的key會設置爲默認值。
5. CASCADE:當parent中的key有變化的時候child中依賴的key會跟着變化。
onUpdate:默認NO_ACTION,當parent裏面有更新操做的時候,child表須要作的動做。Action動做方式和onDelete是同樣的。
deferred:默認值false,在事務完成以前,是否應該推遲外鍵約束。這個怎麼理解,當咱們啓動一個事務插入不少數據的時候,事務還沒完成以前。當parent引發key變化的時候。能夠設置deferred爲ture。讓key當即改變。
public class Address { public String street; public String state; public String city; @ColumnInfo(name = "post_code") public int postCode; } @Entity public class User { @PrimaryKey public int id; public String firstName; @Embedded public Address address; }
@Embedded註解屬性:
DAO是Room的主要組件,負責定義訪問數據庫的方法。Room使用過程當中通常使用抽象DAO類來定義數據庫的CRUD操做。
DAO能夠是一個接口也能夠是一個抽象類。若是它是一個抽象類,它能夠有一個構造函數,它將RoomDatabase做爲其惟一參數。Room在編譯時建立每一個DAO實例。DAO裏面全部的操做都是依賴方法來實現的。
當DAO裏面的某個方法添加了@Insert註解。Room會生成一個實現,將全部參數插入到數據庫中的一個單個事務。
1. OnConflictStrategy.REPLACE:衝突策略是取代舊數據同時繼續事務。
2. OnConflictStrategy.ROLLBACK:衝突策略是回滾事務。
3. OnConflictStrategy.ABORT:衝突策略是終止事務。
4. OnConflictStrategy.FAIL:衝突策略是事務失敗。
5. OnConflictStrategy.IGNORE:衝突策略是忽略衝突。
一個簡單的實例以下:
@Dao public interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertUsers(User... users); }
當DAO裏面的某個方法添加了@Update註解。Room會把對應的參數信息更新到數據庫裏面去(會根據參數裏面的primary key作更新操做)。
@Dao public interface UserDao { @Update(onConflict = OnConflictStrategy.REPLACE) int updateUsers(User... users); }
@Update註解的方法也能夠返回int變量。表示更新了多少行。
當DAO裏面的某個方法添加了@Delete註解。Room會把對應的參數信息指定的行刪除掉(經過參數裏面的primary key找到要刪除的行)。
@Delete也是能夠設置onConflict來代表衝突的時候的解決辦法。
@Dao public interface UserDao { @Delete void deleteUsers(User... users); }
@Delete對應的方法也是能夠設置int返回值來表示刪除了多少行。
@Query註解是DAO類中使用的主要註釋。它容許您對數據庫執行讀/寫操做。@Query在編譯的時候會驗證準確性,因此若是查詢出現問題在編譯的時候就會報錯。
Room還會驗證查詢的返回值,若是返回對象中的字段名稱與查詢響應中的相應列名稱不匹配的時候,Room會經過如下兩種方式之一提醒您:
@Query註解value參數:查詢語句,這也是咱們查詢操做最關鍵的部分。
查詢全部的信息。
@Dao public interface UserDao { @Query("SELECT * FROM user") User[] loadAllUsers(); }
返回結果能夠是數組,也能夠是List
大多數狀況下咱們都須要查詢知足特定的查詢條件的信息。
@Dao public interface UserDao { @Query("SELECT * FROM user WHERE firstName == :name") User[] loadAllUsersByFirstName(String name); }
查詢須要多個參數的狀況
@Dao public interface UserDao { @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") User[] loadAllUsersBetweenAges(int minAge, int maxAge); @Query("SELECT * FROM user WHERE firstName LIKE :search " + "OR lastName LIKE :search") List<User> findUserWithName(String search); }
有的時候可能指向返回某些特定的列信息。
下來的例子只查詢user表中的firstName和lastName信息。
@Entity public class User { @PrimaryKey public String firstName; @PrimaryKey public String lastName; public int age; } public class NameTuple { private String firstName; private String lastName; public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } } @Dao public interface UserDao { @Query("SELECT firstName, lastName FROM user") List<NameTuple> loadFullName(); }
在查詢的時候您可能須要傳遞一組(數組或者List)參數進去。
@Dao public interface UserDao { @Query("SELECT firstName, lastName FROM user WHERE region IN (:regions)") public List<NameTuple> loadUsersFromRegions(List<String> regions); }
意思就是查詢到結果的時候,UI可以自動更新。Room爲了實現這一效果,查詢的返回值的類型爲LiveData。
@Dao public interface UserDao { @Query("SELECT firstName, lastName FROM user WHERE region IN (:regions)") LiveData<List<NameTuple>> loadUsersFromRegionsSync(List<String> regions); }
關於LiveData的具體用法,咱們這裏就不作過多的討論了,後續的文章中咱們會針對LiveData作詳細的說明。
Room的查詢也能夠返回RxJava2的Publisher或者Flowable對象。固然了想要使用這一功能須要在build.gradle文件添加 implementation "android.arch.persistence.room:rxjava2:1.1.1" 依賴。
@Dao public interface UserDao { @Query("SELECT * from user") Flowable<List<User>> loadUser(); }
拿到Flowable<List<User>>數據以後就能夠去調用
mAppDatabase.userDao() .loadUser() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<List<User>>() { @Override public void accept(List<User> entities) { } });
查詢結果直接返回cursor。而後經過cursor去獲取具體的結果信息。
@Dao public interface UserDao { @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5") Cursor loadRawUsersOlderThan(int minAge); }
關於如何從Cursor裏去獲取數據,你們確定都很是熟悉,這裏就不贅述了。
有的時候可能須要經過多個表才能獲取查詢結果。這個就涉及到數據的多表查詢語句了。
@Dao public interface MyDao { @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") public List<Book> findBooksBorrowedByNameSync(String userName); }
也能夠查詢指定的某些列:
@Dao public interface MyDao { @Query("SELECT user.name AS userName, pet.name AS petName " + "FROM user, pet " + "WHERE user.id = pet.user_id") public LiveData<List<UserPet>> loadUserAndPetNames(); // You can also define this class in a separate file, as long as you add the // "public" access modifier. static class UserPet { public String userName; public String petName; } }
@Database註解能夠用來建立數據庫的持有者。該註解定義了實體列表,該類的內容定義了數據庫中的DAO列表。這也是訪問底層鏈接的主要入口點。註解類應該是抽象的而且擴展自RoomDatabase。
Database對應的對象(RoomDatabase)必須添加@Database註解,@Database包含的屬性:
在運行時,你能夠經過調用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()獲取實例。
由於每次建立Database實例都會產生比較大的開銷,因此應該將Database設計成單例的,或者直接放在Application中建立。
兩種方式獲取Database對象的區別:
咱們用一個簡單的實例來講明Database的建立。先定義一個abstract類AppDatabase繼承RoomDatabase:
@Database(entities = {User.class, Book.class}, version = 3) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); public abstract BookDao bookDao(); }
建立RoomDatabase實例(AppDatabase)。這裏咱們把RoomDatabase實例的建立放在Application裏面。
public class AppApplication extends Application { private AppDatabase mAppDatabase; @Override public void onCreate() { super.onCreate(); mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries() .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build(); } public AppDatabase getAppDatabase() { return mAppDatabase; } /** * 數據庫版本 1->2 user表格新增了age列 */ static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE User ADD COLUMN age integer"); } }; /** * 數據庫版本 2->3 新增book表格 */ static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL( "CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)"); } }; }
建立RoomDatabase實例的時候,RoomDatabase.Builder類裏面主要方法的介紹:
/** * 默認值是FrameworkSQLiteOpenHelperFactory,設置數據庫的factory。好比咱們想改變數據庫的存儲路徑能夠經過這個函數來實現 */ public RoomDatabase.Builder<T> openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory); /** * 設置數據庫升級(遷移)的邏輯 */ public RoomDatabase.Builder<T> addMigrations(@NonNull Migration... migrations); /** * 設置是否容許在主線程作查詢操做 */ public RoomDatabase.Builder<T> allowMainThreadQueries(); /** * 設置數據庫的日誌模式 */ public RoomDatabase.Builder<T> setJournalMode(@NonNull JournalMode journalMode); /** * 設置遷移數據庫若是發生錯誤,將會從新建立數據庫,而不是發生崩潰 */ public RoomDatabase.Builder<T> fallbackToDestructiveMigration(); /** * 設置從某個版本開始遷移數據庫若是發生錯誤,將會從新建立數據庫,而不是發生崩潰 */ public RoomDatabase.Builder<T> fallbackToDestructiveMigrationFrom(int... startVersions); /** * 監聽數據庫,建立和打開的操做 */ public RoomDatabase.Builder<T> addCallback(@NonNull RoomDatabase.Callback callback);
public class Converters { @TypeConverter public static Date fromTimestamp(Long value) { return value == null ? null : new Date(value); } @TypeConverter public static Long dateToTimestamp(Date date) { return date == null ? null : date.getTime(); } }
大部分狀況下設計的數據庫在版本的迭代過程當中常常是會有變化的。好比忽然某個表須要新加一個字段,須要新增一個表。這個時候咱們又不想失去以前的數據。Room裏面以Migration類的形式提供可一個簡化SQLite遷移的抽象層。Migration提供了從一個版本到另外一個版本遷移的時候應該執行的操做。
當數據庫裏面表有變化的時候(無論你是新增了表,仍是改變了某個表)有以下幾個場景。
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries() .fallbackToDestructiveMigration() .build();
因此在數據庫有變化的時候,咱們任什麼時候候都應該儘可能提供Migrating。Migrating讓咱們能夠本身去處理數據庫從某個版本過渡到另外一個版本的邏輯。咱們用一個簡單的實例來講明。有這麼個狀況,數據庫開始設計的時候咱們就一個user表(數據庫版本 1),第一次變化來了咱們須要給user表增長一個age的列(數據庫版本 2),過了一段時間又有變化了咱們須要新增長一個book表。三個過程版本1->2->3。
數據庫版本爲1的時候的代碼,以下:
@Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } public class AppApplication extends Application { private AppDatabase mAppDatabase; @Override public void onCreate() { super.onCreate(); mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries().build(); } public AppDatabase getAppDatabase() { return mAppDatabase; } }
數據庫版本爲2的時候的代碼,User增長了age列:
@Entity public class User { @PrimaryKey(autoGenerate = true) private long uid; private String name; private String address; private String phone; private Integer age; public long getUid() { return uid; } public void setUid(long uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } } @Database(entities = {User.class}, version = 2) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } public class AppApplication extends Application { private AppDatabase mAppDatabase; @Override public void onCreate() { super.onCreate(); mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries() .addMigrations(MIGRATION_1_2) .build(); } public AppDatabase getAppDatabase() { return mAppDatabase; } /** * 數據庫版本 1->2 user表格新增了age列 */ static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE user " + " ADD COLUMN age INTEGER"); } }; }
數據庫版本爲3的時候的代碼,新增了一個Book表:
@Entity public class Book { @PrimaryKey(autoGenerate = true) private Long uid; private String name; private Date time; private Long userId; public Long getUid() { return uid; } public void setUid(Long uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getTime() { return time; } public void setTime(Date time) { this.time = time; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } } @Database(entities = {User.class, Book.class}, version = 3) @TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); public abstract BookDao bookDao(); } public class AppApplication extends Application { private AppDatabase mAppDatabase; @Override public void onCreate() { super.onCreate(); mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db") .allowMainThreadQueries() .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build(); } public AppDatabase getAppDatabase() { return mAppDatabase; } /** * 數據庫版本 1->2 user表格新增了age列 */ static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE User ADD COLUMN age integer"); } }; /** * 數據庫版本 2->3 新增book表格 */ static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(SupportSQLiteDatabase database) { database.execSQL( "CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)"); } }; }
Migrating使用過程當中也有碰到一些坑,這裏告誡你們一個經驗Entity中能用Integer的時候不用int。
Room也容許你會將你數據庫的表信息導出爲一個json文件。你應該在版本控制系統中保存該文件,該文件表明了你的數據庫表歷史記錄,這樣容許Room建立舊版本的數據庫用於測試。
只須要在build.gradle文件中添加以下配置。編譯的時候就會導出json文件。
android { ... defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } } } // 用於測試 sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } }
json文件文件會導出在工程目錄下的schemas文件夾下面。裏面會有各個版本數據庫的信息。