Android Room使用詳解


 

使用Room將數據保存在本地數據庫


Room提供了SQLite之上的一層抽象, 既允許流暢地訪問數據庫, 也充分利用了SQLite.

處理大量結構化數據的應用, 能從在本地持久化數據中極大受益. 最常見的用例是緩存有關聯的數據碎片. 以這種方式, 在設備不能訪問網絡的時候, 用戶依然能夠瀏覽離線內容. 任何用戶發起的改變, 都應該在設備重新在線之后同步到服務器.

因為Room為你充分消除了這些顧慮, 使用Room而非SQLite是高度推薦的.

 

添加依賴

 

Room的依賴添加方式如下:

 1 dependencies {
 2     def room_version = "1.1.1"
 3 
 4     implementation "android.arch.persistence.room:runtime:$room_version"
 5     annotationProcessor "android.arch.persistence.room:compiler:$room_version"
 6 
 7     // optional - RxJava support for Room
 8     implementation "android.arch.persistence.room:rxjava2:$room_version"
 9 
10     // optional - Guava support for Room, including Optional and ListenableFuture
11     implementation "android.arch.persistence.room:guava:$room_version"
12 
13     // Test helpers
14     testImplementation "android.arch.persistence.room:testing:$room_version"
15 }

 

Room有3個主要構件:

  • Database: 包含了數據庫持有者, 並對於連接應用上持久化的相關數據, 作為一個主要的訪問點, 來服務. 注解了@Database的類應該滿足以下條件:
  1. 繼承了RoomDatabase的抽象類;
  2. 包含實體列表, 而這些實體與該注解之下數據庫關聯;
  3. 包含一個抽象方法, 無參且返回一個注解了@Dao的類;

在運行時, 你可以通過調用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()方法請求Database實例.

  • Entity: 表示數據庫內的表.
  • DAO: 包含用於訪問數據庫的方法.

這些構件, 以及它們與app余下內容的關系, 如下圖:


下面的代碼片斷, 包含了一個數據庫配置示例, 有一個實體和一個DAO:
User.java

 1 @Entity
 2 public class User {
 3     @PrimaryKey
 4     private int uid;
 5 
 6     @ColumnInfo(name = "first_name")
 7     private String firstName;
 8 
 9     @ColumnInfo(name = "last_name")
10     private String lastName;
11 
12     // Getters and setters are ignored for brevity,
13     // but they're required for Room to work.
14 }

 

UserDao.java

 1 @Dao
 2 public interface UserDao {
 3     @Query("SELECT * FROM user")
 4     List<User> getAll();
 5 
 6     @Query("SELECT * FROM user WHERE uid IN (:userIds)")
 7     List<User> loadAllByIds(int[] userIds);
 8 
 9     @Query("SELECT * FROM user WHERE first_name LIKE :first AND "
10            + "last_name LIKE :last LIMIT 1")
11     User findByName(String first, String last);
12 
13     @Insert
14     void insertAll(User... users);
15 
16     @Delete
17     void delete(User user);
18 }

 

AppDatabase.java

1 @Database(entities = {User.class}, version = 1)
2 public abstract class AppDatabase extends RoomDatabase {
3     public abstract UserDao userDao();
4 }

 

在創建了以上文件之后, 你能夠使用以下代碼來創建一個database實例:

 1 AppDatabase db = Room.databaseBuilder(getApplicationContext(), 2 AppDatabase.class, "database-name").build(); 

 

備注: 在實例化AppDatabase對象的時候, 你應該使用單例模式, 因為每一個RoomDatabase實例都是非常耗時的, 而且你也應該很少訪問多個實例.

 

使用Room實體定義數據

 

在使用Room持久化庫的時候, 把相關聯的域的集合定義為實體. 對於每一個實體, 數據庫都會創建一個表, 該表來持有數據項.

默認情況下, Room會為實體中定義的每個域創建一個列. 如果實體中有你不想持久化的域, 可以使用@Ignore來注解掉. 在Database類中, 你必須通過entities數據來引用實體類.

下面的代碼片斷展示了如何定義一個實體:

 1 @Entity
 2 public class User {
 3     @PrimaryKey
 4     public int id;
 5 
 6     public String firstName;
 7     public String lastName;
 8 
 9     @Ignore
10     Bitmap picture;
11 }


