天天看點

Android Arch Comp - Room Persistence LibraryRoom Persistence Library

Room Persistence Library

Room資料持久化庫

Room在SQLite之上提供了一個抽象層,能夠非常友善的接入資料庫和使用SQLite的全部功能。

注:如何在項目中引入Room請參考adding components to your project.

應用可以很友善地通過本地持久化的資料加載少量的結構化資料。最常見的使用場景就是緩存使用者目前互動界面的相關資料。這樣,當移動裝置無法通路網絡時,使用者仍然可以檢視離線後的界面和資料,提高了使用者體驗。任何使用者在離線後的資料更改都會在網絡重新連接配接時和伺服器進行同步。

Android核心架構中提供了在建構時對原始的SQL語句支援。這些API的功能相當強大,但是相對比較低級,需要花大量的時間和精力去學習和使用。

  • 未在編譯時對SQL腳步語句進行校驗。如果你的資料結構發生變化,你需要手動去修改SQL腳步語句。這個過程很難免會發生錯誤,且很不友善,除非你是一個很有經驗的老手。
  • 你需要使用很多模版代碼來轉換SQL查詢和Java資料結構

Room所提供的抽象層中為你考慮到了以上兩點,并完美的解決了。

以下是Room三個主要的元件:

  • Database :你可以使用這個元件來建立資料庫持有者。這個注解定義了一系列的實體,并且這個類中還定義了一系列資料庫接入對象。這也是資料庫基礎連接配接的入口。

    被注解的類必須是一個抽象類并繼承RoomDatabase。在運作時,可以通過調用Room.databseBuilder()和Room.inMemoryDatabaseBuilder()兩個API擷取資料庫執行個體。

  • Entity : 這個元件代表了資料庫中的一條記錄即表中的一行。對于每個Entity,建立資料庫表來持有這些Entity的具體資料。必須通過資料庫類中的實體數組引用實體類。Entity的每個字段都儲存在資料庫中,除非用@注釋來忽略它。

    注:Entity可以有一個空構造函數(如果DAO類可以通路每個持久化字段),或者構造函數的參數包含與實體中的字段比對的類型和名稱。Room可以使用全部或部分構造函數,例如隻接收一些字段的構造函數。

  • DAO :該元件表示類或接口作為資料通路對象(DAO)。DAO是Room的主要成分,是負責定義通路資料庫的方法(增删查改)。使用@Database注解的類必須包含一個抽象方法,該方法有0個參數,并傳回用@Dao注解的類。當在編譯時生成代碼時,Room會為注解的類建立這個類的一個執行個體。

    注:通過使用DAO而不是查詢建構器或直接查詢通路資料庫,可以分離資料庫架構中的不同元件。此外,DAO可以讓你輕松模拟資料庫通路以此來測試應用程式。

下圖展示類Room的這些元件和應用其他部分的互相關系:

Android Arch Comp - Room Persistence LibraryRoom Persistence Library

接下來的代碼片段将包含一個簡單的資料庫,這個資料庫配置了一個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 ")
    User findByName(String first, String last);

    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);
}
           

AppDatabase.java

@Database(entities = {User.class}, version = )
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中的每個成員變量建立資料表中的一列。如果有哪些成員變量的資料是不需要進行持久化的,可以使用@Ignore進行注釋,具體代碼如下:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
}
           

為了持久化一個成員變量,必須保證Room可以通路它,是以這個成員變量必須聲明為public,或者為這個成員變量建立set/get方法,保證能擷取到,Room是基于Java Bean協定的。

Primary key

主鍵

每個Entity必須至少定義一個成員變量為主鍵,即便隻有一個成員變量也要打上@PrimaryKey注解。另外,如果你想讓Room自動為你生成主鍵,可以通過設定@PrimaryKey的autoGenerate屬性。如果這個Entity中需要多個主鍵,可以通過@Entity注解來設定primary屬性,代碼如下:

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

    @Ignore
    Bitmap picture;
}
           

預設情況下,Room會使用類名來作為表名。如果想要自定義表名,可以通過@Entity的tableName屬性來設定。

