Android架構(gòu)組件(四):Room

前言

上篇我們分析了對于Android架構(gòu)體系最終要的Viewmodel組件,它可以實現(xiàn)數(shù)據(jù)和view之間的管理,并且能提供組件間的通訊(注意fragment獲取viewmodel時傳入的對象要一致)。
那么,接下來我們就學(xué)習(xí)一下和Livedata完美兼容的數(shù)據(jù)庫——Room

Room是Google推出的Android架構(gòu)組件庫中的數(shù)據(jù)持久化組件庫, 也可以說是在SQLite上實現(xiàn)的一套ORM解決方案。
Room數(shù)據(jù)存儲庫支持返回Livedata對象的可觀察查詢,當(dāng)數(shù)據(jù)庫更新時,Room 會生成更新 LiveData 對象所需的所有代碼。在需要時,生成的代碼會在后臺線程上異步運行查詢。此模式有助于使界面中顯示的數(shù)據(jù)與存儲在數(shù)據(jù)庫中的數(shù)據(jù)保持同步。

Room是什么?

Room主要包含四個步驟:

  • Entity:表示持有數(shù)據(jù)庫行的類(即數(shù)據(jù)表結(jié)構(gòu))。對于每個實體,將會創(chuàng)建一個數(shù)據(jù)庫表來持有他們。你必須通過Database類的entities數(shù)組來引用實體類。實體類的中的每個字段除了添加有 @Ignore注解外的,都會存放到數(shù)據(jù)庫中。

  • Dao:表示作為數(shù)據(jù)訪問對象(DAO)的類或接口。DAO是Room的主要組件,負(fù)責(zé)定義訪問數(shù)據(jù)庫的方法。由 @Database注解標(biāo)注的類必須包含一個無參數(shù)且返回使用 @Dao注解的類的抽象方法。當(dāng)在編譯生成代碼時,Room創(chuàng)建該類的實現(xiàn)。

  • Database :用來創(chuàng)建一個數(shù)據(jù)庫持有者。注解定義一系列實體,類的內(nèi)容定義一系列DAO。它也是底層連接的主入口點。

  • Room :數(shù)據(jù)庫的創(chuàng)建者 & 負(fù)責(zé)數(shù)據(jù)庫版本更新的具體實現(xiàn)者

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


Room關(guān)系圖

Room的基本使用

1. 創(chuàng)建Entity實體(Entity)
@Entity
public class User {
    // 主鍵-設(shè)置自增長 默認(rèn)false
    @PrimaryKey(autoGenerate = true)
    private int uid;
    // 數(shù)據(jù)表中的名字 默認(rèn)字段名
    @ColumnInfo(name = "name")
    private String name;

    private int age;
    //注解該字段不加入數(shù)據(jù)表中
    @Ignore
    private String sex;
    //引用其它實體類
    @Embedded
    private Education mEducation;
    
    // ...省略getter and setter

    public class Education{
        private String HighSchool;
        private String University;
    }
}

我們先來介紹下實體類中的注解及其含義:

  • @Entity :數(shù)據(jù)表的實體類
  • @PrimaryKey :每一個實體類都需要一個唯一的標(biāo)識即主鍵。
  • @ColumnInfo :數(shù)據(jù)表中字段的名字
  • @Ignore :標(biāo)注不需要加入數(shù)據(jù)表中的屬性
  • @Embedded :實體類中引用其它實體類
  • @ForeignKey :外鍵約束

1.1 @Entity——實體類

1.1.1 指定表名
用@Entity標(biāo)注的類,默認(rèn)表示當(dāng)前的類名即為表名,當(dāng)然我們也可以指定表名:
@Entity(tableName = "other")
1.1.2 設(shè)置主鍵或復(fù)合主鍵
我們也可以在@Entity中設(shè)置主鍵、復(fù)合主鍵:
這里注意:
主鍵的字段不能為null,也不允許有重復(fù)值
復(fù)合主鍵的字段不能為null,所以需要加上@Nullable注解
復(fù)合主鍵只有主鍵都一致,才會覆蓋,相當(dāng)于&&

@Entity(primaryKeys = "uid")
public class User {...}

@Entity(primaryKeys = {"uid", "name"})
public class User {
    @Nullable //復(fù)合主鍵時需注意,不能為null
    private String name;
}

1.1.3 設(shè)置索引
數(shù)據(jù)庫添加索引,可以提高數(shù)據(jù)庫訪問速度。
索引可以有單列索引,組合索引及索引的唯一性
索引的唯一性unique = true,表示數(shù)據(jù)不可重復(fù),但在組合索引中不作為條件依據(jù)

//單列索引          @Entity(indices = {@Index(value = "name")})
//單列索引唯一性      @Entity(indices = {@Index(value = "name", unique = true)})