在持久化一個域, Room必須能夠訪問它. 你可以將域設置為public, 或者你可以提供該的getter/setter. 如果你使用了getter/setter的方式, 一定要記住: 在Room里面, 它們是基於JavaBeans轉換的.

備注: 實體要么有個空的構造器(如果相應的DAO類能夠訪問每一個持久化域的話), 要有構造器里面的參數, 數據類型和名字跟實體里面定義的域相匹配. Room也能夠使用包含全部或者部分域的構造器, 例如, 一個構造器只能獲取所有域中的幾個.

 

使用主鍵

 

每一個實體必須定義至少1個主鍵. 即使只有一個域, 你依然需要使用@PrimaryKey來注解它. 而且, 如果你想Room分配自動ID給實體的話, 你需要設置@PrimaryKey的autoGenerate屬性. 如果實體有一個復合主鍵的話, 你需要使用注解@Entity的primaryKeys屬性, 示例代碼如下:

1 @Entity(primaryKeys = {"firstName", "lastName"})
2 public class User {
3     public String firstName;
4     public String lastName;
5 
6     @Ignore
7     Bitmap picture;
8 }

 

默認情況下, Room使用實體類的名字作為數據庫表的名字. 如果你想要表擁有一個不同的名字, 設置@Entity注解的tableName屬性, 示例代碼如下:

 1 @Entity(tableName = "users") 2 public class User { 3 ... 4 } 


注意: SQLite中表名是大小寫敏感的.

跟tableName屬性相似的是, Room使用域的名字作為數據庫中列的名字. 如果你想要列有一個不同的名字的話, 給域添加@ColumnInfo注解, 示例代碼如下:

 1 @Entity(tableName = "users")
 2 public class User {
 3     @PrimaryKey
 4     public int id;
 5 
 6     @ColumnInfo(name = "first_name")
 7     public String firstName;
 8 
 9     @ColumnInfo(name = "last_name")
10     public String lastName;
11 
12     @Ignore
13     Bitmap picture;
14 }

 

 

注解索引和唯一性

 

依賴於你如何訪問數據, 你也許想要在數據庫中建立某些域的索引, 以加速查詢速度. 要給實體添加索引, 需要在@Entity中引入indices屬性, 並列出你想要在索引或者復合索引中引入的列的名字. 下列代碼說明了注解的處理過程:

 1 @Entity(indices = {@Index("name"),
 2         @Index(value = {"last_name", "address"})})
 3 public class User {
 4     @PrimaryKey
 5     public int id;
 6 
 7     public String firstName;
 8     public String address;
 9 
10     @ColumnInfo(name = "last_name")
11     public String lastName;
12 
13     @Ignore
14     Bitmap picture;
15 }

有些時候, 數據庫中的某些域或幾組域必須是唯一的. 你可以通過將注解@Index的unique屬性設置為true, 強制完成唯一的屬性.
下面的代碼示例防止表有兩行數據在列firstName和lastName擁有相同值:

 1 @Entity(indices = {@Index(value = {"first_name", "last_name"},
 2         unique = true)})
 3 public class User {
 4     @PrimaryKey
 5     public int id;
 6 
 7     @ColumnInfo(name = "first_name")
 8     public String firstName;
 9 
10     @ColumnInfo(name = "last_name")
11     public String lastName;
12 
13     @Ignore
14     Bitmap picture;
15 }


定義對象之間的關系

 

因為SQLite是關系型數據庫, 你可以指定對象之間的關系. 盡管大多數對象關系的映射允許實體對象引用彼此, 而Room卻顯式地禁止了這個特性. 要想了解這個討論背后的原因, 請查看這篇文章. //todo

盡管你不能使用直接的對象關系, Room仍然允許你在實體之間定義外鍵約束.

比如, 如果有一個實體類Book, 你可以使用@ForeignKey注解定義它和實體User的關系, 示例代碼如下:

 1 @Entity(foreignKeys = @ForeignKey(entity = User.class,
 2                                   parentColumns = "id",
 3                                   childColumns = "user_id"))
 4 public class Book {
 5     @PrimaryKey
 6     public int bookId;
 7 
 8     public String title;
 9 
10     @ColumnInfo(name = "user_id")
11     public int userId;
12 }

