【翻譯】安卓架構(gòu)組件(6)-Room持久化類庫

相關(guān)文章:

Room為SQLite提供了一個抽象層,使得可以流暢使用SQLite的所有功能淮野。

處理大量結(jié)構(gòu)化數(shù)據(jù)的app可以從本地數(shù)據(jù)持久化中獲取巨大利益蠢沿。最常見的用例是緩存相關(guān)的數(shù)據(jù)。在這種情況下刚陡,當(dāng)設(shè)備無法訪問網(wǎng)絡(luò)的時候,用戶仍然可以在離線時瀏覽內(nèi)容株汉。任何用戶原始數(shù)據(jù)的變化都會在連接網(wǎng)絡(luò)后同步筐乳。

核心框架提供了原生SQL的支持。盡管這些API很強大乔妈,但是比較底層并且需要花費大量的時間和努力去使用:

  • 沒有原生SQL查詢語句的編譯時驗證蝙云。當(dāng)你的數(shù)據(jù)結(jié)構(gòu)變化時,你需要手動更新受影響的SQL褒翰。這個過程會花費大量的時間并且很容易錯誤頻出贮懈。

Room考慮到了這些匀泊,提供了SQLite的抽象層优训。

Room有三個主要的組件:

  • 數(shù)據(jù)庫(Database):你可以使用該組件創(chuàng)建數(shù)據(jù)庫的持有者。該注解定義了實體列表各聘,該類的內(nèi)容定義了數(shù)據(jù)庫中的DAO列表揣非。這也是訪問底層連接的主要入口點。注解類應(yīng)該是抽象的并且擴展自RoomDatabase躲因。在運行時早敬,你可以通過調(diào)用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()獲取實例。
  • 實體(Entity):這個組件代表了持有數(shù)據(jù)庫表記錄的類大脉。對每種實體來說搞监,創(chuàng)建了一個數(shù)據(jù)庫表來持有所有項。你必須通過Database中的entities數(shù)組來引用實體類镰矿。實體的每個成員變量都被持久化在數(shù)據(jù)庫中琐驴,除非你注解其為@Ignore

實體類可以擁有無參數(shù)構(gòu)造函數(shù)(如果DAO類可以訪問每個持久化成員變量)或者擁有和實體類成員變量匹配參數(shù)的構(gòu)造函數(shù)。Room也可以使用全部或者部分構(gòu)造函數(shù)绝淡,例如只接收部分成員變量的構(gòu)造函數(shù)宙刘。

  • 數(shù)據(jù)訪問對象(DAO):這個組件代表了作為DAO的類或者接口。DAO是Room的主要組件牢酵,負(fù)責(zé)定義訪問數(shù)據(jù)庫的方法悬包。被注解@Database的類必須包含一個無參數(shù)的抽象方法并返回被@Dao注解的類型。當(dāng)編譯時生成代碼時馍乙,Room會創(chuàng)建該類的實現(xiàn)布近。

通過使用DAO類訪問數(shù)據(jù)庫而不是查詢構(gòu)建器或直接查詢,你可以將數(shù)據(jù)庫架構(gòu)的不同組件分離丝格。此外吊输,DAO允許你在測試時很容易地模擬數(shù)據(jù)訪問。

這些組件和app的其他部分關(guān)系圖如下:

image

(img)

下面的代碼片段包含了簡單的數(shù)據(jù)庫配置铁追,含有1個實體和一個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 Setters(實際代碼中不可省略)
   }

//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();
}

創(chuàng)建上面的文件以后季蚂,你可以使用以下代碼獲取已創(chuàng)建數(shù)據(jù)庫實例:

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

當(dāng)實例化AppDatabase對象時,你可以遵循單例設(shè)計模式琅束,因為每個RoomDatabase實例代價是非常昂貴的扭屁,并且你幾乎不需要訪問多個實例。

實體

當(dāng)一個類被@Entity注解涩禀,并被@Database注解的entities屬性引用時料滥,Room為這個實體在數(shù)據(jù)庫中創(chuàng)建一個表。

