Android Architecture Components 之 Room 篇

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: 包含數據庫持有者,做爲與應用持久化相關數據的底層鏈接的主要接入點。這個類須要用 @Database 註解,並知足下面條件:
    • 必須是繼承 RoomDatabase 的抽象類
    • 註解中包含該數據庫相關的實體類列表
    • 包含的抽象方法不能有參數,且返回值必須是被 @Dao 註解的類
  • Entity: 表示了數據庫中的一張表
  • DAO: 包含了訪問數據庫的一系列方法

它們與應用程序的關係如圖所示:
android

room_architecture

@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 都表明了一張表,其中的字段表明表中的一列。註解處理器會自動生成 AppDatabaseProductDao 對應的實現類 AppDatabase_ImplProductDao_Impl。能夠經過調用Room.databaseBuilder()Room.inMemoryDatabaseBuilder()在運行時獲取Database實例,但要注意,實例化 RoomDatabase 是至關昂貴的,最好按照單例模式只建立一個Database實例。git

定義 Entity

爲了讓 Room 能夠訪問 entity,entity 中的字段必須是 public 的,或者提供了getter/setter方法。默認狀況下,Room 會將 entity 中的每一個字段做爲數據庫表中一列,若是你不想持久化某個字段,可使用 @Ignore 註解。默認數據庫表名爲 entity 類名,你能夠經過 @Entity 註解的 tableName 屬性 更改,默認列名是字段名,你能夠經過 @ColumnInfo 註解更改。github

主鍵

每一個 entity 必須至少有一個字段做爲主鍵(primary key),即便該 entity 只有一個字段。使用 @PrimaryKey 註解來指定主鍵,若是你但願 SQLite 幫你自動生成這個惟一主鍵,須要將 @PrimaryKeyautoGenerate 屬性設置成 true,不過須要改列是 INTEGER 類型的。若是字段類型是 longintInsert 方法會將 0 做爲缺省值,若是字段類型是 IntegerLong 類型,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註解的 onDeleteonUpdate 屬性指定級聯操做,如級聯更新和級聯刪除: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 表包含 idfirstNamestreetstatecity,和 post_code 列。
Embedded 字段也能包含其餘 Embedded 字段。
若是有另外一個組合列也是 Address 類型的,可使用 @Embedded 註解的 prefix 屬性添加列名前綴以保證列的惟一性。

使用 DAO

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,返回值也能夠是 RxJava2PublisherFlowable(須要添加 android.arch.persistence.room:rxjava2 依賴),甚至能夠是 Cursor(不建議直接使用 Cursor API )。

數據庫的更新與遷移

隨着應用功能的改變,你須要去更改 entity 和數據庫,但不少時候,你不但願所以丟失數據庫中已存在的的數據,尤爲是沒法從遠程服務器恢復這些數據時。也就是說,若是你不提供必要的遷移操做,Room 將會重建數據庫,數據庫中全部的數據都將丟失。
爲此, Room 容許你寫一些 Migration 類去保護用戶數據,每一個 Migration 類指定一個 startVersionendVersion,在運行時,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));
    }
}
複製代碼

高級用法與技巧

TypeConverter

有些時候,咱們須要把一些自定義數據類型存入數據庫,或者在存入數據庫前作一些類型轉換,如咱們須要把 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,那麼數據庫中全部的 DaoEntity 都能使用它。若是註解了 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);
}
複製代碼

Database 對象的建立

實例化 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 還容許你在查詢操做中直接返回 LiveDataRxJavaPublisherFlowable

參考

相關文章
相關標籤/搜索