外鍵非常強大, 因為它允許你指定做什么操作, 在引用實體更新的時候. 比如, 你可以告訴SQLite為用戶刪除所有的書, 在相應的User實例被刪除時, 而該User被Book通過在@ForeignKey注解里面聲明onDelete = CASCADE而關聯.

 

備注: SQLite將@Insert(onConflict = REPLACE)作為REMOVE和REPLACE的集合來操作, 而非單獨的UPDATE操作. 這個取代沖突值的方法能夠影響你的外鍵約束.

 

創建嵌套對象

 

有些時候, 在數據庫邏輯中, 你想將一個實體或者POJO表示為一個緊密聯系的整體, 即使這個對象包含幾個域. 在這些情況下, 你能夠使用@Embedded注解來表示一個對象, 而你想將這個對象分解為表內的子域. 然后你可以查詢這些嵌套域, 就像你查詢其它的獨立列一樣.

舉個例子, User類包含一個Address類的域, 這個域表示的是street, city, state, postCode這幾個域的復合. 為了在表中單獨存儲復合的列, 在User類里面, 引入一個注解了@Embedded的Address域, 就像如下代碼片斷展示的一樣:

 1 public class Address {
 2     public String street;
 3     public String state;
 4     public String city;
 5 
 6     @ColumnInfo(name = "post_code")
 7     public int postCode;
 8 }
 9 
10 @Entity
11 public class User {
12     @PrimaryKey
13     public int id;
14 
15     public String firstName;
16 
17     @Embedded
18     public Address address;
19 }

這個表表示User對象包含如下幾列: id, firstName, street, state, city和post_code.

 

備注: 嵌套的域同樣可以包含其它的嵌套域.

 

如果實體擁有多個相同類型的嵌套域, 你可以通過設置prefix屬性保留每一列唯一. 然后Room給嵌套對象的每一個列名的起始處添加prefix設置的給定值.

 

通過Room DAO訪問數據

 

要通過Room持久化庫訪問應用的數據, 你需要使用數據訪問對象(data access objects, 即DAOs). Dao對象集形成了Room的主要構成, 因為每一個DAO對象都引入了提供了抽象訪問數據庫的方法.

使用DAO對象而非查詢構造器或者直接查詢來訪問數據庫, 你可以分開不同的數據庫架構組成. 此外, DAO允許你輕易地模擬數據庫訪問.

DAO要么是接口, 要么是抽象類. 如果DAO是抽象類的話, 它可以隨意地擁有一個將RoomDatabase作為唯一參數的構造器. Room在運行時創建DAO的實現.

備注: Room並不支持在主線程訪問數據庫, 除非在Builder調用allowMainThreadQueries()方法, 因為它很可能將UI鎖上較長一段時間. 但是, 異步查詢--返回LiveData/Flowable實例的查詢--則從此規則中免除, 因為它們在需要的時候會在后台線程異步地運行查詢.

 

方便地定義方法

 

使用DAO類, 可以非常方便地表示查詢.

 

插入

 

當你創建了一個DAO方法並注解了@Insert的時候, Room生成了一個實現, 在單個事務中將所有的參數插入數據庫.
下面的代碼片斷展示了幾個示例查詢:

 1 @Dao
 2 public interface MyDao {
 3     @Insert(onConflict = OnConflictStrategy.REPLACE)
 4     public void insertUsers(User... users);
 5 
 6     @Insert
 7     public void insertBothUsers(User user1, User user2);
 8 
 9     @Insert
10     public void insertUsersAndFriends(User user, List<User> friends);
11 }

如果@Insert方法只接收了一個參數, 它可以返回一個long, 表示新插入項的rowId; 如果參數是數組或者集合, 同時地, 它應該返回long[]或者List<Long>.

 

更新

 

按照慣例, 在數據庫中, Update方法修改了作為參數傳遞的實體集合. 它使用查詢來匹配每一個實體的主鍵.
下面的代碼片斷展示了如何定義這個方法:

1 @Dao
2 public interface MyDao {
3     @Update
4     public void updateUsers(User... users);
5 }

盡管通常情況下並不需要, 但是依然可以將這個方法返回int值, 表示在數據庫中被修改的行數.

 

