理解Android Architecture Components系列之Room(六)

Room在SQLite上提供了一個方便訪問的抽象層涉茧。App把經(jīng)常需要訪問的數(shù)據(jù)存儲在本地將會大大改善用戶的體驗状飞。這樣用戶在網(wǎng)絡(luò)不好時仍然可以瀏覽內(nèi)容汰瘫。當用戶網(wǎng)絡(luò)可用時狂打,可以更新用戶的數(shù)據(jù)。

使用原始的SQLite可以提供這樣的功能混弥,但是有以下兩個缺點:

  • 沒有編譯時SQL語句的檢查菱父。尤其是當你的數(shù)據(jù)庫表發(fā)生變化時,需要手動的更新相關(guān)代碼剑逃,這會花費相當多的時間并且容易出錯。
  • 編寫大量SQL語句和Java對象之間相互轉(zhuǎn)化的代碼官辽。

針對以上的缺點蛹磺,Google提供了Room來解決這些問題。Room包含以下三個重要組成部分:

  • Database 創(chuàng)建數(shù)據(jù)庫同仆。

  • Entities 數(shù)據(jù)庫表中對應(yīng)的Java對象

  • DAOs 訪問數(shù)據(jù)庫

詳細的結(jié)構(gòu)關(guān)系可以看下圖:


room_architecture.png

其實這和傳統(tǒng)寫數(shù)據(jù)庫創(chuàng)建訪問的代碼大概形式差不多的萤捆。以存儲User信息為例,看一下下面的代碼:

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.
    //Getters和setters為了簡單起見就省略了俗批,但是對Room來說是必須的
}

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

下面詳細介紹提到的各個部分:

Entities

@Entity
如果上面的User類中包含一個字段是不希望存放到數(shù)據(jù)庫中的,那么可以用@Ignore注解這個字段:

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;
    public String lastName;

    //不需要被存放到數(shù)據(jù)庫中
    @Ignore
    Bitmap picture;
}

Room持久化一個類的field必須要求這個field是可以訪問的岁忘⌒廖浚可以把這個field設(shè)為public或者設(shè)置setter和getter。

Primary Key 主鍵

每個Entity都必須定義一個field為主鍵干像,即使是這個Entity只有一個field帅腌。如果想要Room生成自動的primary key驰弄,可以使用@PrimaryKeyautoGenerate屬性。如果Entity的primary key是多個Field的復(fù)合Key速客,可以向下面這樣設(shè)置:

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

    @Ignore
    Bitmap picture;
}

在默認情況下Room使用類名作為數(shù)據(jù)庫表的名稱戚篙。如果想要設(shè)置不同的名稱,可以參考下面的代碼溺职,設(shè)置表名tableName為users:

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

和設(shè)置tableName相似岔擂,Room默認使用field的名稱作為表的列名。如果想要使用不同的名稱浪耘,可以通過@ColumnInfo(name = "first_name")設(shè)置乱灵,代碼如下:

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

索引和唯一性

根據(jù)訪問數(shù)據(jù)庫的方式,你可能想對特定的field建立索引來加速你的訪問点待。下面這段代碼展示了如何在Entity中添加索引或者復(fù)合索引:

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

下面的代碼展示了對數(shù)據(jù)庫中特定的field設(shè)置唯一性(這個表中的firstNamelastName不能同時相同):

@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ù)庫阔蛉,那么就可以在兩個對象之間建立聯(lián)系。大多數(shù)ORM庫允許Entity對象互相引用癞埠,但Room明確禁止了這樣做状原。詳細的原因,可以參考這里苗踪。

既然不允許建立直接的關(guān)系颠区,Room提供以外鍵的方式在兩個Entity之間建立聯(lián)系。

外鍵

例如通铲,有一個Pet類需要和User類建立關(guān)系毕莱,可以通過@ForeignKey來達到這個目的,代碼如下:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))
class Pet {
    @PrimaryKey
    public int petId;

    public String name;

    @ColumnInfo(name = "user_id")
    public int userId;
}

外鍵可以允許你定義被引用的Entity更新時發(fā)生的行為颅夺。例如朋截,你可以定義當刪除User時對應(yīng)的Pet類也被刪除“苫疲可以在@ForeignKey中添加onDelete = CASCADE實現(xiàn)部服。