//組合索引           @Entity(indices ={@Index(value = {"name","age"})})
//組合索引唯一性      @Entity(indices ={@Index(value = {"name","age"},unique = true)})

//當(dāng)然可以混起來用 如下:
@Entity(indices ={@Index(value = "name"),@Index(value = {"name","age"},unique = true)})
public class User {...}

1.1.4 外鍵約束
我們再創(chuàng)建一個實體類Book

@Entity(foreignKeys = @ForeignKey(entity = User.class,parentColumns = "uid",childColumns = "fatherId"))
public class Book{
    private int bookId;
    private String bookName;
    private int fatherId;
}

我們看下這段注解的含義:
它表示Book實體類依附于User實體類entity = User.class
并且注明父類User的列uid字段parentColumns = "uid"
子類Book的列fatherId字段childColumns = "fatherId"
表明了子類的fatherId相當(dāng)于父類uid(fatherId == uid)
@ForeignKey還有兩個屬性onDeleteonUpdate

@Entity(foreignKeys = @ForeignKey(onDelete = CASCADE,onUpdate = CASCADE,entity = User.class,parentColumns = "uid",childColumns = "fatherId"))
public class Book {...}

這里屬性值有以下幾種:

  • NO_ACTION:當(dāng)User中的uid有變化的時候Book中的father_id不做任何動作
  • RESTRICT:當(dāng)User中的uid在Book里有依賴的時候禁止對User做動作,做動作就會報錯。
  • SET_NULL:當(dāng)User中的uid有變化的時候Book的fatherId會設(shè)置為NULL。
  • SET_DEFAULT:當(dāng)User中的uid有變化的時候Book的fatherId會設(shè)置為默認(rèn)值,我這里是int型,那么會設(shè)置為0
  • CASCADE:當(dāng)User中的uid有變化的時候Book的fatherId跟著變化,假如我把uid = 1的數(shù)據(jù)刪除,那么Book表里,fatherId = 1的都會被刪除。

1.2 @PrimaryKey——主鍵

public class User {
    //我們可以直接在字段上設(shè)置uid為主鍵
    @PrimaryKey
    private int uid;
    //想要自增長那么這樣
    @PrimaryKey(autoGenerate = true)
    private int uid;
}

1.3 @ColumnInfo——表中字段名

public class User {
    //默認(rèn)實體類字段名為表中字段名
    private int uid;
    //指定后表里的key就是uid_
    @ColumnInfo(name = "uid_")
    private int uid;
}

1.4 @Ignore——忽略字段,不添加進表中

public class User{
    //注解標(biāo)記后 sex字段不會添加進數(shù)據(jù)表中
    @Ingore
    private String sex;
}

1.5 @Embedded——引用其它實體類

public class User{
    @Embedded
    private Book book;
}

假如實體類中包含了多個同一類型的嵌入字段(比如一個人User擁有兩本Book),我們可以通過設(shè)置prefix屬性來保持每列的唯一性。Room會將提供的值添加到嵌入對象的每個列名的開頭。

//@Embedded(prefix = "one"),這個是區(qū)分唯一性的,
//比如說一這個人有2本書并添加了tag,那么在數(shù)據(jù)表中就會以prefix+屬性值命名
@Embedded(prefix = "one")
private Book address;
@Embedded(prefix = "two")
private Book address;
2. 創(chuàng)建數(shù)據(jù)訪問對象(DAO)

Dao以簡潔的方式抽象了我們對數(shù)據(jù)庫的訪問。
Dao可以定義為接口或者抽象類。如果它是抽象類,它可以有一個RoomDatabase作為唯一參數(shù)的構(gòu)造函數(shù)。

注意:Room不允許在主線程中訪問數(shù)據(jù)庫,除非你可以builder上調(diào)用allowMainThreadQueries(),因為它可能會長時間鎖住UI。
異步查詢(返回LiveDataRxJava Flowable的查詢)則不受此影響,因為它們可以異步運行在后臺線程上。

Dao的相關(guān)注解很簡單,我們來看一下:

  • @Dao : 標(biāo)注數(shù)據(jù)庫操作的類。
  • @Query : 包含所有Sqlite語句操作。
  • @Insert : 標(biāo)注數(shù)據(jù)庫的插入操作。
  • @Delete : 標(biāo)注數(shù)據(jù)庫的刪除操作。
  • @Update : 標(biāo)注數(shù)據(jù)庫的更新操作。

這里不用過多敘述了,除了一個標(biāo)注操作類的@Dao,其余就是增刪改查了。

我們直接上代碼:

@Dao
public interface UserDao {
    //查詢所有數(shù)據(jù)
    @Query("Select * from user")
    List<User> getAll();