@Entity(tableName = "users")
class User {
    ...
}
           

注:在SQLite中表名是大小寫不敏感的。

和表名設定一樣,Room也支援自定義列名,通過@ColumnInfo的name屬性來自定義列名。

@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(value = {"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;
}
           

有時候,某些成員變量或資料庫中的字段組中必須是唯一的。通過将索引注釋的唯一屬性設定為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;
}
           

Relationships

關聯

因為SQLite是一個關系型資料庫,你可以指定對象之間的關聯。盡管大多數ORM庫允許實體對象互相引用,但Room明确禁止這一點。有關詳細資訊,請檢視Addendum: No object references between entities。

盡管您不能進行直接關聯,但仍然可以通過在實體之間定義外鍵限制。

例如,如果有一個實體Book,可以使用@ForeignKey注釋定義它的關系到User實體,具體代碼如下:

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

外鍵非常強大,因為它們允許您指定引用實體更新時發生的情況。例如,你可以告訴SQLite删除指定使用者的所有書籍,當該使用者被删除後。前提是在@ForeignKey注解中使用OnDelete = CASCADE屬性。

注:SQLite通過@Insert(OnConflict=REPLACE) 來處理Remove, Replace操作,而不僅僅是update操作。這種方法來修改沖突的值會影響相關聯的外鍵。詳情見on_conflict 在SQLite documentation的條款。

Nested objects

對象嵌套

有時,你想表達一個實體或普通的java對象(POJOs)作為你的資料庫邏輯連貫的整體,即使對象包含多個字段。在這種情況下,你可以使用“@Embedded來表示一個對象,你想分解成它的子域内的表。然後您可以像其他單獨的列一樣查詢嵌入式字段。

例如,User可以包含一個Address的字段,它代表一個字段命名的街道組成的城市,省,郵編。要将組合的列分别存儲在表中,請在User中包含與 @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, street, state, city, 和post_code

注:嵌入式字段還可以包含其他嵌入字段。

如果一個Entity具有相同類型的多個嵌入字段,則可以通過設定字首屬性來保持每個列的唯一性。Room将會為每個嵌入的對象指派。

Data Access Objects (DAOs)

資料接入對象

DAO是Room中的主要元件。DAO通過一種簡潔的方式抽象通路資料庫。

Dao可以是一個接口也可以是一個抽象類。如果是一個抽象類,可以有一個以RoomDatabse作為參數的構造函數。

注:Room不允許在主線程中通路資料庫,除非在builder中調用allowmainthreadqueries()接口,因為它可能會導緻UI界面長時間無響應。異步查詢(傳回LiveData或RxJava Flowable)将不受這個規則限制。

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方法隻接收一個參數,可以傳回一個long值,代表所插入的具體的哪一行的id,如果參數是一個數組或集合,将會傳回long[]或List.

有關更多關于@Insert注解的相關資訊,請參考 SQLite documentation for rowid tables

Update

Update是更新資料庫很友善的一個方法,通過主鍵來查找要更新的資料,然後進行更新,具體代碼如下:

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

在需要的适合也可以傳回具體更新的id作為傳回值傳回。

Delete

和Update類似,這裡就不進行過多的叙述。

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

Methods using @Query

使用@Query注解的方法

@Query是Dao中主要的一個注解。它允許對資料庫進行讀寫操作。每個@Query注解的方法都會在編譯的時候進行校驗,如果腳本有錯,就不會在運作時才抛出異常。

Room還驗證查詢的傳回值,以便如果傳回對象中字段的名稱與查詢響應中的相應列名不比對,Room将會通過以下兩種方式進行警告:

  • 當隻有部分列比對時會發出警告
  • 如果沒有列比對時會發出錯誤提示

簡單查詢

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

這是一個非常簡單的查詢,查詢所有使用者。在編譯時,Room知道将要查詢使用者表中的所有列的資料。如果查詢包含文法錯誤,或者如果資料庫中不存在使用者表,那麼當應用程式編譯時,Room将會在編譯時顯示相應的錯誤資訊。

帶參數查詢

大多數情況下,會使用條件查詢,例如隻顯示大于某個年齡的使用者。要完成此任務,需要在Room注釋中使用方法參數,代碼如下:

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

當在編譯的時候處理這個查詢時,Room将: minAge和方法參數minAge進行比對和綁定。Room使用名稱進行比對。如果不比對,将會在應用編譯時報錯。

當然也支援多條件查詢:

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

傳回對應列的子集

大多數情況,你隻需要Entity中的部分字段資料。例如,界面上隻需要顯示使用者的姓名,而不是使用者的所有資訊。通過隻提取應用程式UI中需要的列,可以節省寶貴的資源,并且更快的完成查詢。

Room允許在查詢的結果中和傳回對象進行映射。例如,

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可以将查詢資料中的firstname, lastname和傳回對象中的資料進行映射。是以,Room會生成對應的代碼。如果查詢傳回的資料列過多,或者沒有比對到,會提示警告。

多個資料查詢

一些查詢可能要求傳遞一個可變數量的參數,參數的确切數量直到運作時才知道。例如,可能想找回指定地區的所有使用者資訊。Room會自動解析集合,并進行查詢。

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

可觀察的查詢

在查詢時,可能會希望UI界面會跟随資料的變化而自動更新。要達到這個目的,必須使用LiveData作為查詢參數進行傳回。Room會自動生成代碼,當資料庫資料發生變化時會自動更新LiveData

RxJava

Room查詢還可以傳回RxJava2 Publisher和Flowable對象。如果要使用這個傳回對象,需要添加依賴的庫。

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

更多詳情可以檢視谷歌開發者中的文章:Room and RxJava

傳回遊标

如果想要在查詢中直接傳回遊标來通路對應的資料對象,代碼如下:

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

注:非常不推薦使用這種方式傳回資料,它不能保證通路的行資料是否存在,或者目前行資料包含哪些列資料。而且不能對使用遊标的查詢進行重構。

多表查詢

有些資料查詢會涉及多個表,Room支援各種查詢,比如多表聯合查詢。此外,如果響應是一個可觀察的資料類型,比如LiveData或Flowable,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);
}
           
@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支援資料類型的自動裝箱和拆箱。但是,有時使用一種自定義的資料類型,并希望在資料庫中一個列中存儲對應的值。使用TypeConvert可以提供自定義資料結構的存儲,它會将自定義類型轉化成目前支援的資料類型進行持久化。

比如說,如果要持久化Date對象,就可以使用TypeConvert進行時間戳資料轉換:

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就可以将Date對象持久化成已經支援的long資料

然後就是使用@TypeConvert注解對資料庫進行注釋:

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

有了這個類型轉換,就可以在自定義類型中直接使用Date對象:

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

當然也可以限制@TypeConvert注解的作用範圍,比如單獨的Entity, Dao, 或具體的方法。更多詳細可參考: @TypeConverters

Database migration

資料庫遷移或更新

當在應用程式中添加和更改功能時,需要修改實體類來反映這些更改。當使用者更新到應用程式的最新版本時,您不希望它們丢失所有現有資料,尤其是如果不能從遠端伺服器恢複資料。

Room允許你寫更新類儲存這樣的使用者資料。每個更新類指定Startversion和endversion。在運作時,Room執行每個更新類的migrate()方法,使用正确的順序将資料庫遷移到新版本。

注:如果沒有提供遷移類,Room會重新建立資料庫,這也就意味着将會出現資料丢失。

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

static final Migration MIGRATION_1_2 = new Migration(, ) {
    @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(, ) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Book "
                + " ADD COLUMN pub_year INTEGER");
    }
};
           

注:為了保持遷移邏輯的正常運作,使用完整查詢,而不是引用表示查詢的常量。

在遷移過程完成後,Room将驗證資料庫,以確定正确遷移資料庫。如果Room發現問題,它會抛出一個包含不比對資訊的異常。

具體的測試:// TODO