刪除

 

按照慣例, Delete方法從數據庫中刪除了作為參數傳遞的實體集合. 它使用主鍵找到要刪除的實體.
下面的代碼片斷展示了如何定義這個方法:

1 @Dao
2 public interface MyDao {
3     @Delete
4     public void deleteUsers(User... users);
5 }

盡管通常情況下並不需要, 但是依然可以將這個方法返回int值, 表示從數據庫中刪除的行數.

 

查詢

 

@Query是在DAO類中使用的主要的注解. 它允許你在數據庫中執行讀寫操作. 每一個@Query方法都在編譯時被證實, 因為, 如果查詢有問題出現的話, 會出現編譯錯誤而非運行失敗.
Room也證實查詢的返回值, 以確定返回對象的域的名字是否跟查詢響應中對應列的名字匹配, Room使用如下兩種方式提醒你:

  • 如果只有一些域匹配, 它會給予警告;
  • 如果沒有域匹配, 它會給予錯誤;

 

簡單查詢

 

1 @Dao
2 public interface MyDao {
3     @Query("SELECT * FROM user")
4     public User[] loadAllUsers();
5 }

這是一個非常簡單的查詢, 加載了所有User. 在編譯時, Room知曉這是在查詢user表中所有列.

如果查詢語句包含語法錯誤, 或者user表在數據庫中並不存在, Room會在編譯時展示恰當的錯誤信息.

 

查詢語句中傳參

 

大多數時候, 你需要向查詢語句中傳參, 以執行過濾操作, 比如, 只展示大於某個年齡的user.

要完成這個任務, 在Room注解中使用方法參數, 如下所示:

1 @Dao
2 public interface MyDao {
3     @Query("SELECT * FROM user WHERE age > :minAge")
4     public User[] loadAllUsersOlderThan(int minAge);
5 }

當這個查詢在編譯時處理的時候, Room匹配到 :minAge, 並將它跟方法參數minAge綁定. Room使用參數名來執行匹配操作. 如果不匹配的話, app編譯時會發生錯誤.

你也可以在查詢中傳遞多個參數, 或者將參數引用多次, 如下所示:

1 @Dao
2 public interface MyDao {
3     @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
4     public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
5 
6     @Query("SELECT * FROM user WHERE first_name LIKE :search "
7            + "OR last_name LIKE :search")
8     public List<User> findUserWithName(String search);
9 }

 

返回列的子集

 

大多數情況下, 你只需要實體中的幾個域. 比如, UI中只需要展示用戶的姓和名, 而非用戶的每一個細節. 通過只查詢UI中展示的列, 將節省寶貴的資源, 查詢也更快.

Room允許從查詢中返回基於Java的對象, 只要結果列集合能夠映射成返回對象. 比如, 你創建了一個POJO來獲取用戶的名和姓:

1 public class NameTuple {
2     @ColumnInfo(name="first_name")
3     public String firstName;
4 
5     @ColumnInfo(name="last_name")
6     public String lastName;
7 }

現在, 你可以在查詢方法中使用這個POJO了:

1 @Dao
2 public interface MyDao {
3     @Query("SELECT first_name, last_name FROM user")
4     public List<NameTuple> loadFullName();
5 }

Room明白: 查詢返回了列first_name和last_name, 這些值能夠映射到NameTuple為的域中.

由此, Room能夠產生適當的代碼. 如果查詢返回了太多列, 或者返回了NameTuple類中並不存在的列, Room將展示警告信息.
備注: POJO也可以使用@Embedded注解.

 

傳遞參數集

 

一些查詢可能要求你傳入可變數目的參數, 直到運行時才知道精確的參數數量.

比如, 你可能想要搜索地區子集下的所有用戶. Room明白參數表示集合的時機, 並在運行時自動地基於提供了參數數目展開它.

1 @Dao
2 public interface MyDao {
3     @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
4     public List<NameTuple> loadUsersFromRegions(List<String> regions);
5 }

 

可觀察查詢

 

在執行查詢的時候, 經常想要在數據發生改變的時候自動更新UI. 要達到這個目的, 需要在查詢方法描述中返回LiveData類型的值. 在數據庫更新的時候, Room生成所有必要的代碼以更新LiveData.