    //刪除全部數(shù)據(jù)
    @Query("DELETE FROM user")
    void deleteAll();

    //一次插入單條數(shù)據(jù) 或 多條
    //@Insert(onConflict = OnConflictStrategy.REPLACE),這個是干嘛的呢,下面有詳細(xì)教程
    @Insert
    void insert(User... users);

    //一次刪除單條數(shù)據(jù) 或 多條
    @Delete
    void delete(User... users);

    //一次更新單條數(shù)據(jù) 或 多條
    @Update
    void update(User... users);

    //根據(jù)字段去查找數(shù)據(jù)
    @Query("SELECT * FROM user WHERE uid= :uid")
    Person getUserByUid(int uid);

    //一次查找多個數(shù)據(jù)
    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(List<Integer> userIds);

    //多個條件查找
    @Query("SELECT * FROM user WHERE name = :name AND age = :age")
    Person getUserByNameage(String name, int age);
}

這里唯一特殊的就是@Insert。其有一段介紹:對數(shù)據(jù)庫設(shè)計時,不允許重復(fù)數(shù)據(jù)的出現(xiàn)。否則,必然造成大量的冗余數(shù)據(jù)。實際上,難免會碰到這個問題:沖突。當(dāng)我們像數(shù)據(jù)庫插入數(shù)據(jù)時,該數(shù)據(jù)已經(jīng)存在了,必然造成了沖突。該沖突該怎么處理呢?在@Insert注解中有conflict用于解決插入數(shù)據(jù)沖突的問題,其默認(rèn)值為OnConflictStrategy.ABORT。對于OnConflictStrategy而言,它封裝了Room解決沖突的相關(guān)策略。

  • OnConflictStrategy.REPLACE:沖突策略是取代舊數(shù)據(jù)同時繼續(xù)事務(wù)
  • OnConflictStrategy.ROLLBACK:沖突策略是回滾事務(wù)
  • OnConflictStrategy.ABORT:沖突策略是終止事務(wù)
  • OnConflictStrategy.FAIL:沖突策略是事務(wù)失敗
  • OnConflictStrategy.IGNORE:沖突策略是忽略沖突

這里比如在插入的時候我們加上了OnConflictStrategy.REPLACE,那么往已經(jīng)有uid=1的person表里再插入uid =1的person數(shù)據(jù),那么新數(shù)據(jù)會覆蓋舊數(shù)據(jù)。如果我們什么都不加,那么久是默認(rèn)的OnConflictStrategy.ABORT,重復(fù)上面的動作,你會發(fā)現(xiàn),程序崩潰了。也就是上面說的終止事務(wù)。

3. 數(shù)據(jù)庫持有者(Database)

我們下來看下代碼:

//注解指定了database的表映射實體數(shù)據(jù)以及版本等信息(后面會詳細(xì)講解版本升級)
@Database(entities = {User.class, Book.class}, version = 1)
public abstract class AppDataBase extends RoomDatabase {
    public abstract UserDao getUserDao();
    
    public abstract BookDao getBookDao();
}

如果后期我們需要往已建的數(shù)據(jù)表中加入新的字段,或者增加新的索引,這時候就需要我們
對數(shù)據(jù)庫版本進行升級
Room中,我們需要在Database中修改版本信息,并添加Migration類,告訴Room是哪張表?改了什么內(nèi)容?


//修改version = 2
@Database(entities = {User.class, Book.class}, version = 2)
public abstract class AppDataBase extends RoomDatabase {
    public abstract UserDao getUserDao();
    public abstract BookDao getBookDao();
    //數(shù)據(jù)庫變動添加Migration,簡白的而說就是版本1到版本2改了什么東西
    public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            //告訴user表,增添一個String類型的字段 job
            database.execSQL("ALTER TABLE user ADD COLUMN job TEXT");
        }
    };
}
//我們添加完了migration后,根據(jù)Room.builder把我們版本更新的信息add進去
//我們稍后會講到
Room.databaseBuilder(...)
    //加上版本升級信息
    .addMigrations(AppDataBase.MIGRATION_1_2)
    .build();
4. 數(shù)據(jù)庫的創(chuàng)建者(Room)

Room是數(shù)據(jù)庫的創(chuàng)建者,在創(chuàng)建Database實例的時候,我們需要遵循單例模式,避免操作時創(chuàng)建多個Database實例,所以我們把它封裝成單例:

