使用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的類應該滿足以下條件:
- 繼承了RoomDatabase的抽象類;
- 包含實體列表, 而這些實體與該注解之下數據庫關聯;
- 包含一個抽象方法, 無參且返回一個注解了@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魯棒的查詢證實能力, 允許應用在加載資源時消耗更少的資源, 提升了應用的性能和用戶體驗.