默認(rèn)情況艾船,Room為實體類的每個成員變量創(chuàng)建一個列葵腹。如果一個實體類的某個成員變量不想被持久化,你可以使用Ignore注解標(biāo)記屿岂,如:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;//不進行持久化
}

為了持久化成員變量践宴,Room必須可以訪問它。你可以使成員變量是公共的爷怀,或者提供getter和setter方法阻肩。如果你使用getter/setter方法,請記住它們在Room中遵循Java Beans的概念运授。

主鍵

每個實體必須至少定義一個成員變量作為主鍵烤惊。甚至僅僅有一個成員變量,也要標(biāo)記其為@PrimaryKey吁朦。同時柒室,如果你想要Room指定ID自增,你可以設(shè)置@PrimaryautoGenerate屬性逗宜。如果實體的主鍵是綜合的雄右,你可以使用@EntityprimaryKeys屬性剥啤,如:

@Entity(primaryKeys = {"firstName", "lastName"})
class User {
    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}

默認(rèn)情況下,Room使用類名作為數(shù)據(jù)庫表的表名不脯。如果你想要數(shù)據(jù)庫表有一個其他的名字府怯,設(shè)置@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;
}

索引與唯一

取決于你如何訪問數(shù)據(jù)复局,你可能想要索引確切的字段以加速數(shù)據(jù)的查詢冲簿。為了向?qū)嶓w添加索引,在@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;
}

有些時候具體的成員或成員組必須是獨一無二的峦剔。你可以設(shè)置@Index的屬性uniquetrue

@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;
}

關(guān)系

因為SQLite是一個關(guān)系型數(shù)據(jù)庫,你可以指定對象間的關(guān)系角钩。即使大多數(shù)ORM類庫允許實體對象互相引用吝沫,Room則顯式地禁止了這一點。

即使你不能直接使用關(guān)系映射递礼,Room仍然允許你去定義實體鍵的外鍵約束惨险。

例如,有另一個叫做Book的實體脊髓,你可以通過使用@ForeignKey注解定義其和User實體的關(guān)系辫愉,如:

@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;
}

外鍵是非常強大的,因為它們允許你指定引用實體更新時會發(fā)生什么将硝、例如恭朗,你可以告訴SQLite去刪除所有的書籍,如果這些書所對應(yīng)的User被刪除并且指定了@ForeignKey的屬性onDelete=CASCADE依疼。

SQLite將@Insert(OnConfilct=REPLACE)處理為REMOVEREPLACE的集合而不僅僅是更新操作痰腮。這個替換沖突值的方法可能會對你的外鍵約束起作用。

內(nèi)嵌對象

有些時候你想要一個實體類或POJO類作為數(shù)據(jù)庫邏輯的一部分涛贯。這種情況下诽嘉,你可以使用@Embedded注解來蔚出。你可以查詢內(nèi)嵌成員弟翘,就像你可能查詢其他字段一樣。

例如骄酗,我們的User類包含一個Address類型的成員稀余,代表了streetcity趋翻、statepostCode睛琳。為了分別存儲這些字段,在User類中包含一個Address成員并標(biāo)記為@Embedded,如:

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;
}

這個表代表了一個User對象包含了以下字段:id师骗,firstName历等,streetstate辟癌,citypost_code寒屯。

內(nèi)嵌成員也可以含有其他內(nèi)嵌成員

如果一個實體含有多種相同類型的內(nèi)嵌成員,你可以通過設(shè)置prefix屬性保持每個字段的唯一性黍少。Room之后添加提供的值到每個內(nèi)嵌對象的起始位置寡夹。

數(shù)據(jù)訪問對象(DAO)

Room的主要組件是Dao類。DAO以清晰的方式抽象除了訪問數(shù)據(jù)庫的行為厂置。