@Insert(OnConflict = REPLACE)
定義了REMOVEREPLACE而不是簡單的UPDATE操作。這樣產(chǎn)生的后果會影響外鍵定義的約束行為拗慨,詳細的信息可以參考 SQLite documentation廓八。

獲取關(guān)聯(lián)的Entity

Entity之間可能也有一對多之間的關(guān)系。比如一個User有多個Pet赵抢,通過一次查詢獲取多個關(guān)聯(lián)的Pet剧蹂。

public class UserAndAllPets {
    @Embedded
    public User user;
    @Relation(parentColumn = "id", entityColumn = "user_id")
    public List<Pet> pets;
}

 @Dao
 public interface UserPetDao {
     @Query("SELECT * from User")
     public List<UserAndAllPets> loadUserAndPets();
 }

使用 @Relation 注解的field必須是一個List或者一個Set。通常情況下烦却, Entity 的類型是從返回類型中推斷出來的宠叼,可以通過定義 entity()來定義特定的返回類型。
@Relation 注解的field必須是public或者有public的setter其爵。這是因為加載數(shù)據(jù)是分為兩步的:1. 父Entity被查詢 2. 觸發(fā)用 @Relation 注解的entity的查詢车吹。所以筹裕,在上面UserAndAllPets例子中,首先User所在的數(shù)據(jù)庫被查詢窄驹,然后觸發(fā)查詢Pets的查詢朝卒。即Room首先出創(chuàng)建一個空的對象,然后設(shè)置父Entity和一個空的list乐埠。在第二次查詢后抗斤,Room將會填充這個list。

對象嵌套對象

有時候需要在類里面把另一個類作為field丈咐,這時就需要使用@Embedded 瑞眼。這樣就可以像查詢其他列一樣查詢這個field。
例如棵逊,User類可以包含一個field Address伤疙,代表User的地址包括所在街道、城市辆影、州和郵編徒像。代碼如下:

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蛙讥。
Embedded 的field中也可以包含其他Embedded的field锯蛀。
如果多個Embedded的field是類型相同的,可以通過設(shè)置 prefix 來保證列的唯一性次慢。

Data Access Objects(DAOs)

DAOs是數(shù)據(jù)庫訪問的抽象層旁涤。
Dao可以是一個接口也可以是一個抽象類。如果是抽象類迫像,那么它可以接受一個RoomDatabase作為構(gòu)造器的唯一參數(shù)劈愚。
Room不允許在主線程中防偽數(shù)據(jù)庫,除非在builder里面調(diào)用allowMainThreadQueries() 闻妓。因為訪問數(shù)據(jù)庫是耗時的造虎,可能阻塞主線程,引起UI卡頓纷闺。

添加方便使用的方法

Insert

使用 @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 方法只接受一個參數(shù)犁功,那么將返回一個long,對應(yīng)著插入的rowId婚夫。如果接受多個參數(shù)浸卦,或者數(shù)組,或者集合案糙,那么就會返回一個long的數(shù)組或者list限嫌。

Update

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

也可以讓update方法返回一個int型的整數(shù)靴庆,代表被update的行號。

Delete

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

和update方法一樣怒医,也可以返回一個int型的整數(shù)炉抒,代表被delete的行號。

使用@Query注解的方法

@Query 注解的方法在編譯時就會被檢查稚叹,如果有任何查詢的問題焰薄,都會拋出編譯異常,而不是等到運行以后才會發(fā)現(xiàn)異常扒袖。
Room也會檢查查詢返回值的類型塞茅,如果返回類型的字段和數(shù)據(jù)路列名存在不一致,會收到警告季率。如果兩者完全不一致野瘦,就會產(chǎn)生錯誤。

簡單的查詢

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

帶參數(shù)查詢

下面的代碼顯示了如何根據(jù)年齡條件查詢User信息:

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

同理飒泻,這里也會在編譯時做類型檢查鞭光,如果表中沒有age這個列,那么就會拋出錯誤蠢络。
也可以穿入多個參數(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);
}

返回列的子集

有時可能只需要Entity的幾個field衰猛,例如只需要獲取User的姓名就行了。通過只獲取這兩列的數(shù)據(jù)不僅能夠節(jié)省寶貴的資源刹孔,還能加快查詢速度啡省。
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();
}

可被觀察的查詢

通過和LiveData 的配合使用髓霞,就可以實現(xiàn)當數(shù)據(jù)庫內(nèi)容發(fā)生變化時自動收到變化后的數(shù)據(jù)的功能卦睹。