1 @Dao
2 public interface MyDao {
3     @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
4     public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
5 }

備注: 在1.0版本的時候, Room使用查詢中訪問的表的列表來決定是否更新LiveData實例.

 

RxJava響應式查詢

 

Room也可以從定義的查詢中返回RxJava2中的Publisher和Flowable.

要使用這個功能, 在build.gradle文件中添加依賴: android.arch.persistence.room:rxjava2. 之后, 你可以返回在RxJava2中定義的數據類型, 如下所示:

1 @Dao
2 public interface MyDao {
3     @Query("SELECT * from user where id = :id LIMIT 1")
4     public Flowable<User> loadUserById(int id);
5 }


游標直接訪問

 

如果你的應用邏輯要求直接訪問返回的行, 你可以從查詢中返回Cursor對象, 如下所示:

1 @Dao
2 public interface MyDao {
3     @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
4     public Cursor loadRawUsersOlderThan(int minAge);
5 }

注意: 十分不推薦使用Cursor API. 因為它並不保證行是否存在以及行包含什么值.

除非你有需要Cursor的代碼並且並不輕易的修改它的時候, 你才可以使用這個功能.

 

查詢多表

 

有些查詢可能要求訪問多個表以計算結果. Room允許你寫任何查詢, 所以你也可以聯接表. 此外, 如果響應是可觀測數據類型, 諸如Flowable/LiveData, Room觀察並證實查詢中引用的所有表.

下面的代碼片段展示了如何執行表聯接, 以合並包含借書用戶的表和包含在借書數據的表的信息:

1 @Dao
2 public interface MyDao {
3     @Query("SELECT * FROM book "
4            + "INNER JOIN loan ON loan.book_id = book.id "
5            + "INNER JOIN user ON user.id = loan.user_id "
6            + "WHERE user.name LIKE :userName")
7    public List<Book> findBooksBorrowedByNameSync(String userName);
8 }

你也可以從這些查詢中返回POJO. 比如, 你可以寫查詢加載用戶和它的寵物名:

 1 @Dao
 2 public interface MyDao {
 3    @Query("SELECT user.name AS userName, pet.name AS petName "
 4           + "FROM user, pet "
 5           + "WHERE user.id = pet.user_id")
 6    public LiveData<List<UserPet>> loadUserAndPetNames();
 7 
 8 
 9    // You can also define this class in a separate file, as long as you add the
10    // "public" access modifier.
11    static class UserPet {
12        public String userName;
13        public String petName;
14    }
15 }

 

遷移Room數據庫

 

當應用中添加或者改變特性的時候, 需要修改實體類以反映出這些改變. 當用戶升級到最新版本的時候, 你不想用戶失去所有數據, 尤其是如果你還不能從遠程服務器恢復這些數據的時候.

Room持久化庫允許寫Migration類來保留用戶數據. 每一個Migration類指定了startVersion和endVersion. 在運行時, Room運行每一個Migration類的migrate()方法, 使用正確的順序遷移數據庫到最新版本.

注意: 如果你不提供必要的遷移, Room會重建數據庫, 這意味着你會失去原有數據庫中的所有數據.

 1 Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
 2         .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
 3 
 4 static final Migration MIGRATION_1_2 = new Migration(1, 2) {
 5     @Override
 6     public void migrate(SupportSQLiteDatabase database) {
 7         database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
 8                 + "`name` TEXT, PRIMARY KEY(`id`))");
 9     }
10 };
11 
12 static final Migration MIGRATION_2_3 = new Migration(2, 3) {
13     @Override
14     public void migrate(SupportSQLiteDatabase database) {
15         database.execSQL("ALTER TABLE Book "
16                 + " ADD COLUMN pub_year INTEGER");
17     }
18 };

注意: 要保證遷移邏輯按照預期進行, 需要使用全查詢而非引用表示查詢的常量.
在遷移完成之后, Room會證實這個計划, 以確保遷移正確在發生了. 如果Room發現了問題, 它會拋出包含不匹配信息的異常.

 

遷移測試

 

寫Migration並不是沒有價值的, 不能恰當的寫Migration會在應用中引起崩潰. 在保持應用的穩定性, 你應該事先測試Migration. Room提供了一個Maven測試工具. 但是, 如果要使這個工具工作, 你需要導出數據庫schema.

 

