Room,一個 SQLite 的 ORM 庫,能夠方便地將 Java 對象轉成 SQLite 的表數據,不用再像傳統方式那樣寫 SQLite API 的樣板代碼了。同時 Room 提供了 SQLite 語法的編譯時檢查,而且能夠返回 RxJava,Flowable 和 LiveData observables。html
// Room (use 1.1.0-beta2 for latest beta)
implementation "android.arch.persistence.room:runtime:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
// Test helpers for Room
testImplementation "android.arch.persistence.room:testing:1.0.0"
複製代碼
Room 主要包含三個組件:java
@Database
註解,並知足下面條件:
RoomDatabase
的抽象類@Dao
註解的類它們與應用程序的關係如圖所示:
android
@Entity(tableName = "products")
public class ProductEntity {
@PrimaryKey
private int id;
private String name;
private String description;
...
}
複製代碼
@Dao
public interface ProductDao {
@Query("select * from products")
List<ProductEntity> getAllProducts();
@Query("select * from products where id = :id")
ProductEntity findProductById(int id);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertProduct(ProductEntity product);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAllProducts(List<ProductEntity> products);
@Delete
void deleteProduct(ProductEntity product);
}
複製代碼
@Database(entities = {ProductEntity.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract ProductDao productDao();
}
複製代碼
AppDatabase appDatabase = Room.databaseBuilder(this, AppDatabase.class, "product.db").build();
ProductDao productDao = appDatabase.productDao();
...
List<ProductEntity> allProducts = productDao.getAllProducts();
...
productDao.insertProduct(productEntity);
複製代碼
每一個 entity 都表明了一張表,其中的字段表明表中的一列。註解處理器會自動生成 AppDatabase
和 ProductDao
對應的實現類 AppDatabase_Impl
和 ProductDao_Impl
。能夠經過調用Room.databaseBuilder()
或 Room.inMemoryDatabaseBuilder()
在運行時獲取Database
實例,但要注意,實例化 RoomDatabase
是至關昂貴的,最好按照單例模式只建立一個Database
實例。git
爲了讓 Room 能夠訪問 entity,entity 中的字段必須是 public 的,或者提供了getter/setter方法。默認狀況下,Room 會將 entity 中的每一個字段做爲數據庫表中一列,若是你不想持久化某個字段,可使用 @Ignore
註解。默認數據庫表名爲 entity 類名,你能夠經過 @Entity
註解的 tableName
屬性 更改,默認列名是字段名,你能夠經過 @ColumnInfo
註解更改。github
每一個 entity 必須至少有一個字段做爲主鍵(primary key),即便該 entity 只有一個字段。使用 @PrimaryKey
註解來指定主鍵,若是你但願 SQLite 幫你自動生成這個惟一主鍵,須要將 @PrimaryKey
的 autoGenerate
屬性設置成 true
,不過須要改列是 INTEGER
類型的。若是字段類型是 long
或 int
,Insert
方法會將 0 做爲缺省值,若是字段類型是 Integer
或 Long
類型,Insert
方法會將 null 做爲缺省值。
若是 entity 的主鍵是複合主鍵(composite primary key),你就須要使用 @Entity
註解的 primaryKeys
屬性定義這個約束,如:數據庫
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
public String avatar;
}
複製代碼
有些時候,咱們須要添加索引以加快查詢速度,可使用 @Entity
註解的 indices
屬性建立索引,若是某個字段或字段組是惟一的,能夠將 @Index
註解的 unique
屬性設置爲 true
來強制這個惟一性,如:數組
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
複製代碼
SQLite 是關係型數據庫,不少時候咱們須要指定對象間的關係。即便大多數 ORM 庫容許實體類對象間相互引用,但 Room 明確禁止這樣作。由於級聯查詢不能發生在 UI 線程,UI 線程只有 16 ms 時間計算和繪製佈局,因此即便一個查詢只花費 5 ms,你的應用仍可能所以繪製超時,形成明顯的視覺問題。並且若是此時還有其餘的數據庫事務正在運行或者設備正在運行其餘磁盤敏感任務,那麼該查詢將花費更多的時間。而若是你不使用懶加載,你的應用將不得不去獲取比所須要的更多的數據,從而產生內存佔用問題。
ORM 庫一般把這個決定權交給開發者,以便開發者根據本身應用的狀況採起措施,而開發者一般會決定在應用和 UI 之間共享 model,然而,這種解決方案並不能很好地擴展,由於隨着UI的變化,共享 model 會產生一些難以讓開發人員預測和調試的問題。
例如,UI 加載了 Book
對象列表,每一個 book 都有一個 Author
對象,你可能最開始想採用懶加載的方式獲取 Book
實例(使用getAuthor()
方法獲取 author),第一次調用 getAuthor()
會調用數據庫查詢。過一會,你意識到你須要在 UI 上顯示做者名,你寫了下面這樣的代碼:服務器
authorNameTextView.setText(user.getAuthor().getName());
複製代碼
這看似正常的變動會致使 Author
表在主線程中被查詢。那提早查詢好做者信息是否是就好了呢?明顯不行,若是你再也不須要這些數據,就很難改變數據的加載方式了。例如,若是你的 UI 再也不須要顯示做者信息了,你的應用仍然會加載這些不須要的數據,從而浪費昂貴的內存空間,若是 Author
又引用了其餘表,那麼應用的效率將會進一步下降。
因此爲了讓 Room 能同時引用多個 entity,你須要建立一個包含每一個 entity 的 POJO,而後編寫一個鏈接相應表的查詢。這個結構良好的 model,結合 Room 健壯的查詢校驗功能,就可以讓你的應用花費更少的資源加載數據,提高應用的性能和用戶體驗。
雖然不能直接指定對象間關係,但能夠指定外鍵(Foreign Key)約束。例如對於 Book
entity 有一個做者的外鍵引用 User
,能夠經過 @ForeignKey
註解指定這個外鍵約束:app
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
class Book {
@PrimaryKey
public int bookId;
public String title;
@ColumnInfo(name = "user_id")
public int userId;
}
複製代碼
能夠經過 @ForeignKey
註解的 onDelete
和 onUpdate
屬性指定級聯操做,如級聯更新和級聯刪除:ide
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id",
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE))
複製代碼
有時,一個包含嵌套對象的 entity 或 POJO 表示一個完整的數據庫邏輯,可使用 @Embedded
註解將該嵌套對象的字段分解到該表中,如 User
表須要包含 Address
相關字段,可使用 @Embedded
註解表示這是個組合列:
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;
}
複製代碼
也就是說, User
表包含 id
,firstName
,street
,state
,city
,和 post_code
列。
Embedded 字段也能包含其餘 Embedded 字段。
若是有另外一個組合列也是 Address
類型的,可使用 @Embedded
註解的 prefix
屬性添加列名前綴以保證列的惟一性。
DAO(data access objects)是應用中操做數據庫的最直接的接口,應用中對數據庫的操做都表如今這個對象上,也就是說,應用不須要知道具體的數據庫操做方法,只須要利用 DAO 完成數據庫操做就好了,因此這一系列 Dao
對象也構成了 Room 的核心組件。DAO 能夠是個接口,也能夠是個抽象類,若是是個抽象類,那麼它能夠有個構造器,以 RoomDatabase
做爲惟一參數,Room 會在編譯時自動生成每一個 DAO 的實現類。
定義一個用 @Insert
註解的 DAO 方法,Room 會自動生成一個在單個事務中將全部參數插入數據庫的實現,若是方法只有一個參數,那麼它能夠返回 long
類型的 rowId
,若是方法參數是數組或集合,那麼它能夠返回 long[]
或 List<Long>
:
@Dao
public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);
@Insert
public void insertBothUsers(User user1, User user2);
@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}
複製代碼
@Update
註解的方法能夠更改一系列給定的 entity, 使用匹配的主鍵去查詢更改這些 entity,能夠返回 int
型的數據庫更新行數:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
複製代碼
@Delete
註解的方法能夠刪除一系列給定的 entity, 使用匹配的主鍵去查詢更改這些 entity,能夠返回 int
型的數據庫刪除行數:
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
複製代碼
@Query
註解的方法可讓你方便地讀寫數據庫,Room 會在編譯時驗證這個方法,因此若是查詢有問題編譯時就會報錯。Room 還會驗證查詢的返回值,若是查詢響應的字段名和返回對象的字段名不匹配,若是有些字段不匹配,你會看到警告,若是全部字段都不匹配,你會看到 error。下面是一個簡單的查詢,查詢全部的用戶:
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
複製代碼
若是你想要添加查詢條件,可使用 :
加 參數名
的方式獲取參數值:
@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);
}
複製代碼
固然,查詢條件集合也是支持的:
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
複製代碼
不少時候,咱們不須要查詢表中的全部字段,咱們只用到了 UI 用到的那幾列,爲了節省資源,也爲了加快查詢速度,咱們就能夠定義一個包含用到的字段的 POJO(這個 POJO 可使用 @Embedded
註解) ,查詢方法可使用這個 POJO:
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();
}
複製代碼
Room 也容許你方便地進行多表查詢,如查詢某個用戶所借的全部書籍信息:
@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);
}
複製代碼
多表查詢也能使用 POJO,如查詢用戶名和他的寵物名:
@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;
}
}
複製代碼
查詢方法的返回值能夠是 LiveData
以便你能隨着數據庫的更新實時更新 UI,返回值也能夠是 RxJava2
的 Publisher
或 Flowable
(須要添加 android.arch.persistence.room:rxjava2
依賴),甚至能夠是 Cursor
(不建議直接使用 Cursor API )。
隨着應用功能的改變,你須要去更改 entity 和數據庫,但不少時候,你不但願所以丟失數據庫中已存在的的數據,尤爲是沒法從遠程服務器恢復這些數據時。也就是說,若是你不提供必要的遷移操做,Room 將會重建數據庫,數據庫中全部的數據都將丟失。
爲此, Room 容許你寫一些 Migration
類去保護用戶數據,每一個 Migration
類指定一個 startVersion
和 endVersion
,在運行時,Room 會運行每一個 Migration
類的 migrate()
方法,以正確的順序將數據庫遷移到最新版本:
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
+ "`name` TEXT, PRIMARY KEY(`id`))");
}
};
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book "
+ " ADD COLUMN pub_year INTEGER");
}
};
複製代碼
注意,爲了保證遷移邏輯按預期運行,應該使用完整的查詢而不是引用表示查詢的常量。
在遷移過程完成後,Room 會驗證 schema 以確保遷移正確的完成了,若是 Room 發現了問題,會拋出一個包含不匹配信息的異常。
遷移數據庫是很重要也是沒法避免的操做,若是遷移出錯可能會致使你的應用陷入崩潰循環,爲了保持應用的穩定性,你必須提早測試好遷移的整的過程。爲了更好地測試,你須要添加 android.arch.persistence.room:testing
依賴,而且你須要導出數據庫的 schema。在編譯時,Room 會將你數據庫的 schema 信息導出爲 JSON 文件。爲了導出 schema,你須要在 build.gradle
文件中設置 註解處理器屬性room.schemaLocation
:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
複製代碼
你須要將這個導出的 JSON 文件保存在版本控制系統中,由於這個文件表明了數據庫的 schema 歷史記錄。同時你須要添加 schema 位置做爲 asset 文件夾:
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
複製代碼
測試工具中的 MigrationTestHelper
類能夠讀這些 schema 文件,同時它也實現了 JUnit4 的 TestRule
接口,因此它能夠管理建立數據庫:
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
MigrationDb.class.getCanonicalName(),
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrate1To2() throws IOException {
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
// db has schema version 1. insert some data using SQL queries.
// You cannot use DAO classes because they expect the latest schema.
db.execSQL(...);
// Prepare for the next version.
db.close();
// Re-open the database with version 2 and provide
// MIGRATION_1_2 as the migration process.
db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
// MigrationTestHelper automatically verifies the schema changes,
// but you need to validate that the data was migrated properly.
}
}
複製代碼
寫 JUnit 測試一般比 UI 測試更快更直觀,利用 Room.inMemoryDatabaseBuilder
構造 in-memory 版本的數據庫可讓你的測試更封閉:
@RunWith(AndroidJUnit4.class)
public class SimpleEntityReadWriteTest {
private UserDao mUserDao;
private TestDatabase mDb;
@Before
public void createDb() {
Context context = InstrumentationRegistry.getTargetContext();
mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
mUserDao = mDb.getUserDao();
}
@After
public void closeDb() throws IOException {
mDb.close();
}
@Test
public void writeUserAndReadInList() throws Exception {
User user = TestUtil.createUser(3);
user.setName("george");
mUserDao.insert(user);
List<User> byName = mUserDao.findUsersByName("george");
assertThat(byName.get(0), equalTo(user));
}
}
複製代碼
有些時候,咱們須要把一些自定義數據類型存入數據庫,或者在存入數據庫前作一些類型轉換,如咱們須要把 Date
類型的字段做爲 Unix 時間戳存入數據庫:
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();
}
}
複製代碼
而後使用 @TypeConverters
註解那些須要使用轉換器的元素。若是註解了 Database
,那麼數據庫中全部的 Dao
和 Entity
都能使用它。若是註解了 Dao
,那麼 Dao 中全部的方法都能使用它。若是註解了 Entity
,那麼 Entity 中全部的字段都能使用它。若是註解了 POJO,那麼 POJO 中全部的字段都能使用它。若是註解了 Entity
字段,那麼只有這個 Entity 字段能使用它。若是註解了 Dao
方法,那麼該 Dao 方法中全部的參數都能使用它。若是註解了 Dao
方法參數,那麼只有這個參數能使用它:
@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
複製代碼
查詢的時候,你仍然能夠用你的自定義類型,就像使用原語類型同樣:
@Dao
public interface UserDao {
...
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}
複製代碼
實例化 RoomDatabase
是至關昂貴的,最好使用 Dagger2
等依賴注入工具注入惟一的 Database
實例,如:
@Module(includes = ViewModelModule.class)
class AppModule {
...
@Singleton @Provides
GithubDb provideDb(Application app) {
return Room.databaseBuilder(app, GithubDb.class,"github.db").build();
}
@Singleton @Provides
UserDao provideUserDao(GithubDb db) {
return db.userDao();
}
@Singleton @Provides
RepoDao provideRepoDao(GithubDb db) {
return db.repoDao();
}
}
複製代碼
即便不使用依賴注入,也應該採用單例的方式建立 Database:
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase INSTANCE;
public abstract UserDao userDao();
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "sample.db")
.build();
}
}
}
return INSTANCE;
}
}
複製代碼
操做數據庫是個很是耗時操做,因此不能在主線程(UI線程)中查詢或更改數據庫,Room 也爲此作了線程檢查,若是你在主線程中操做了數據庫會直接拋出異常。爲了方便,Room 還容許你在查詢操做中直接返回 LiveData
或 RxJava
的 Publisher
和 Flowable
。