Room不允許在主線程方位數(shù)據(jù)庫菩掏,除非你在Builder調(diào)用allowMainThreadQueries(),因為這可能會導(dǎo)致UI被鎖住昵济。而異步查詢則不受此約束智绸,因為異步調(diào)用在后臺線程運行查詢工作。

便捷方法

有很多可以使用DAO類的便捷查詢方法访忿,例如:

Insert

當(dāng)你創(chuàng)建一個DAO方法并標(biāo)記其為@Insert传于,Room會生成在單一事務(wù)中將所有參數(shù)存入數(shù)據(jù)庫的實現(xiàn):

@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方法只接收一個參數(shù),它會返回long醉顽,表示新插入項的row Id沼溜。如果參數(shù)是數(shù)組或集合,它會返回long[]或者List<Long>游添。

Update

Update是更新一組實體的便捷方法系草。它查詢匹配主鍵的記錄然后更新。如:

@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}

盡管通常并不需要如此唆涝,你可以讓該方法返回一個int值找都,表示更新至數(shù)據(jù)庫的行號。

Delete

Delete是刪除一組實體的便捷方法廊酣。它使用主鍵去尋找記錄并刪除:

@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

同上能耻,你可以讓該方法返回一個int值表示被刪除的行號。

使用@Query

@Query是用于DAO類的主要注解亡驰。它允許你在數(shù)據(jù)庫上執(zhí)行讀寫操作晓猛。每個Query方法都會在編譯時驗證,因此如果查詢語句有問題凡辱,那么編譯時就會報錯戒职,而不是在運行時發(fā)生。

Room同樣驗證查詢的返回值透乾,如果返回對象的成員名和字段名不一致洪燥,Room會以以下兩種方式警告:

  • 如果僅僅部分成員名相符磕秤,則發(fā)出警告
  • 如果沒有成員名相符,則發(fā)出錯誤

簡單查詢

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

這是一個加載所有用戶的簡單查詢捧韵。在編譯時市咆,Room知道這是查詢用戶表的所有字段。如果查詢語句含有語法錯誤再来,或者用戶表在數(shù)據(jù)庫中并不存在床绪,Room會顯示相應(yīng)的錯誤。

查詢中傳遞參數(shù)

大多數(shù)時候其弊,你需要在查詢中傳遞參數(shù)來執(zhí)行過濾操作癞己,例如僅僅顯示具體年齡的用戶。為了完成這個任務(wù)梭伐,在你的Room注解中使用方法參數(shù)痹雅,如:

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

當(dāng)編譯時處理這個查詢時,糊识,Room將:minAgeminAge匹配在一起绩社。Room使用參數(shù)名進行匹配,如果匹配不成功赂苗,會在編譯時報錯愉耙。

你也可以傳遞多個參數(shù)或引用多次,如:

@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);
}

返回所有字段的子集

大多數(shù)時候拌滋,你可能需要一個實體的一部分成員變量朴沿,例如你的UI可能只顯示用戶的名和姓,而不是用戶的所有細(xì)節(jié)败砂。通過僅僅獲取出現(xiàn)在你UI中的字段赌渣,你可以存儲很多資源,并且你的查詢完成地更快昌犹。

Room允許你從查詢中返回任何對象坚芜,只要結(jié)果字段集可以被映射到返回的對象上。例如斜姥,你可以創(chuàng)建下面的POJO類來獲取用戶的姓和名:

public class NameTuple {
    @ColumnInfo(name="first_name")
    public String firstName;

    @ColumnInfo(name="last_name")
    public String lastName;
}

現(xiàn)在你可以在你的查詢方法中這樣使用POJO類:

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

Room理解這次返回first_namelast_name字段的查詢鸿竖,并可以映射到NameTuple類。這樣铸敏,Room就能生成正確的代碼缚忧。如果查詢返回太多的字段,或者某個字段并不存在于NameTuple中搞坝,Room會顯示一個警告搔谴。

傳遞參數(shù)集合