@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實現(xiàn)響應(yīng)式查詢

Room也可以返回RxJava2中PublisherFlowable
格式的數(shù)據(jù)。如果需要使用這項功能方库,需要在Gradle中添加android.arch.persistence.room:rxjava2结序。

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

詳細的信息可以參考 Room and RxJava這篇文章。

直接獲取Cursor

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

查詢多個表

有時可能需要查詢多個表來獲取結(jié)果纵潦,Room也定義這樣的功能徐鹤。下面這段代碼演示了如何從一個包含借閱用戶信息的表和一個包含已經(jī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();

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

使用類型轉(zhuǎn)換器

如果想要在數(shù)據(jù)庫中存儲Date邀层,可以存儲等價的Unix時間戳返敬。通過 TypeConverter 可以很方便的做到這一點:

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和Unix時間戳相互轉(zhuǎn)換寥院。Room支持存儲Long類型的對象劲赠,這樣就可以通過這種方法存儲Date。
接下來將 TypeConverter添加到AppDatabase中,這樣Room就能識別這種轉(zhuǎn)換:
AppDatabase.java

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

接下來就可以像使用基本類型一樣使用自定義類型的查詢凛澎,比如:
User.java

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

UserDao.java

@Dao
public interface UserDao {
    ...
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    List<User> findUsersBornBetweenDates(Date from, Date to);
}

關(guān)于更多 @TypeConverters的用法霹肝,可以參考這里

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

隨著業(yè)務(wù)的擴展有時候需要對數(shù)據(jù)庫調(diào)整一些字段塑煎。當數(shù)據(jù)庫升級時沫换,需要保存已有的數(shù)據(jù)。
Room使用 Migration 來實現(xiàn)數(shù)據(jù)庫的遷移轧叽。每個 Migration 都指定了startVersionendVersion苗沧。在運行的時候Room運行每個 Migrationmigrate() 方法,按正確的順序來遷移數(shù)據(jù)庫到下個版本炭晒。如果沒有提供足夠的遷移信息待逞,Room會重新創(chuàng)建數(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的使用识樱。如果想要詳細了解Room,建議大家動手去寫一個簡單的demo震束。文章結(jié)尾主要還是想提一下數(shù)據(jù)庫在設(shè)計一個App架構(gòu)的重要地位怜庸。下一篇文章將會講解一個高效加載數(shù)據(jù)的庫。

相關(guān)文章:
理解Android Architecture Components系列(一)
理解Android Architecture Components系列(二)
理解Android Architecture Components系列之Lifecycle(三)
理解Android Architecture Components系列之LiveData(四)
理解Android Architecture Components系列之ViewModel(五)
理解Android Architecture Components系列之Room(六)
理解Android Architecture Components系列之Paging Library(七)
理解Android Architecture Components系列之WorkManager(八)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末垢村,一起剝皮案震驚了整個濱河市割疾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嘉栓,老刑警劉巖宏榕,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侵佃,居然都是意外死亡麻昼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門馋辈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抚芦,“玉大人,你說我怎么就攤上這事迈螟〔媛眨” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵答毫,是天一觀的道長褥民。 經(jīng)常有香客問我,道長烙常,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮蚕脏,結(jié)果婚禮上侦副,老公的妹妹穿的比我還像新娘。我一直安慰自己驼鞭,他們只是感情好秦驯,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著挣棕,像睡著了一般译隘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上洛心,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天固耘,我揣著相機與錄音,去河邊找鬼词身。 笑死厅目,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的法严。 我是一名探鬼主播损敷,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼深啤!你這毒婦竟也來了拗馒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤溯街,失蹤者是張志新(化名)和其女友劉穎诱桂,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苫幢,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡访诱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了韩肝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片触菜。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哀峻,靈堂內(nèi)的尸體忽然破棺而出涡相,到底是詐尸還是另有隱情,我是刑警寧澤剩蟀,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布催蝗,位于F島的核電站,受9級特大地震影響育特,放射性物質(zhì)發(fā)生泄漏丙号。R本人自食惡果不足惜先朦,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望犬缨。 院中可真熱鬧喳魏,春花似錦、人聲如沸怀薛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枝恋。三九已至创倔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間焚碌,已是汗流浹背畦攘。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留呐能,地道東北人念搬。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像摆出,于是被迫代替她去往敵國和親朗徊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

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