原文地址
Room持久化庫
Room為SQLite提供一個抽象層,在充分利用SQLite的同時,允許流暢的數據庫訪問
注意:引入Room到你的android工程,參看 adding components to your project
應用處理大量的結構化數據能夠從本地持久化數據獲益很多,最通用的例子是緩存相關的數據碎片。那樣,當設備不能訪問網絡的時候,用戶仍然可以瀏覽內容。任何用戶發起的內容改變在設備恢復網絡的時候同步到服務器上。
核心框架對raw SQL內容提供嵌入支持。盡管這些APIs是很給力的,但是他們相當低級并且需要大量的時間和精力去使用:
- raw SQL查詢沒有編譯時驗證。當你的數據圖改變,你需要手動的更新受影響的SQL查詢。這個過程是耗時的和容易出錯的。
- 你需要使用大量的樣板代碼在數據查詢和java數據對象之間轉換
Room為你處理這些問題。在Room中有三個主要組件。
-
Database: 你可以使用這個組件創建一個數據庫holder。注解定義了一系列entities并且類的內容提供了一系列DAOs,它也是下層的主要連接 的訪問點。
注解的類應該是一個抽象的繼承 RoomDatabase的類。在運行時,你能獲得一個實例通過調用Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder() - Entity:這個組件代表了一個持有數據行的類。對于每個entity,一個數據庫表被創建用于持有items。你必須引用entity類通過Database類中的entities數組。每個entity字段被持久化到數據庫中除非你注解它通過@Ignore.
注意:Entities能夠有一個空的構造函數(如果dao類能夠訪問每個持久化的字段)或者一個參數帶有匹配entity中的字段的類型和名稱的構造函數,例如一個只接收其中一些字段的構造函數。
- DAO:這個組件代表了一個類或者接口作為DAO。DAOs 是Room中的主要組件,并且負責定義訪問數據庫的方法。被注解為@Database的類必須包含一個沒有參數的抽象方法并且返回注解為@Dao的類。當在編譯時生成代碼,Room創建一個這個類的實現。
注意:使用DAO類訪問數據庫而不是query builders或者直接查詢。你可以把數據庫分成幾個組件。還有,DAOs允許你輕松的模擬數據庫訪問當你測試你的應用的時候。
這些組件和rest app的關系,如圖1.
圖1:room 架構圖
如下代碼片段包含一個數據庫配置的例子、一個entity,一個DAO:
User.java
@Entity
public class User {
@PrimaryKey
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
// Getters and setters are ignored for brevity,
// but they're required for Room to work.
}
UserDao.java
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
AppDatabase.java
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
通過創建以上文件,你可以使用如下代碼創建一個數據庫實例:
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
注意:你必須遵守單例模式當初始化一個AppDatabase對象,因為每個RoomDatabase實例是相當昂貴的,并且你幾乎不需要訪問多個實例。
Entities
當一個類被注解為@Entity并且引用到帶有@Database 注解的entities屬性,Room為這個數據庫做的entity創建一個數據表。
默認情況下,Room為每個定義在entity中的字段創建一個列。如果一個entity的一些字段你不想持久化,你可以使用@Ignore注解它們,像如下展示的代碼片段:
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
為了持久化一個字段,Room必須有它的入口。你可以使字段為public,或者你可以提供一個setter或者getter。如果你使用setter或者getter方法,記住在Room中他們遵守Java Beans的慣例。
Primary Key(主鍵)
每個entity必須定義至少一個字段作為主鍵。即使這里只有一個字段,你仍然需要使用@PrimaryKey注解這個字段。并且,如果你想Room動態給entities分配IDs,你可以設置@PrimaryKey’s 的autoGenerate屬性。如果entity有個組合的主鍵,你可以使用@Entity注解的primaryKeys的屬性,正如如下片段展示的那樣。
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
默認情況下,Room使用類名作為數據庫的表名。如果你希望表有一個不同的名稱,設置@Entity注解的tableName屬性,如下所示:
@Entity(tableName = "users")
class User {
...
}
注意:SQLite中的表名是大小寫敏感的。
與tablename屬性相似的是,Room使用字段名稱作為列名稱。如果你希望一個列有不同的名稱,為字段增加@ColumnInfo注解,如下所示:
@Entity(tableName = "users")
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
Indices and uniqueness(索引和唯一性)
根據你訪問數據的方式,你可能希望索引確切的字段去加速你的數據庫查詢。為了給一個entity增加索引。
在@Entity屬性中包含indices屬性,在索引或者組合索引中列出你希望包含的列的名稱。如下代碼片段說明了這個注解過程:
@Entity(indices = {@Index("name"), @Index("last_name", "address")})
class User {
@PrimaryKey
public int id;
public String firstName;
public String address;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
有時,確切的字段和組字段必須是獨一無二的。你可以強加這個獨一無二的特性通過設置一個@Index注解的unique屬性為true。如下代碼阻止了表擁有兩行包含同樣的firstName和last列的值集合。
@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;
}
Relationships
因為SQLite是個關系型數據庫,你能夠指明兩個對象的關系。雖然大多數ORM庫支持entity對象引用其他的。Room明確的禁止這樣。更多細節請參考 Addendum: No object references between entities.
即使你不能使用直接關系,Room仍然允許你定義外鍵約束在兩個entities中。
例如:如果有一個entity叫book,你可以定義它和user的關系通過使用 @ForeignKey
注解,如下所示:
@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;
}
外鍵是十分強大的,因為它們允許你指明當引用的entity被更新后做什么。例如,你可以讓SQLite為一個user刪除所有的書籍如果相應的user實例被刪除了通過包含@ForeignKey注解的onDelete=CASCADE屬性
注意:SQLite處理@Insert(OnConflict=REPLACE) 作為一個REMOVE和REPLACE操作而不是單獨的UPDATE操作。這個替換沖突值的方法能夠影響你的外鍵約束。更多細節,參看 SQLite documentation。
Nested objects
有時,你希望entity或者POJOs作為一個整體在你數據庫的邏輯當中,即使對象包含幾個字段。在這種情況下,你可以使用@Embedded注解去代表一個你希望分解成一個表中的次級字段的對象。接著你就可以查詢嵌入字段就像其他單獨的字段那樣。
例如,我們的user類能夠包含一個代表了street,city,state,postCode的組合字段Address。為了分別的保存組合列,包括被@Embedded注解的user類中的Address字段,如下所示:
class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
Table表示了一個包含如下名稱列的User對象:id,firstName,street,state,city和post_code。
注意:嵌入字段也包括其他嵌入字段
如果一個字段有多個同一類型的嵌入字段,你能保持每個列是獨一無二的通過設置prefix屬性。Room然后將所提供的值添加到嵌入對象中每個列名的開頭
Data Access Objects (DAOs)
Room中的主要組件是Dao類。DAOs抽象地以一種干凈的方式去訪問數據庫。
注意:Room不允許在主線程中訪問數據庫除非你在建造器中調用allowMainThreadQueries(),因為它可能長時間的鎖住UI。異步查詢(返回LiveData或者RxJava流的查詢)是從這個規則中豁免的因為它們異步的在后臺線程中進行查詢。
Methods for convenience(慣例方法)
這里有很多你可表示的查詢慣例使用DAO類。這篇文檔包括幾個通用的例子:
Insert
當你創建一個DAO方法并且使用@Insert注解它,Room生成一個在單獨事務中插入所有參數到數據庫中的實現。
如下代碼展示了幾個查詢實例:
@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);
}
如果@Insert方法接收只有一個參數,它可以返回一個插入item的新rowId 的long值,如果參數是一個集合的數組,它應該返回long[]或者List<Long>
更多細節,參看文檔 @Insert
注解,和 SQLite documentation for rowid tables
Update
Update 是更新一系列entities集合、給定參數的慣例方法。它使用query來匹配每個entity的主鍵。如下代碼說明如何定義這個方法:
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
盡管通常不是必須的,你能夠擁有這個方法返回int值指示數據庫中更新的數量。
Delete
Delete是一個從數據庫中刪除一系列給定參數的entities的慣例方法。它使用主鍵找到要刪除的entities。如下所示:
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
盡管通常不是必須的,你能夠擁有這個方法返回int值指示數據庫中刪除的數量。
Methods using @Query
@query 是DAO類中使用的主要注解,它允許你執行讀/寫操作在數據庫中。每個@Query方法在編譯時被校驗,所以如果查詢出了問題,將在編譯時出現而不是運行時。
- 它給出警告如果僅有一些字段匹配
- 它報錯如果沒有字段匹配
查詢示例:
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
這是載入所有用戶的非常簡單的查詢例子。在編譯時,Room知道這是查詢user表中的所有列。如果查詢包含語法錯誤,或者如果用戶表不存在,Room在你app編譯時會報出合適的錯誤消息。
往查詢中傳入參數:
大多數時間,你需要傳入參數到查詢中去過濾操作,例如只展示比一個特定年齡大的用戶,為了完成這個任務,在你的Room注解中使用方法參數,如下所示:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
當這個查詢在編譯器被處理,Room匹配:minAge綁定的方法參數。Room執行匹配通過使用參數名稱,如果沒有匹配到,在你的app編譯期將會報錯。
你也可以通過傳入多個參數或者多次引用它們在一個查詢當中,如下所示:
@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);
}
Returning subsets of columns(返回列中的子集)
多數時候,你僅僅需要獲取一個entity中的部分字段。例如,你的UI可能只展示user’s第一個和最后一個名稱,而不是所有關于用戶的細節。你保存有價值的資源通過獲取展示在你app’s的UI的列,你的查詢完成的更快。
Room允許你返回任何java對象從查詢中只要列結果集能夠被映射到返回的對象中。例如:
你能夠創建如下POJO通過拿取用戶的姓和名。
public class NameTuple {
@ColumnInfo(name="first_name")
public String firstName;
@ColumnInfo(name="last_name")
public String lastName;
}
現在,你可以使用這個POJO在你的查詢方法中:
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
Room理解查詢返回first_name和last_name的列值并且這些值被映射到NameTuple類的字段中。因此,Room能夠生成合適的代碼。如果查詢返回太多columns,或者一個列不存在,Room將會報警。
注意:這些POJOs也使用@Embedded注解
Passing a collection of arguments
你的部分查詢可能需要你傳入可變數量的參數,確切數量的參數直到運行時才知道。例如,你可能想提取來自某個地區所有用戶的信息。Room理解當一個參數代表一個集合并且自動的在運行時擴展它根據提供的參數數量。
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
Observable queries
你經常希望你的app’sUI自動更新當數據發生改變。為了實現這點,使用返回值類型為liveData在你的查詢方法描述中。Room生成所有需要的代碼去更新LiveData當數據庫被更新。
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}
注意:在1.0版本,Room使用被訪問的table列表在查詢中決定是否更新數據對象。
RxJava
Room也能返回RxJava2 Publisher和Flowable對象從你定義的查詢當中。為了使用這個功能,添加android.arch.persistence.room:rxjava2 到你的build Gradle依賴。你能夠返回Rxjava2定義的對象,如下所示:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
Direct cursor access(直接游標訪問)
如果你的應用邏輯直接訪問返回的行,你可以返回一個Cursor對象從你的查詢當中,如下所示:
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
注意:非常不建議使用Cursor API 因為它不能保證行是否存在或者行包含什么值。使用這個功能僅僅是因為你已經有期望返回一個cursor的代碼并且你不能輕易的重構。
Querying multiple tables
你的一些查詢可能訪問多個表去計算結果。Room允許你寫任何查詢,所以你也能連接表格。還有,如果答復是一個observable數據類型,例如Flowable或者LiveData,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);
}
你也能返回POJOs從這些查詢當中,例如,你可以寫一個查詢去裝載user和他們的寵物名稱,如下:
@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;
}
}
Using type converters (使用類型轉換)
Room為原始類型和可選的裝箱類型提供嵌入支持。然而,有時你可能使用一個單獨存入數據庫的自定義數據類型。為了添加這種類型的支持,你可以提供一個把自定義類轉化為一個Room能夠持久化的已知類型的TypeConverter。
例如:如果我們想持久化日期的實例,我們可以寫如下TypeConverter去存儲相等的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();
}
}
之前的例子定義了兩個函數,一個把Date對象轉換為Long對象。另一個逆向轉換,從Long到Date。因為Room已經知道了如何持久化Long對象,它能使用轉換器持久化Date類型。
接著,你增加@TypeConverters注解到AppDatabase類為了Room能夠使用你已經為每個entity定義的轉換器和DAO
AppDatabase.java
@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
使用這些轉換器,你可以使用你自定義類型在其他查詢中,就像你使用的原始類型,如下代碼片段所示:
User.java
@Entity
public class User {
...
private Date birthday;
}
UserDao.java
@Dao
public interface UserDao {
...
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}
您還可以將@typeconverter限制在不同的范圍內,包含單獨的entities,DAOs,和DAO methods。更多細節,請參考 @TypeConverters
文檔
Database migration
當你添加或改變你app的特性,你需要修改你的entity類去反映這些改變。當一個用戶更新你應用到最近的版本,你不希望他們丟失已經存在的數據,特別是你無法從遠程服務器恢復數據。
Room允許你使用Migration類保留用戶數據以這種方式。每個Migration類在運行時指明一個開始版本和一個結束版本,Room執行每個Migration類的migrate()方法,使用正確的順序去遷移數據庫到一個最近版本。
注意:如果你不提供必需的migrations類,Room重建數據庫,也就意味你將丟失數據庫中的所有數據。
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發現問題,它將拋出不匹配異常。
Testing migrations
遷移并不是一件簡單的事情,如果不能正確編寫將會造成應用崩潰。為了保證你應用的穩定性,你應該在提交前測試你的遷移類。Room提供一個測試Maven組件去協助測試過程。然而,為了讓這個組件工作,你需要到處你的數據庫schema。
Exporting schemas
根據編譯,Room導出你的數據庫Schema到一個JSON文件中。為了導出schema,設置 注釋處理器的屬性room.schemaLocation在你的build.gradle文件中,如下所示:
build.gradle
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
}
你應該存儲導出的JSON文件-代表了你數據庫schema的歷史-在你的版本控制系統中,正如它允許創建老版本的數據庫去測試。
為了測試這些migrations,添加 android.arch.persistence.room:testing Maven artifac從Room當中到你的測試依賴當中,并且把schema 位置當做一個asset文件添加,如下所示:
build.gradle
android {
...
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
測試package提供一個 可以讀取這些schema文件的MigrationTestHelper類。它也是Junit4 TestRule類,所以它能管理創建的數據庫。
如下代碼展示了一個測試migration的例子:
@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.
}
}
Testing your database
當運行你app的測試時,你不應該創建一個完全的數據庫如果你不測試數據庫本身。Room允許你輕松的模仿數據訪問層在測試當中。這個過程是可能的因為你的DAOs不暴漏任何你數據庫的細節。當測試你的應用,你應該創建模仿你的DAO類的假的實例。
這兒有兩種方式去測試你的數據庫:
- 在你的開發主機上
- 在一個Android設備上
Testing on your host machine
Room使用SQLite支持庫,這個支持庫提供匹配這些Android Framework類的接口并且允許你通過自定義支持庫實現去測試你的數據庫查詢。
即使這個裝置允許你的測試運行很快,它是不建議的因為用戶設備的SQLite版本和可能與host主機不匹配。
Testing on an Android device
測試你的數據庫推薦的方法實現是寫一個單元測試在Android設備上。因為這些測試不需要創建一個activity,他講bicentennialUI單元測試快。
當裝置你的測試用例時,你應該創建一個數據庫的內存版本好讓你的測試更密閉,如下所示:
@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));
}
}
更多關于測試數據庫migrations的信息參看 Migration Testing