你的某些查詢可能會傳遞大量的參數(shù),而且直到運行時才知道具體的參數(shù)桩撮。例如敦第,你可能會獲取關(guān)于用戶所屬區(qū)域的信息。當(dāng)參數(shù)為集合時店量,Room能夠理解并自動根據(jù)當(dāng)前提供的參數(shù)進行擴展:

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

可觀察查詢

當(dāng)運行查詢時芜果,你通常想要在數(shù)據(jù)變化的時候你的app界面自動更新。為了做到這一點融师,在查詢方法中使用LiveData類型的返回值右钾。Room會生成所有必要的代碼,當(dāng)數(shù)據(jù)更新時旱爆,會自動更新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);
}

RxJava

Room也可以從你定義的查詢中直接返回RxJava2的PublisherFlowable對象。為了使用這個功能怀伦,添加android.arch.persistence.room:rxjava2到你的Gradle構(gòu)建依賴脆烟。你可以隨后返回RxJava2定義的類型,如:

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

查詢多張表

你的一些查詢可能需要訪問多張表來計算結(jié)果房待。Room允許你寫任何的查詢邢羔,因此你可以使用連接表。另外桑孩,如果結(jié)果是可觀察數(shù)據(jù)類型拜鹤,例如Flowable或者LiveData,Room會驗證所有SQL查詢語句流椒。

下面的代碼片段展示了如何連接兩張表敏簿,一張表是包含用戶借書的信息,另一張包含當(dāng)前借出的信息:

@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();

   // 你也可以在單獨的文件中定義該類极谊,只要你添加了public修飾符
     static class UserPet {
       public String userName;
       public String petName;
   }
}

使用類型轉(zhuǎn)換

Room提供內(nèi)置工具用于基本類型和其封裝類型的裝換。但是有些時候你可能使用了使用了自定義數(shù)據(jù)類型安岂,而想在數(shù)據(jù)庫表中始終單個字段轻猖。為了添加這類自定義類型支持,你需要提供一個TypeConverter域那,將自定義類轉(zhuǎn)換到Room已知可以持久化的類型咙边。

例如,如果我們想要持久化Date實例次员,我們可以這樣寫:

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();
    }
}

上面的例子定義了兩個函數(shù)败许,一個將Date類型轉(zhuǎn)換為Long類型,另一個進行相反的轉(zhuǎn)換淑蔚。

接下來市殷,在AppDataBase添加@TypeConverters注解,使得Room可以使用你定義的轉(zhuǎn)換器:

@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

使用了這些轉(zhuǎn)換器以后刹衫,你可以在其他查詢中使用你的自定義類型醋寝,就像基本類型一樣:

//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);
}

你可以限制@TypeConverters的作用范圍搞挣,包括單獨實體,DAO以及DAO方法音羞。

數(shù)據(jù)庫遷移

當(dāng)你在app添加以及修改功能時囱桨,你需要修改你的實體類以響應(yīng)這些變化。當(dāng)一個用戶更新到最新版本的app時嗅绰,你不想讓他們丟掉所有已經(jīng)存在的數(shù)據(jù)舍肠,特別是不能再從遠(yuǎn)程服務(wù)器獲取的數(shù)據(jù)。

Room允許你編寫Migration類來保護用戶數(shù)據(jù)窘面。每個Migration類指定一個startVersionendVersion翠语。在運行時,Room運行每個Migration類的migrate()方法财边,使用正確的順序遷移至數(shù)據(jù)庫的更新版本肌括。

如果你沒有提供必要的遷移,Room會重新構(gòu)建數(shù)據(jù)庫制圈,這意味著你將丟失所有數(shù)據(jù)庫中的數(shù)據(jù)们童。

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驗證當(dāng)前的表以確保遷移的正確性。如果Room找到問題馋嗜,會拋出未匹配的異常信息齐板。

測試遷移