導出schema

 

在編譯的時候, Room會導出數據庫schem信息, 形成一個Json文件. 要導出schema, 需要在build.gradle文件中設置room.schemaLocation注解處理器屬性, 如下所示:
build.gradle:

 1 android {
 2     ...
 3     defaultConfig {
 4         ...
 5         javaCompileOptions {
 6             annotationProcessorOptions {
 7                 arguments = ["room.schemaLocation":
 8                              "$projectDir/schemas".toString()]
 9             }
10         }
11     }
12 }

你應該保存導出的Json文件--這些文件表示了數據庫schema的歷史--在你的版本控制體系中, 因為它允許Room創建老版本數據庫用於測試.

要測試這些Migration, 需要在測試需要的依賴中添加 anroid.arch.persistence.room:testing , 並在資產文件夾下添加schema地址, 如下所示:
build.gradle:

1 android {
2     ...
3     sourceSets {
4         androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
5     }
6 }

測試包提供了MigrationTestHelper類, 它能夠讀取這些schema文件. 它也實現了JUnit4 TestRule接口, 所有它能夠管理已創建的數據庫.

示例Migration測試如下:

 1 @RunWith(AndroidJUnit4.class)
 2 public class MigrationTest {
 3     private static final String TEST_DB = "migration-test";
 4 
 5     @Rule
 6     public MigrationTestHelper helper;
 7 
 8     public MigrationTest() {
 9         helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
10                 MigrationDb.class.getCanonicalName(),
11                 new FrameworkSQLiteOpenHelperFactory());
12     }
13 
14     @Test
15     public void migrate1To2() throws IOException {
16         SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
17 
18         // db has schema version 1. insert some data using SQL queries.
19         // You cannot use DAO classes because they expect the latest schema.
20         db.execSQL(...);
21 
22         // Prepare for the next version.
23         db.close();
24 
25         // Re-open the database with version 2 and provide
26         // MIGRATION_1_2 as the migration process.
27         db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);
28 
29         // MigrationTestHelper automatically verifies the schema changes,
30         // but you need to validate that the data was migrated properly.
31     }
32 }


測試數據庫

 

在使用Room持久化庫創建數據庫的時候, 證實應用數據庫和用戶數據的穩定性非常重要.

有兩種方式測試你的數據庫:

  • 在真機上;
  • 在虛擬機上(不推薦);

備注: 在運行應用的測試的時候, Room允許你創建模擬DAO類的實例. 使用這種方式的話, 如果不是在測試數據庫本身的話, 你不必創建完成的數據庫. 這個功能是可能的, 因為DAO並不泄露任何數據庫細節.

 

真機測試

 

測試數據庫實現的推薦途徑是在真機上運行JUnit測試. 因為這些測試並不創建Activity, 它們應該比UI測試執行地更快.

在設置測試的時候, 你應該創建內存版本數據庫, 以確保測試更加地密封. 如下所示:

 1 @RunWith(AndroidJUnit4.class)
 2 public class SimpleEntityReadWriteTest {
 3     private UserDao mUserDao;
 4     private TestDatabase mDb;
 5 
 6     @Before
 7     public void createDb() {
 8         Context context = InstrumentationRegistry.getTargetContext();
 9         mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
10         mUserDao = mDb.getUserDao();
11     }
12 
13     @After
14     public void closeDb() throws IOException {
15         mDb.close();
16     }
17 
18     @Test
19     public void writeUserAndReadInList() throws Exception {
20         User user = TestUtil.createUser(3);
21         user.setName("george");
22         mUserDao.insert(user);
23         List<User> byName = mUserDao.findUsersByName("george");
24         assertThat(byName.get(0), equalTo(user));
25     }
26 }

 

虛擬機測試

 

Room使用了SQLite支持庫, 后者提供了在Android Framework類里面匹配的接口. 這個支持允許你傳遞自定義的支持庫實現來測試數據庫查詢.
備注: 盡管這個設置允許測試運行地很快, 但它並不是值得推薦的, 因為運行在自己以及用戶真機上面的SQLite版本, 可能並不匹配你的虛擬機上面的SQLite版本.

 

使用Room引用復雜數據

 

