Android架構組件—Room

概述

android系統中的數據庫SQLite使用起來並不方便,早期學習的時候一直很討厭使用,後來出現了GreenDao、OrmLite、Realm極大的方便了android開發中的數據持久化。去年google推出了架構組件,其中room就是一款orm框架。

添加Room依賴庫

詳細查看room配置

1.添加google的maven庫,在project的gradle文件:

allprojects {
    repositories {
        jcenter()
        google() // 添加谷歌maven庫
    }
}

2.添加架構組件依賴庫,在module的gradle文件:

dependencies {
    // Room (use 1.1.0-alpha2 for latest alpha)
    implementation "android.arch.persistence.room:runtime:1.0.0"
    annotationProcessor "android.arch.persistence.room:compiler:1.0.0"

    // Test helpers for Room
    testImplementation "android.arch.persistence.room:testing:1.0.0"
}

1.以上爲gradle插件3.0
2.如果是kotlin項目,確保用kapt代替annotationProcessor,同時也要添加kotlin-kapt插件

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'

3.爲room添加rxjava支持庫

dependencies {
    // RxJava support for Room (use 1.1.0-alpha1 for latest alpha)
    implementation "android.arch.persistence.room:rxjava2:1.0.0"
}

4.Room @Dao查詢中添加對Guava的Optional和ListenableFuture類型的支持。

dependencies {
    // Guava support for Room
    implementation "android.arch.persistence.room:guava:1.1.0-alpha2"
}

5.和LiveData一起使用

 // ReactiveStreams support for LiveData
 implementation "android.arch.lifecycle:reactivestreams:1.0.0"

Room三大組件

Room中有三個主要組件。

  • Database: 用這個組件創建一個數據庫。註解定義了一系列entities,並且類中提供一系列Dao的抽象方法,也是下層主要連接的訪問點。註解的類應該是一個繼承 RoomDatabase的抽象類。在運行時,你能通過調用Room.databaseBuilder()或者 Room.inMemoryDatabaseBuilder()獲得一個實例
  • Entity: 用這個組件創建表,Database類中的entities數組通過引用這些entity類創建數據庫表。每個entity中的字段都會被持久化到數據庫中,除非用@Ignore註解
  • DAO: 這個組件代表了一個用來操作表增刪改查的dao。Dao 是Room中的主要組件,負責定義訪問數據庫的方法。被註解@Database的類必須包含一個沒有參數的且返回註解爲@Dao的類的抽象方法。在編譯時,Room創建一個這個類的實現。

    Entity類能夠有一個空的構造函數(如果dao類能夠訪問每個持久化的字段)或者一個參數帶有匹配entity中的字段的類型和名稱的構造函數

    如下代碼片段包含一個簡單的三大組件使用例子:
    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();

生成數據庫實例的具體操作:

Room.databaseBuilder(getApplicationContext(),
                        RoomDemoDatabase.class, "database_name")
                        .addCallback(new RoomDatabase.Callback() {
                            //第一次創建數據庫時調用,但是在創建所有表之後調用的
                            @Override
                            public void onCreate(@NonNull SupportSQLiteDatabase db) {
                                super.onCreate(db);
                            }

                            //當數據庫被打開時調用
                            @Override
                            public void onOpen(@NonNull SupportSQLiteDatabase db) {
                                super.onOpen(db);
                            }
                        })
                        .allowMainThreadQueries()//允許在主線程查詢數據
                        .addMigrations()//遷移數據庫使用
                        .fallbackToDestructiveMigration()//遷移數據庫如果發生錯誤,將會重新創建數據庫,而不是發生崩潰
                        .build();

注意:初始化AppDatabase對象時必須遵守單例模式。因爲每個RoomDatabase實例是相當昂貴的,並且幾乎不需要訪問多個實例。

Entity相關

當一個類被註解爲@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;
}

Entity的字段必須爲public或提供setter或者getter。

Primary Key(主鍵)

每個entity必須定義至少一個字段作爲主鍵。即使這裏只有一個字段,仍然需要使用@PrimaryKey註解這個字段。並且,如果想Room動態給entity分配自增主鍵,可以設置@PrimaryKey的autoGenerate屬性爲true。如果entity有個組合的主鍵,你可以使用@Entity註解的primaryKeys屬性,正如如下片段展示的那樣:

@Entity
class User {
    public String firstName;
    public String lastName;

    @Ignore
    Bitmap picture;
    // 自增主鍵
    @PrimaryKey(autoGenerate = true)
    public int id;
}

// 組合主鍵
@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(索引和唯一性)

public @interface Index {
  //定義需要添加索引的字段
  String[] value();
  //定義索引的名稱
  String name() default "";
  //true-設置唯一鍵,標識value數組中的索引字段必須是唯一的,不可重複
  boolean unique() default false;
}

數據庫索引可以加速數據庫查詢,@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;
}

有時,表中的某個字段或字段組合需要確保唯一性,可以設置@Entity的@Index註解的unique屬性爲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庫支持entity對象引用其他的。Room明確的禁止這樣。更多細節請參考Understand why Room doesn’t allow object references

public @interface ForeignKey {
  //引用外鍵的表的實體
  Class entity();
  //要引用的外鍵列
  String[] parentColumns();
  //要關聯的列
  String[] childColumns();
  //當父類實體(關聯的外鍵表)從數據庫中刪除時執行的操作
  @Action int onDelete() default NO_ACTION;
  //當父類實體(關聯的外鍵表)更新時執行的操作
  @Action int onUpdate() default NO_ACTION;
  //在事務完成之前,是否應該推遲外鍵約束
  boolean deferred() default false;
  //給onDelete,onUpdate定義的操作
  int NO_ACTION = 1;
  int RESTRICT = 2;
  int SET_NULL = 3;
  int SET_DEFAULT = 4;
  int CASCADE = 5;
  @IntDef({NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT, CASCADE})
  @interface Action {
    }
}

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

1.外鍵是十分強大的,因爲它們允許你指明當引用的entity被更新後做什麼。例如,如果相應的user實例被刪除了,你可以通過包含@ForeignKey註解的onDelete=CASCADE屬性讓SQLite爲這個user刪除所有的書籍。
2.SQLite處理@Insert(OnConflict=REPLACE) 作爲一個REMOVE和REPLACE操作而不是單獨的UPDATE操作。這個替換衝突值的方法能夠影響你的外鍵約束。

Nested objects
有時,希望entity中包含一個具有多個字段的對象作爲字段。在這種情況下,可以使用@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。

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

如果一個實體具有相同類型的多個內嵌字段,則可以通過設置前綴屬性(prefix)使每個列保持惟一。然後將所提供的值添加到嵌入對象中每個列名的開頭

 @Embedded(prefix = "foo_")
 Coordinates coordinates;

Data Access Objects (DAOs)相關

Room的三大組件之一Dao(interface),以一種乾淨的方式去訪問數據庫。

注意: Room不允許在主線程中訪問數據庫。除非在建造器中調用allowMainThreadQueries(),可能會造成長時間的鎖住UI。異步查詢(返回LiveData或者RxJava流的查詢)是從這個規則中豁免,因爲它們異步的在後臺線程中進行查詢。

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

Update
@Update 是更新一系列entities集合、給定參數的慣例方法。它使用query來匹配每個entity的主鍵。如下代碼說明如何定義這個方法:

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

儘管通常不是必須的,你能夠擁有這個方法返回int值指示數據庫中更新的數量。

@Insert, @Update都可以執行事務操作,定義在OnConflictStrategy註解類中

public @interface OnConflictStrategy {
    //策略衝突就替換舊數據
    int REPLACE = 1;
    //策略衝突就回滾事務
    int ROLLBACK = 2;
    //策略衝突就退出事務
    int ABORT = 3;
    //策略衝突就使事務失敗 
    int FAIL = 4;
    //忽略衝突
    int IGNORE = 5;
}

Delete
@Delete是一個從數據庫中刪除一系列給定參數的entities的慣例方法。它使用主鍵找到要刪除的entities。如下所示:

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

儘管通常不是必須的,你能夠擁有這個方法返回int值指示數據庫中刪除的數量。

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第一個和最後一個名稱,而不是所有關於用戶的細節。通過獲取展示在UI的有效數據列可以使查詢完成的更快。
只要查詢中列結果集能夠被映射到返回的對象中,Room允許你返回任何java對象。例如:
創建如下POJO通過拿取用戶的姓和名。

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理解查詢返回first_name和last_name的列值被映射到NameTuple類中。因此,Room能夠生成合適的代碼。如果查詢返回太多columns,或者一個列不存在,Room將會報警。

Passing a collection of arguments
部分查詢可能需要傳入可變數量的參數,確切數量的參數直到運行時才知道。例如,想提取來自某個地區所有用戶的信息。如下:

@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
使用返回值類型爲LiveData實現數據庫更新時ui數據自動更新。

@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 Publisher和Flowable對象,需添加android.arch.persistence.room: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允許你寫任何查詢,所以你也能連接表格。

如下代碼段展示如何執行一個根據借書人姓名模糊查詢借的書的相關信息。

@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,例如,可以寫一個POJO去裝載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類
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限制在不同的範圍內,包含單獨的entity,Dao和Dao中的 methods,具體查看官方文檔。

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

輸出模式
在編譯時,將數據庫的模式信息導出到JSON文件中,這樣可有利於我們更好的調試和排錯(DataBase的exportSchema = true)

module中的build.gradle

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

數據庫遷移以及修改需要經過測試才能放心更新,具體測試方法請參考官方文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章