遷移是很重要的事情,錯誤的編寫會導(dǎo)致你app的崩潰循環(huán)葛菇。為了保證app的穩(wěn)定性甘磨,你應(yīng)該測試你的遷移工作。Room提供了一個測試的Maven構(gòu)件來幫助測試眯停。但是济舆,為了該構(gòu)件可以工作,你需要導(dǎo)出你的數(shù)據(jù)庫表莺债。

導(dǎo)出數(shù)據(jù)庫表

//build.gradle
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}

Room會將你數(shù)據(jù)庫的表信息導(dǎo)出為一個json文件滋觉。你應(yīng)該在版本控制系統(tǒng)中保存該文件,該文件代表了你的數(shù)據(jù)庫表歷史記錄齐邦,這樣允許Room創(chuàng)建舊版本的數(shù)據(jù)庫用于測試椎侠。

為了測試遷移,添加android.arch.persistence.room:testing到你的測試依賴措拇,以及添加模式表的位置至asset文件夾我纪,如:

//build.gradle
android {
    ...
    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

測試包提供了一個MigrationTestHelper類,可以讀取這些模式表文件。

@RunWith(AndroidJUnit4.class)
public class MigrationTest {
    private static final String TEST_DB = "migration-test";

    @Rule
    public MigrationTestHelper helper;

    public MigrationTest() {
        helper = new MigrationTestHelper(InstrumentationRegistry.getContext(),
                MigrationDb.class.getCanonicalName(),
                new FrameworkSQLiteOpenHelperFactory());
    }

    @Test
    public void migrate1To2() throws IOException {
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);

        // db 版本為1. 使用SQL添加一些數(shù)據(jù)
        // 你不能使用DAO浅悉,因為它表示的是最新的數(shù)據(jù)庫
        db.execSQL(...);

        // 準(zhǔn)備下個版本
        db.close();

        // 重新打開數(shù)據(jù)庫版本2
        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);

         }
}

測試你的數(shù)據(jù)庫

當(dāng)運行測試你的app時趟据,如果沒有測試數(shù)據(jù)庫本身,你不需要創(chuàng)建全部的數(shù)據(jù)庫仇冯。Room允許在你的測試中模擬數(shù)據(jù)訪問層之宿。這個過程是可能的族操,因為你的DAO并沒有泄漏任何數(shù)據(jù)庫的細(xì)節(jié)苛坚。當(dāng)測試剩下的app部分時,你應(yīng)該創(chuàng)建模擬你的DAP類色难。

這里有兩種測試數(shù)據(jù)庫的方式:

  • 在你的開發(fā)主機上
  • 在Android設(shè)備上

在你的主機上測試

Room使用SQLite支持庫泼舱,提供了匹配安卓框架類的接口。這種支持允許你傳遞支持類庫的自定義實現(xiàn)以測試你的數(shù)據(jù)庫枷莉。

即使這種方案允許你測試非辰筷迹快捷,但是并不值得推薦笤妙,這是因為你設(shè)備上以及你用戶設(shè)備上運行的SQLite版本可能和你主機上運行的版本并不匹配冒掌。

在Android設(shè)備上測試

這種推薦的測試數(shù)據(jù)庫方法是編寫運行在安卓設(shè)備上的JUnit測試。因為這些測試并不需要創(chuàng)建Activity蹲盘,它們應(yīng)該會比在UI上測試要快股毫。

當(dāng)設(shè)置你的測試時,你應(yīng)該創(chuàng)建一個數(shù)據(jù)庫的內(nèi)存版本來使得測試更密閉召衔,如:

@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));
    }
}

附加::沒有實體鍵的對象引用

從數(shù)據(jù)庫到對象間關(guān)系的映射是一個很常見的實踐铃诬,并且在服務(wù)端運行良好,在它們被訪問的時候進行高性能的惰性加載苍凛。

但是在客戶端趣席,惰性加載并不可行,這是因為很有可能發(fā)生在主線程醇蝴,在主線程查詢磁盤信息會導(dǎo)致很嚴(yán)重的性能問題宣肚。主線程有大概16ms來計算并繪制一個Activity的界面更新,因此甚至一個查詢僅僅耗費5ms悠栓,你的app仍然會耗光繪制畫面的時間霉涨,導(dǎo)致顯著的Jank問題。更糟的是闸迷,如果有個并發(fā)運行的數(shù)據(jù)庫事務(wù)嵌纲,或者如果設(shè)備正忙于處理其他磁盤相關(guān)的繁重工作,查詢會花費更多的時間完成腥沽。如果你不使用惰性加載的方式逮走,app會獲取多余其所需要的數(shù)據(jù),從而導(dǎo)致內(nèi)存消耗的問題今阳。

ORM通常將該問題交給開發(fā)者決定师溅,使得他們可以根據(jù)自己的用例選擇最佳的方式茅信。不幸地是,開發(fā)者通常終止模型和UI之間的共享墓臭。當(dāng)UI變更超時時蘸鲸,問題隨之發(fā)生并且很難預(yù)感和解決。

舉個例子窿锉,UI界面讀取一組Book列表酌摇,每本書擁有一個Author對象。你可能開始會設(shè)計你的查詢?nèi)ナ褂枚栊约虞d嗡载,從而Book實例使用getAuthor()方法查詢數(shù)據(jù)庫窑多。過了一些時間,你意識到你需要在app的UI界面顯示作者名洼滚。你可以添加以下方法:

authorNameTextView.setText(user.getAuthor().getName());

但是這種看似沒有問題的代碼會導(dǎo)致Author表在主線程被查詢埂息。

如果你急于查詢作者信息,這會變得很難去改變數(shù)據(jù)是如何加載的遥巴,如果你不再需要這個數(shù)據(jù)的話千康,例如當(dāng)你app的UI不再需要顯示關(guān)于特定作者信息的時候。于是你的app必須繼續(xù)加載不再顯示的信息铲掐。這種方式更為糟糕拾弃,如果Author類引用了其他表,例如getBooks()方法迹炼。

由于這些原因砸彬,Room禁止實體間的對象引用。作為替換斯入,你必須顯式地請求你所需要的數(shù)據(jù)砂碉。

簡單通俗地解釋一下Jank:第2幀畫面同步信號已經(jīng)到來,由于第2幀數(shù)據(jù)還沒有準(zhǔn)備就緒刻两,顯示的還是第1幀增蹭。這種情況被Android開發(fā)組命名為“Jank”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市磅摹,隨后出現(xiàn)的幾起案子滋迈,更是在濱河造成了極大的恐慌,老刑警劉巖户誓,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饼灿,死亡現(xiàn)場離奇詭異,居然都是意外死亡帝美,警方通過查閱死者的電腦和手機碍彭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人庇忌,你說我怎么就攤上這事舞箍。” “怎么了皆疹?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵疏橄,是天一觀的道長。 經(jīng)常有香客問我略就,道長捎迫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任残制,我火速辦了婚禮立砸,結(jié)果婚禮上掖疮,老公的妹妹穿的比我還像新娘初茶。我一直安慰自己,他們只是感情好浊闪,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布恼布。 她就那樣靜靜地躺著,像睡著了一般搁宾。 火紅的嫁衣襯著肌膚如雪折汞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天盖腿,我揣著相機與錄音爽待,去河邊找鬼。 笑死翩腐,一個胖子當(dāng)著我的面吹牛鸟款,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茂卦,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼何什,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了等龙?” 一聲冷哼從身側(cè)響起处渣,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛛砰,沒想到半個月后罐栈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡泥畅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年荠诬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡浅妆,死狀恐怖望迎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凌外,我是刑警寧澤辩尊,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站康辑,受9級特大地震影響摄欲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜疮薇,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一胸墙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧按咒,春花似錦迟隅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至掠抬,卻和暖如春吼野,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背两波。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工瞳步, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腰奋。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓单起,卻偏偏與公主長得像,于是被迫代替她去往敵國和親氛堕。 傳聞我的和親對象是個殘疾皇子馏臭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容