public class DBInstance {
    private static final String DB_NAME = "room_test";
    public static AppDataBase appDataBase;
    public static AppDataBase getInstance(){
        if(appDataBase==null){
            synchronized (DBInstance.class){
                if(appDataBase==null){
                    appDataBase = Room.databaseBuilder(App.getContext(), AppDataBase.class, DB_NAME)
                        //下面注釋表示允許主線程進行數(shù)據(jù)庫操作,但是不推薦這樣做。
                        //我這里是為了Demo展示,稍后會介紹和LiveData、RxJava的使用
                        .allowMainThreadQueries()
                        .build();
                }
            }
        }
        return appDataBase;
    }
}
5. 舉個完整栗子~

上面我們四個部分已經(jīng)分析完畢了,我們接下來舉一個完整栗子來貫穿一下Room的用法:

// 1.首先我們創(chuàng)建實體類 Entity
@Entity
public class User {
    ...
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

// 2.創(chuàng)建數(shù)據(jù)訪問對象 UserDao ——提供增刪改查接口
@Dao
public interface UserDao {
    //查詢所有數(shù)據(jù)
    @Query("Select * from user")
    List<User> getAll();
    ...
    //多個條件查找
    @Query("SELECT * FROM user WHERE name = :name AND age = :age")
    Person getUserByNameage(String name, int age);
}

// 3.創(chuàng)建數(shù)據(jù)庫持有者 Database
@Database(entities = {User.class, Book.class}, version = 2)
public abstract class AppDataBase extends RoomDatabase {
    public abstract UserDao getUserDao();
    public abstract BookDao getBookDao();
    //數(shù)據(jù)庫變動添加Migration,簡白的而說就是版本1到版本2改了什么東西
    public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            //告訴user表,增添一個String類型的字段 job
            database.execSQL("ALTER TABLE user ADD COLUMN job TEXT");
        }
    };
}

//4. 實例化并操作數(shù)據(jù)庫
public class MainActivity extends AppCompatActivity {
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_insert:
                User user = new User("Room", 18);
                DBInstance.getInstance().getUserDao().insert(user);
                break;
            ...
        }
    }
}

Room結(jié)合LiveData使用

上文之中我們在單例模式中提到了

Room.databaseBuilder(...)
    //允許在主線程中查詢
    .allowMainThreadQueries()
    .build();

如果數(shù)據(jù)庫中數(shù)據(jù)龐大,會導(dǎo)致阻塞UI,進而帶來不好的用戶體驗,那么我們選擇用Livedata就可以解決這一問題。
我們以上一篇Viewmodel中的栗子來說(省略以上Room四個步驟):

public class MyViewModel extends ViewModel {
    //如果不熟悉Livedata用法可以閱讀我關(guān)于Livedata的博客
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<Users>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // 異步調(diào)用獲取用戶列表
        ...
        users.setValue(data);
    }
}

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            //存進數(shù)據(jù)庫
            DBInstance.getInstance().getUserDao().insert(users);
            //數(shù)據(jù)庫中查詢所有
            query();
        });
    }
    public void query(){
        //getAll()返回Livedata對象
        DBInstance.getInstance().getUserDao().getAll()
            .observe(this, new Observer<List<User>>() {
                    @Override
                    public void onChanged(List<User> users) {
                        //查詢到所有user用戶
                    }
                });
    }
}

參考&感謝

Android從零開始搭建MVVM架構(gòu)(4)————Room(從入門到進階)

玩Android

總結(jié)

以上就是我們最新學(xué)習(xí)的系統(tǒng)架構(gòu)組件之一的——Room,相信我們通過文章的四部過程,完美詮釋了數(shù)據(jù)庫從創(chuàng)建,操作,版本更新,及配合Livedata的使用步驟,我也相信各位小伙伴已經(jīng)掌握了它的大部分使用原理,當(dāng)然了Room還有更多的細(xì)節(jié)等待著我們?nèi)ヌ剿鳌?br> 至此,我的Android架構(gòu)組件系列主體部分均已講解完畢了,或許有人會問到為什么沒有和MVVM架構(gòu)完美匹配的Databinding的講解呢?
其實,關(guān)于Databinding我也已經(jīng)學(xué)習(xí)及了解過了,它可以將數(shù)據(jù)和xml進行綁定,當(dāng)數(shù)據(jù)發(fā)生變化時會自動更新UI,這確實幫助我們有效的減少了view組件中不少的亢余代碼,然而也帶了一些缺點,比如復(fù)雜頁面xml會很沉重,以及代碼閱讀性、單元測試及定位bug起到了負(fù)面作用。
所以我這次決定搭建的MVVM框架剔除了對databinding的依賴。那么下來我們就開始搭建MVVM之旅吧~

Android架構(gòu)組件系列文章

我的博客(Power)
Android架構(gòu)組件(一):Lifecycle
Android架構(gòu)組件(二):LiveData
Android架構(gòu)組件(三):Viewmodel
Android架構(gòu)組件(四):Room

感謝您的閱讀和支持!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,958評論 2 373