Room提供了功能支持基數數據類型和包裝類型之間的轉變, 但是並不允許實體間的對象引用.

 

使用類型轉換器

 

有時候, 應用需要使用自定義數據類型, 該數據類型的值將保存在數據庫列中. 要添加這種自定義類型的支持, 你需要提供TypeConverter, 用來將自定義類型跟Room能夠持久化的已知類型相互轉換.

比如, 如果我們想要持久化Date類型, 我們需要寫下面的TypeConverter來在數據庫中保存等價的Unix時間戳:

 1 public class Converters {
 2     @TypeConverter
 3     public static Date fromTimestamp(Long value) {
 4         return value == null ? null : new Date(value);
 5     }
 6 
 7     @TypeConverter
 8     public static Long dateToTimestamp(Date date) {
 9         return date == null ? null : date.getTime();
10     }
11 }

上述示例定義了2個方法, 一個把Date轉變成Long, 一個把Long轉變成Date. 因為Room已經知道如何持久化Long對象, 它將使用這個轉換器持久化Date類型的值.

接下來, 添加@TypeConverters注解到AppDatabbase類上, 之后Room就能夠在AppDatabase中定義的每一個實體和DAO上使用這個轉換器.
AppDatabase.java

1 @Database(entities = {User.class}, version = 1)
2 @TypeConverters({Converters.class})
3 public abstract class AppDatabase extends RoomDatabase {
4     public abstract UserDao userDao();
5 }

使用這些轉換器, 你之后就能夠在其它的查詢中使用自定義的類型, 就像你使用基本數據類型一樣, 如下所示:
User.java

1 @Entity
2 public class User {
3     ...
4     private Date birthday;
5 }

UserDao.java

1 @Dao
2 public interface UserDao {
3     ...
4     @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
5     List findUsersBornBetweenDates(Date from, Date to);
6 }

你也可以限制@TypeConverters的使用范圍, 包括單個實體, DAO和DAO方法.

 

理解為什么Room不允許對象引用

 

要點: Room不允許實體類間的對象引用. 相反, 你必須顯式地請求應用需要的數據.

從數據庫到對應對象模型的映射關系是通用最佳實踐, 在服務器端也運行良好. 即使是在程序加載它們正在訪問的域的時候, 服務器依然執行良好.

然而在客戶端, 這種類型的懶加載並不可行, 因為它通常發生在UI線程, 而在UI線程止查詢硬盤信息產生了顯著的性能問題. UI線程只有16ms計算和繪制Activity更新的布局, 所以, 即使查詢花費了僅僅5ms, 看起來依然是應用繪制超時, 引起顯著的視覺差錯. 如果有另外的事件並行運行, 或者, 設備正在運行其它的硬盤密集型任務, 查詢要完成就要花費更多的時間. 然而, 如果不使用懶加載, 應用獲取超過需要的數據, 也會引起內存消耗問題.

對象關系型映射通常將這個決定留給開發者, 讓他們做出應用用例最佳的選擇. 開發者通常決定在應用和UI之間共享模型. 然后, 這個解決方案並不權衡地很好, 因為UI隨着時間改變, 共享模型會產生對於開發者而言難以參與和debug的問題.

比如, UI加載Book對象列表, 同時每一本書有個Author對象. 最初你可能設計查詢使用懶加載, 之后Book對象使用getAuthor()方法返回作者. getAuthor()方法的首次調用查詢了數據庫. 之后一段時間, 你發現同樣需要展示作者姓名. 你輕易地添加如下這樣的方法調用:

 1 authorNameTextView.setText(book.getAuthor().getName()); 

 

然后, 這個貌似無辜的改變引起Author表在主線程被查詢.

如果你提前查詢作者信息, 而在你不再需要這個數據之后, 將很難改變加載的方式. 比如, UI不再需要展示Author信息, 而應用依然高效地加載不同展示的數據, 浪費了寶貴的內存空間. 應用的效率將會降級, 如果Author類引用了其它的表, 如Books.

要使用Room同時引用多個實體, 需要創建包含每個實體的POJO類, 之后寫聯接了相應表的查詢語句. 這個結構良好的模型, 結合了Room魯棒的查詢證實能力, 允許應用在加載資源時消耗更少的資源, 提升了應用的性能和用戶體驗.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM