探索Android架構(gòu)組件Room

一等限、簡(jiǎn)介

Room是Google推出的Android架構(gòu)組件庫(kù)中的數(shù)據(jù)持久化組件庫(kù), 也可以說(shuō)是在SQLite上實(shí)現(xiàn)的一套ORM解決方案峦树。Room主要包含三個(gè)部分:

  • Database : 持有DB和DAO
  • Entity : 定義POJO類(lèi)政供,即數(shù)據(jù)表結(jié)構(gòu)
  • DAO(Data Access Objects) : 定義訪(fǎng)問(wèn)數(shù)據(jù)(增刪改查)的接口

其關(guān)系如下圖所示:

Room Architecture Diagram

二掏觉、基本使用

1. 創(chuàng)建Entity

1.1 一個(gè)簡(jiǎn)單的Entitiy

一個(gè)簡(jiǎn)單Entity定義如下:

@Entity(tableName = "user" 
          indices = {@Index(value = {"first_name", "last_name"})})
public class User {

    @PrimaryKey
    private int uid;

    @ColumnInfo(name = "first_name")
    private String firstName;

    @ColumnInfo(name = "last_name")
    private String lastName;

    @Ignore
    public User(String firstName, String lastName) {
        this.uid = UUID.randomUUID().toString();
        this.firstName = firstName;
        this. lastName = lastName;
    }

    public User(String id, String firstName, String lastName) {
        this.uid = id;
        this.firstName = userName;
        this. lastName = userName;
    }
    
    // Getters and setters
}

  • @Entity(tableName = "table_name**") 注解POJO類(lèi)投剥,定義數(shù)據(jù)表名稱(chēng);
  • @PrimaryKey 定義主鍵迷帜,如果一個(gè)Entity使用的是復(fù)合主鍵输吏,可以通過(guò)@Entity注解的primaryKeys 屬性定義復(fù)合主鍵:@Entity(primaryKeys = {"firstName", "lastName"})
  • @ColumnInfo(name = “column_name”) 定義數(shù)據(jù)表中的字段名
  • @Ignore 用于告訴Room需要忽略的字段或方法
  • 建立索引:在@Entity注解的indices屬性中添加索引字段权旷。例如:indices = {@Index(value = {"first_name", "last_name"}, unique = true), ...}, unique = true可以確保表中不會(huì)出現(xiàn){"first_name", "last_name"} 相同的數(shù)據(jù)。

1.2 Entitiy間的關(guān)系

不同于目前存在的大多數(shù)ORM庫(kù)贯溅,Room不支持Entitiy對(duì)象間的直接引用拄氯。(具體原因可以參考: Understand why Room doesn't allow object references
但Room允許通過(guò)外鍵(Foreign Key)來(lái)表示Entity之間的關(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;
}

如上面代碼所示它浅,Book對(duì)象與User對(duì)象是屬于的關(guān)系译柏。Book中的user_id,對(duì)應(yīng)User中的id。 那么當(dāng)一個(gè)User對(duì)象被刪除時(shí)姐霍, 對(duì)應(yīng)的Book會(huì)發(fā)生什么呢鄙麦?

@ForeignKey注解中有兩個(gè)屬性onDeleteonUpdate, 這兩個(gè)屬性對(duì)應(yīng)ForeignKey中的onDelete()onUpdate(), 通過(guò)這兩個(gè)屬性的值來(lái)設(shè)置當(dāng)User對(duì)象被刪除/更新時(shí)镊折,Book對(duì)象作出的響應(yīng)黔衡。這兩個(gè)屬性的可選值如下:

  • CASCADE:User刪除時(shí)對(duì)應(yīng)Book一同刪除; 更新時(shí)腌乡,關(guān)聯(lián)的字段一同更新
  • NO_ACTION:User刪除時(shí)不做任何響應(yīng)
  • RESTRICT:禁止User的刪除/更新盟劫。當(dāng)User刪除或更新時(shí),Sqlite會(huì)立馬報(bào)錯(cuò)与纽。
  • SET_NULL:當(dāng)User刪除時(shí)侣签, Book中的userId會(huì)設(shè)為NULL
  • SET_DEFAULT:與SET_NULL類(lèi)似,當(dāng)User刪除時(shí)急迂,Book中的userId會(huì)設(shè)為默認(rèn)值

1.3 對(duì)象嵌套

在某些情況下影所, 對(duì)于一張表中的數(shù)據(jù)我們會(huì)用多個(gè)POJO類(lèi)來(lái)表示,在這種情況下可以用@Embedded注解嵌套的對(duì)象僚碎,比如:

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

以上代碼所產(chǎn)生的User表中猴娩,Column 為id, firstName, street, state, city, post_code

2. 創(chuàng)建數(shù)據(jù)訪(fǎng)問(wèn)對(duì)象(DAO)

@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(List<User> users);
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);

    @Delete
    void delete(User user);
    
    @Update
    public void updateUsers(List<User> users);
}

DAO 可以是一個(gè)接口,也可以是一個(gè)抽象類(lèi), Room會(huì)在編譯時(shí)創(chuàng)建DAO的實(shí)現(xiàn)卷中。

Tips:

  • @Insert方法也可以定義返回值矛双, 當(dāng)傳入?yún)?shù)僅有一個(gè)時(shí)返回long, 傳入多個(gè)時(shí)返回long[]List<Long>, Room在實(shí)現(xiàn)insert方法的實(shí)現(xiàn)時(shí)會(huì)在一個(gè)事務(wù)進(jìn)行所有參數(shù)的插入。
  • @Insert的參數(shù)存在沖突時(shí)蟆豫, 可以設(shè)置onConflict屬性的值來(lái)定義沖突的解決策略议忽, 比如代碼中定義的是@Insert(onConflict = OnConflictStrategy.REPLACE), 即發(fā)生沖突時(shí)替換原有數(shù)據(jù)
  • @Update@Delete 可以定義int類(lèi)型返回值,指更新/刪除的函數(shù)

DAO中的增刪改方法的定義都比較簡(jiǎn)單十减,這里不展開(kāi)討論栈幸,下面更多的聊一下查詢(xún)方法。

2.1 簡(jiǎn)單的查詢(xún)

Talk is cheap, 直接show code:

@Query("SELECT * FROM user")
List<User> getAll();

Room會(huì)在編譯時(shí)校驗(yàn)sql語(yǔ)句帮辟,如果@Query() 中的sql語(yǔ)句存在語(yǔ)法錯(cuò)誤速址,或者查詢(xún)的表不存在,Room會(huì)在編譯時(shí)報(bào)錯(cuò)由驹。

2.2 查詢(xún)參數(shù)傳遞

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

看代碼應(yīng)該比較好理解壳繁, 方法中傳遞參數(shù)arg, 在sql語(yǔ)句中用:arg即可。編譯時(shí)Room會(huì)匹配對(duì)應(yīng)的參數(shù)荔棉。

如果在傳參中沒(méi)有匹配到:arg對(duì)應(yīng)的參數(shù), Room會(huì)在編譯時(shí)報(bào)錯(cuò)闹炉。

2.3 查詢(xún)表中部分字段的信息

在實(shí)際某個(gè)業(yè)務(wù)場(chǎng)景中, 我們可能僅關(guān)心一個(gè)表部分字段的值润樱,這時(shí)我僅需要查詢(xún)關(guān)心的列即可局嘁。

定義子集的POJO類(lèi):

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

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

在DAO中添加查詢(xún)方法:

@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();

這里定義的POJO也支持使用@Embedded

2.3 查詢(xún)結(jié)果的返回類(lèi)型

Room中查詢(xún)操作除了返回POJO對(duì)象及其List以外臣淤, 還支持:

  • LiveData<T>:
    LiveData是架構(gòu)組件庫(kù)中提供的另一個(gè)組件沉御,可以很好滿(mǎn)足數(shù)據(jù)變化驅(qū)動(dòng)UI刷新的需求扭勉。Room會(huì)實(shí)現(xiàn)更新LiveData的代碼。
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)") 
public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
  • Flowablbe<T> Maybe<T> Single<T>:
    Room 支持返回RxJava2 的Flowablbe, MaybeSingle對(duì)象店展,對(duì)于使用RxJava的項(xiàng)目可以很好的銜接养篓, 但需要在gradle添加該依賴(lài):android.arch.persistence.room:rxjava2
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
  • Cursor:
    返回Cursor是為了支持現(xiàn)有項(xiàng)目中使用Cursor的場(chǎng)景赂蕴,官方不建議直接返回Cursor.

Caution: It's highly discouraged to work with the Cursor API because it doesn't guarantee whether the rows exist or what values the rows contain. Use this functionality only if you already have code that expects a cursor and that you can't refactor easily.

2.4 聯(lián)表查詢(xún)

Room支持聯(lián)表查詢(xún)柳弄,接口定義上與其他查詢(xún)差別不大, 主要還是sql語(yǔ)句的差別概说。

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

3. 創(chuàng)建數(shù)據(jù)庫(kù)

Room中DataBase類(lèi)似SQLite API中SQLiteOpenHelper碧注,是提供DB操作的切入點(diǎn),但是除了持有DB外糖赔, 它還負(fù)責(zé)持有相關(guān)數(shù)據(jù)表(Entity)的數(shù)據(jù)訪(fǎng)問(wèn)對(duì)象(DAO), 所以Room中定義Database需要滿(mǎn)足三個(gè)條件:

  • 繼承RoomDataBase萍丐,并且是一個(gè)抽象類(lèi)
  • 用@Database 注解,并定義相關(guān)的entity對(duì)象放典, 當(dāng)然還有必不可少的數(shù)據(jù)庫(kù)版本信息
  • 定義返回DAO對(duì)象的抽象方法
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

創(chuàng)建好以上Room的三大組件后逝变, 在代碼中就可以通過(guò)以下代碼創(chuàng)建Database實(shí)例基茵。

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

三、數(shù)據(jù)庫(kù)遷移

3.1 Room數(shù)據(jù)庫(kù)升級(jí)

在傳統(tǒng)的SQLite API中壳影,我們?nèi)绻?jí)數(shù)據(jù)庫(kù)拱层, 通常在SQLiteOpenHelper.onUpgrade方法執(zhí)行數(shù)據(jù)庫(kù)升級(jí)的sql語(yǔ)句,這些sql語(yǔ)句的通常根據(jù)數(shù)據(jù)庫(kù)版本以文件的方式或者用數(shù)組來(lái)管理态贤。有人說(shuō)這種方式升級(jí)數(shù)據(jù)庫(kù)就像在拆炸彈舱呻,相比之下在Room中升級(jí)數(shù)據(jù)庫(kù)簡(jiǎn)單的就像是按一個(gè)開(kāi)關(guān)而已醋火。

Room提供了Migration類(lèi)來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)的升級(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");
    }
};

在創(chuàng)建Migration類(lèi)時(shí)需要指定startVersionendVersion, 代碼中MIGRATION_1_2MIGRATION_2_3的startVersion和endVersion是遞增的悠汽, Migration其實(shí)是支持從版本1直接升到版本3,只要其migrate()方法里執(zhí)行的語(yǔ)句正常即可芥驳。那么Room是怎么實(shí)現(xiàn)數(shù)據(jù)庫(kù)升級(jí)的呢柿冲?其實(shí)本質(zhì)上還是調(diào)用SQLiteOpenHelper.onUpgrade,Room中自己實(shí)現(xiàn)了一個(gè)SQLiteOpenHelper兆旬, 在onUpgrade()方法被調(diào)用時(shí)觸發(fā)Migration假抄,當(dāng)?shù)谝淮卧L(fǎng)問(wèn)數(shù)據(jù)庫(kù)時(shí),Room做了以下幾件事:

  • 創(chuàng)建Room Database實(shí)例
  • SQLiteOpenHelper.onUpgrade被調(diào)用丽猬,并且觸發(fā)Migration
  • 打開(kāi)數(shù)據(jù)庫(kù)

這樣一看宿饱, Room中處理數(shù)據(jù)庫(kù)升級(jí)確實(shí)很像是加一個(gè)開(kāi)關(guān)。

3.2 原有SQLite數(shù)據(jù)庫(kù)遷移至Room

因?yàn)镽oom使用的也是SQLite脚祟, 所以可以很好的支持原有Sqlite數(shù)據(jù)庫(kù)遷移到Room谬以。

假設(shè)原有一個(gè)版本號(hào)為1的數(shù)據(jù)庫(kù)有一張表User, 現(xiàn)在要遷移到Room, 我們需要定義好Entity, DAO, Database, 然后創(chuàng)建Database時(shí)添加一個(gè)空實(shí)現(xiàn)的Migraton即可由桌。需要注意的是为黎,即使對(duì)數(shù)據(jù)庫(kù)沒(méi)有任何升級(jí)操作,也需要升級(jí)版本行您, 否則會(huì)拋異常IllegalStateException.

@Database(entities = {User.class}, version = 2)
public abstract class UsersDatabase extends RoomDatabase {
…
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        // Since we didn't alter the table, there's nothing else to do here.
    }
};
…
database =  Room.databaseBuilder(context.getApplicationContext(),
        UsersDatabase.class, "Sample.db")
        .addMigrations(MIGRATION_1_2)
        .build();

四铭乾、復(fù)雜數(shù)據(jù)的處理

在某些場(chǎng)景下我們的應(yīng)用可能需要存儲(chǔ)復(fù)雜的數(shù)據(jù)類(lèi)型,比如Date娃循,但是Room的Entity僅支持基本數(shù)據(jù)類(lèi)型和其裝箱類(lèi)之間的轉(zhuǎn)換炕檩,不支持其它的對(duì)象引用。所以Room提供了TypeConverter給使用者自己實(shí)現(xiàn)對(duì)應(yīng)的轉(zhuǎn)換捌斧。

一個(gè)Date類(lèi)型的轉(zhuǎn)換如下:

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

定義好轉(zhuǎn)換方法后捧书,指定到對(duì)應(yīng)的Database上即可, 這樣就可以在對(duì)應(yīng)的POJO(User)中使用Date類(lèi)了骤星。

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

五经瓷、總結(jié)

在SQLite API方式實(shí)現(xiàn)數(shù)據(jù)持久化的項(xiàng)目中,相信都有一個(gè)任務(wù)繁重的SQLiteOpenHelper實(shí)現(xiàn), 一堆維護(hù)表的字段的Constant類(lèi)洞难, 一堆代碼類(lèi)似的數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)類(lèi)(DAO)舆吮,訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)時(shí)需要做Cursor的遍歷,構(gòu)建并返回對(duì)應(yīng)的POJO類(lèi)...相比之下,Room作為在SQLite之上封裝的ORM庫(kù)確實(shí)有諸多優(yōu)勢(shì)色冀,比較直觀(guān)的體驗(yàn)是:

  • 比SQLite API更簡(jiǎn)單的使用方式
  • 省略了許多重復(fù)代碼
  • 能在編譯時(shí)校驗(yàn)sql語(yǔ)句的正確性
  • 數(shù)據(jù)庫(kù)相關(guān)的代碼分為Entity, DAO, Database三個(gè)部分潭袱,結(jié)構(gòu)清晰
  • 簡(jiǎn)單安全的數(shù)據(jù)庫(kù)升級(jí)方案

想要了解更多Room相關(guān)內(nèi)容可以戳下面的鏈接:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市锋恬,隨后出現(xiàn)的幾起案子屯换,更是在濱河造成了極大的恐慌,老刑警劉巖与学,帶你破解...
    沈念sama閱讀 212,332評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彤悔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡索守,警方通過(guò)查閱死者的電腦和手機(jī)晕窑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,508評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)卵佛,“玉大人杨赤,你說(shuō)我怎么就攤上這事〗赝簦” “怎么了疾牲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,812評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)衙解。 經(jīng)常有香客問(wèn)我阳柔,道長(zhǎng),這世上最難降的妖魔是什么丢郊? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,607評(píng)論 1 284
  • 正文 為了忘掉前任盔沫,我火速辦了婚禮,結(jié)果婚禮上枫匾,老公的妹妹穿的比我還像新娘架诞。我一直安慰自己,他們只是感情好干茉,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,728評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布谴忧。 她就那樣靜靜地躺著,像睡著了一般角虫。 火紅的嫁衣襯著肌膚如雪沾谓。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,919評(píng)論 1 290
  • 那天戳鹅,我揣著相機(jī)與錄音均驶,去河邊找鬼。 笑死枫虏,一個(gè)胖子當(dāng)著我的面吹牛妇穴,可吹牛的內(nèi)容都是我干的爬虱。 我是一名探鬼主播,決...
    沈念sama閱讀 39,071評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼腾它,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼跑筝!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起瞒滴,我...
    開(kāi)封第一講書(shū)人閱讀 37,802評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤曲梗,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后妓忍,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體虏两,經(jīng)...
    沈念sama閱讀 44,256評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,576評(píng)論 2 327
  • 正文 我和宋清朗相戀三年单默,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了碘举。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忘瓦。...
    茶點(diǎn)故事閱讀 38,712評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搁廓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耕皮,到底是詐尸還是另有隱情境蜕,我是刑警寧澤,帶...
    沈念sama閱讀 34,389評(píng)論 4 332
  • 正文 年R本政府宣布凌停,位于F島的核電站粱年,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏罚拟。R本人自食惡果不足惜台诗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,032評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赐俗。 院中可真熱鬧拉队,春花似錦、人聲如沸阻逮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)叔扼。三九已至事哭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓜富,已是汗流浹背鳍咱。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留与柑,地道東北人谤辜。 一個(gè)月前我還...
    沈念sama閱讀 46,473評(píng)論 2 360
  • 正文 我出身青樓澎现,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親每辟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子剑辫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,606評(píng)論 2 350

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