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

在真正接觸並使用MVVM架構的時候,整我的都很差了。由於我的以爲,MVVM相對於MVC、MVP學習難度比較大,設計的知識點不是一點半點。因此想慢慢記錄下本身的成長。若有錯誤但願指正。java


從零開始搭建MVVM架構系列文章(持續更新):
Android從零開始搭建MVVM架構(1)————DataBinding
Android從零開始搭建MVVM架構(2)————ViewModel
Android從零開始搭建MVVM架構(3)————LiveData
Android從零開始搭建MVVM架構(4)————Room(從入門到進階)
Android從零開始搭建MVVM架構(5)————Lifecycles
Android從零開始搭建MVVM架構(6)————使用玩Android API帶你搭建MVVM框架(初級篇)
Android從零開始搭建MVVM架構(7) ———— 使用玩Android API帶你搭建MVVM框架(終極篇)react


仍是那張圖AAC(Android Architecture Components)android

這篇咱們講Room,讓咱們瞭解和認識Room後,最終運用到咱們的MVVM的項目中去。本文是本身的總結,若有錯誤,請指正git


1、Room介紹和簡單認識

簡介
Room是google爲了簡化舊式的SQLite操做專門提供的一個覆蓋SQLite抽象層框架庫github

做用
實現SQLite的增刪改查(經過註解的方式實現增刪改查,相似Retrofit。)數據庫


在使用Room,有4個模塊:架構

  • Bean:實體類,表示數據庫表的數據
  • Dao:數據操做類,包含用於訪問數據庫的方法
  • Database:數據庫持有者 & 數據庫版本管理者
  • Room:數據庫的建立者 & 負責數據庫版本更新的具體實現者

與greendao的區別(這裏只是簡單從表面看):一樣基於ORM模式封裝的數據庫。而Room和其餘ORM對比,具備編譯時驗證查詢語句正常性,支持LiveData數據返回等優點。咱們選擇room,更可能是由於對LiveData的完美支持。同時也支持RxJava,咱們都知道數據庫操做這些耗時操做都應該放在子線程裏,因此配合RxJava和LiveData很完美了。由於他們都是異步的app

//添加Room的依賴
    implementation 'android.arch.persistence.room:runtime:2.1.4'
    annotationProcessor 'android.arch.persistence.room:compiler:2.1.4'
複製代碼

2、Bean:實體類,表示數據庫表的數據

意思就是咱們要往數據庫裏建表、建字段。就是使用這個bean對象。首先介紹下註解框架

  • @Entity : 數據表的實體類。
  • @PrimaryKey : 每個實體類都須要一個惟一的標識。
  • @ColumnInfo : 數據表中字段名稱。
  • @Ignore : 標註不須要添加到數據表中的屬性。
  • @Embedded : 實體類中引用其餘實體類。
  • @ForeignKey : 外鍵約束。

這裏咱們建一個Person類(爲了能保存數據,使數據持久化且Room必須可以對它進行操做,你能夠用public修飾屬性,或者你也能夠設置成private,但必須提供set和get方法)。這裏只是簡單展現,後面詳細講解,以爲細節太多了異步

表名爲person的表

@Entity
public class Person {
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "uid")
    private int uid;

    private String name;
    private int age;

    @Ignore
    private int money;
    @Embedded
    private Address address;
    
    //...我用的是private,暫且去掉了set和get方法。便於讀者理解
}

複製代碼

Address類:
public class Address {
    private String city;
    private String street;
    //...省略部分代碼,便於理解
}
複製代碼

2.一、@Entity

用了@Entity標註的類,表示當前類的類名做爲表名,這個類裏面的全部屬性,做爲表裏的字段。這裏咱們先只關注@Entity來說,後面又不少細節,文章接下來都以這種講解分格。更加直擊重點

2.1.一、若是不想用類名做爲表名,咱們能夠這樣

//這樣的話,咱們的表名就變成了 other
@Entity(tableName = "other")
public class Person {
}
複製代碼

2.1.二、@Entity裏的複合主鍵

在Person裏,咱們用@PrimaryKey(autoGenerate = true)標識uid爲主鍵,且設置爲自增加。設置爲主鍵的字段不得爲空也不容許有重複值。

複合主鍵:多字段主鍵則構成主鍵的多個字段的組合不得有重複(假如咱們用name作主鍵,若是咱們有2個name相同的數據一塊兒插入,數據就會被覆蓋掉。可是現實中真的有同名的人,是2條數據,這時候咱們就要用name和出生日期做爲複合主鍵也就是多個主鍵,主鍵都一致纔會覆蓋數據)

@Entity(primaryKeys = {"uid","name"})
public class Person {
}
複製代碼

直接這樣設置後,運行項目。這裏有幾點要注意的:

  • 首先會報錯:You must annotate primary keys with @NonNull. "name" is nullable。因此要加上,
@Entity(primaryKeys = {"uid","name"})
public class Person {
    //name字段要用@NonNull標註
    @NonNull
    private String name;
}
複製代碼

2.1.三、@Entity裏的索引的使用

索引的使用(有單列索引和組合索引,還有索引的惟一性)

//單列索引 @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)})
//固然能夠混起來用 以下:
@Entity(indices ={@Index(value = "name"),@Index(value = {"name","age"},unique = true)})
public class Person {
    
}
複製代碼
  • 數據庫索引是用來提升數據庫訪問速度的,能夠說單純是優化的意思。咱們加上索引後,以後的其餘操做都沒有變的
  • 若是加上惟一性有點相似主鍵,重複數據會報錯,可是索引並不像主鍵那樣,做爲條件才能去覆蓋數據
  • 插入數據的時候加上動做@Insert(onConflict = OnConflictStrategy.REPLACE)加上動做,他的意思是主鍵相同的話,舊數據會替換新數據。但若是咱們主鍵不一樣,但加了索引惟一性的話,索引相同的話,此次插入則失敗。相信這麼說,應該明白了

2.1.四、@Entity裏的外鍵約束

一樣以以前的Person做爲父類,咱們再定一個衣服類Clothes。(這裏先省略Dao,Database,Room步驟,後面會細講)

Clothes:

@Entity(foreignKeys = @ForeignKey(entity = Person.class,parentColumns = "uid",childColumns = "father_id"))
public class Clothes {
    @PrimaryKey(autoGenerate = true)
    private int id;
    private String color;
    private int father_id;
    //...省略get和set
}
複製代碼

好多人不知道外鍵約束是什麼意思,這裏咱們先往裏面插數據,而後咱們看看db裏的數據:

第一步:咱們往Person裏面插入2填數據
一、(uid =1 name = 岩漿 age =18)
二、(uid =2 name = 小學生 age=10);

第二部:咱們往衣服裏面插入3條數據
一、(id = 1 color = 紅色 father_id = 1)
二、(id = 2 color = 黑色 father_id = 1)
三、(id = 3 color = 紅色 father_id = 2)

這裏其實顯而易見,能夠先認爲,person岩漿有2件衣服,紅色和黑色的衣服;person小學生有1件衣服,紅色的衣服。咱們看看錶是怎麼樣的。意思就是用parentColumns = "uid"(person的uid字段)做爲childColumns = "father_id"(clothes的father_id字段)。這裏就至關於約束到了。先不急,咱們看看2張表。

person表(後面會有教程,教你怎麼看db數據庫):

clothes表

那麼爲何說是外鍵約束呢?固然這裏有操做。以下:

@Entity(foreignKeys = @ForeignKey(onDelete = CASCADE,onUpdate = CASCADE,entity = Person.class,parentColumns = "uid",childColumns = "father_id"))
public class Clothes {
    
}
複製代碼

這裏我加了2個動做,在刪除和更新的時候用了onDelete = CASCADE,onUpdate = CASCADE。這裏動做有如下:

  • NO_ACTION:當person中的uid有變化的時候clothes的father_id不作任何動做
  • RESTRICT:當person中的uid在clothes裏有依賴的時候禁止對person作動做,作動做就會報錯。
  • SET_NULL:當person中的uid有變化的時候clothes的father_id會設置爲NULL。
  • SET_DEFAULT:當person中的uid有變化的時候clothes的father_id會設置爲默認值,我這裏是int型,那麼會設置爲0
  • CASCADE:當person中的uid有變化的時候clothes的father_id跟着變化,假如我把uid = 1的數據刪除,那麼clothes表裏,father_id = 1的都會被刪除。

如今是否是很清楚了。不少博客都帶過。我也費力講清楚了。給博主個贊把。文章demo沒有作處理,在觀察時,記得請按順序觀察。


2.二、@PrimaryKey

//省略部分代碼,便於理解
public class Person {
    //person固然不須要符合主鍵,咱們能夠直接這樣默認uid爲主鍵
    //想要自增加那麼這樣@PrimaryKey(autoGenerate = true)
    @PrimaryKey
    private int uid;
    }
複製代碼

2.三、@ColumnInfo

咱們都知道,Person裏的屬性值名就是表裏的字段名。假如不像用屬性名當字段名,能夠這樣

//省略部分代碼,便於理解
public class Person {
    //那麼這個時候個人主鍵在表裏的key就是uid_
    @ColumnInfo(name = "uid_")
    private int uid;
    }
複製代碼

2.四、@Ignore

若是不想要屬性值做爲表裏的字段,那麼忽略掉

//省略部分代碼,便於理解
public class Person {
    //讓咱們忽略調錢,人要錢幹嗎。。
    @Ignore
    private int money;
    }
複製代碼

2.五、@Embedded

實體類中引用其餘實體類。這樣的話Address裏屬性也成爲了表person的字段。

//省略部分代碼,便於理解
public class Person {
    @Embedded
    private Address address;
    }
複製代碼

咱們Address裏有2個字段,city,street,因此咱們的表也是

這裏有個特殊的地方,好比說這我的頗有錢(剛剛纔忽略掉錢),有2個家,有2個Address類,那麼怎麼辦呢,

//@Embedded(prefix = "one"),這個是區分惟一性的,好比說一這我的可能有2個地址相似於tag,那麼在數據表中就會以prefix+屬性值命名
    @Embedded(prefix = "one")
    private Address address;
    @Embedded(prefix = "two")
    private Address address;
複製代碼

3、Dao:數據操做類,包含用於訪問數據庫的方法

這裏直接上代碼,相關標註是:

  • @Dao : 標註數據庫操做的類。
  • @Query : 包含全部Sqlite語句操做。
  • @Insert : 標註數據庫的插入操做。
  • @Delete : 標註數據庫的刪除操做。
  • @Update : 標註數據庫的更新操做。
@Dao
public interface PersonDao {
    //查詢全部數據
    @Query("Select * from person")
    List<Person> getAll();

    //刪除所有數據
    @Query("DELETE FROM person")
    void deleteAll();

    //一次插入單條數據 或 多條
// @Insert(onConflict = OnConflictStrategy.REPLACE),這個是幹嗎的呢,下面有詳細教程
    @Insert
    void insert(Person... persons);

    //一次刪除單條數據 或 多條
    @Delete
    void delete(Person... persons);

    //一次更新單條數據 或 多條
    @Update
    void update(Person... persons);

    //根據字段去查找數據
    @Query("SELECT * FROM person WHERE uid= :uid")
    Person getPersonByUid(int uid);

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

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

複製代碼

這裏惟一特殊的就是@Insert。其有一段介紹:對數據庫設計時,不容許重複數據的出現。不然,必然形成大量的冗餘數據。實際上,不免會碰到這個問題:衝突。當咱們像數據庫插入數據時,該數據已經存在了,必然形成了衝突。該衝突該怎麼處理呢?在@Insert註解中有conflict用於解決插入數據衝突的問題,其默認值爲OnConflictStrategy.ABORT。對於OnConflictStrategy而言,它封裝了Room解決衝突的相關策略。

  • OnConflictStrategy.REPLACE:衝突策略是取代舊數據同時繼續事務
  • OnConflictStrategy.ROLLBACK:衝突策略是回滾事務
  • OnConflictStrategy.ABORT:衝突策略是終止事務
  • OnConflictStrategy.FAIL:衝突策略是事務失敗
  • OnConflictStrategy.IGNORE:衝突策略是忽略衝突

這裏好比在插入的時候咱們加上了OnConflictStrategy.REPLACE,那麼往已經有uid=1的person表裏再插入uid =1的person數據,那麼新數據會覆蓋就數據。若是咱們什麼都不加,那麼久是默認的OnConflictStrategy.ABORT,重複上面的動做,你會發現,程序崩潰了。也就是上面說的終止事務。其餘你們能夠本身試試


4、Database:數據庫持有者 & 數據庫版本管理者

直接上代碼

//註解指定了database的表映射實體數據以及版本等信息(後面會詳細講解版本升級)
@Database(entities = {Person.class, Clothes.class}, version = 1)
public abstract class AppDataBase extends RoomDatabase {
    public abstract PersonDao getPersonDao();
    
    public abstract ClothesDao getClothesDao();
}
複製代碼

5、Room:數據庫的建立者 & 負責數據庫版本更新的具體實現者

Room建立咱們的AppDataBase,咱們把它封裝成單例,省的每次都去執行一遍,耗性能

public class DBInstance {
    //private static final String DB_NAME = "/sdcard/LianSou/room_test.db";
    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(MyApplication.getInstance(),AppDataBase.class, DB_NAME)
                            //下面註釋表示容許主線程進行數據庫操做,可是不推薦這樣作。
                            //我這裏是爲了Demo展現,稍後會結束和LiveData和RxJava的使用
                            .allowMainThreadQueries()
                            .build();
                }
            }
        }
        return appDataBase;
    }
}
複製代碼

作完這一切,那麼咱們的準備工做就作完了。讓咱們來插入一條數據

Person person_ = new Person("Room", 18);
                DBInstance.getInstance().getPersonDao().insert(person_);
複製代碼

5.一、額外知識點

這裏怎麼查看db數據呢?首先咱們把db文件存在手機內存裏,記得打開存儲權限,就是在上面代碼裏指定路徑

private static final String DB_NAME = "/sdcard/LianSou/room_test.db";
插入數據後,就會在手機內存卡生成db文件。

拿到db文件,怎麼辦呢。用插件!!Database Navigator,插件教程


6、數據庫版本升級

這裏的意思好比我已經往person表存裏數據。可是我要增長字段,或者是增長索引。若是你直接寫上去,你會發現,你再使用數據庫的時候,會直接崩潰。怎麼辦呢,用過greendao的人都知道,咱們要升級數據庫版本

@Entity
public class Person {
    //...省略部分代碼,便於理解。
    //這裏給Person加上一個兒子
    
}

複製代碼

而後來到咱們的Database類裏,把版本信息改下,並增添一個Migration 類,告訴Room是哪張表改了什麼東西

//修改版本信息爲2
@Database(entities = {Person.class, Clothes.class}, version = 2)
public abstract class AppDataBase extends RoomDatabase {
    public abstract PersonDao getPersonDao();

    public abstract ClothesDao getClothesDao();

    //數據庫變更添加Migration,簡白的而說就是版本1到版本2改了什麼東西
    public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            //告訴person表,增添一個String類型的字段 son
            database.execSQL("ALTER TABLE person ADD COLUMN son TEXT");
        }
    };
}
複製代碼

關於版本更新的execSQL裏的用法,能夠參考Room升級。也能夠自行度娘,網上不少


最後來到咱們的Room裏:

public class DBInstance {
// private static final String DB_NAME = "/sdcard/LianSou/room_test.db";
    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){
                    return Room.databaseBuilder(MyApplication.getInstance(),AppDataBase.class, DB_NAME)
                            .allowMainThreadQueries()
                            //加上版本升級信息
                            .addMigrations(AppDataBase.MIGRATION_1_2)
                            .build();
                }
            }
        }
        return appDataBase;
    }
}
複製代碼

作完以上操做後,咱們來運行下項目看看。成功,打開數據看看(本文demo裏,我把升級代碼註釋了,想測試的可自行打開):


7、Room 結合RxJava使用(須要先了解RxJava的使用)

首先看咱們DBInstance裏的Room建立咱們的AppDataBase,這句代碼

//下面註釋表示容許主線程進行數據庫操做,可是不推薦這樣作。
.allowMainThreadQueries()
複製代碼

咱們,把這句代碼註釋掉,其餘不變,運行下代碼,看看。結果會報錯,報錯信息以下

Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.


這個時候,咱們來結合RxJava來使用下,這樣數據操做能夠放在子線程,回調能夠切換到主線程更改UI。首先是引入咱們的依賴

implementation 'android.arch.persistence.room:rxjava2:2.1.4'
    //下面這個是配合rxjava使用的
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
複製代碼

這裏須要注意2點:

一、在數據庫執行@Insert、@Delete、@Update操做時候可使用(注意是可使用)RxJava裏的類型有:Completable,Single,Maybe

二、在執行@Query操做時,能夠返回的類型有:Single,Maybe,Observable,Flowable
這裏須要注意:

  • 你想一次性查詢就用: Single,Maybe;這樣的話,若是查詢數據庫以後數據庫有改變時,後面不會有任何事務。
  • 若是你是想觀察數據庫: Observable,Flowable。那麼當已經查詢數據了,若是以後數據還有改變,那麼將自動執行Observable,Flowable裏觀察的代碼。意思就是對數據可持續觀察,實時顯示數據庫中最新的數據

這裏可能你們對Single,Maybe,Completable,Observable,Flowable不大瞭解,這裏作個簡單介紹:

一、Completable:只有onComplete和onError方法,即只有「完成」和「錯誤」兩種狀態,不會返回具體的結果。

二、Single:其回調爲onSuccess和onError,查詢成功會在onSuccess中返回結果,須要注意的是,若是未查詢到結果,即查詢結果爲空,會直接走onError回調,拋出EmptyResultSetException異常。

三、Maybe:其回調爲onSuccess,onError,onComplete,查詢成功,若是有數據,會先回調onSuccess再回調onComplete,若是沒有數據,則會直接回調onComplete。

四、Flowable/Observable:這是返回一個可觀察的對象,查詢的部分有變化時,都會回調它的onNext方法,沒有數據變化的話,不回調。直到Rx流斷開。


這裏爲了demo的清晰化,咱們再建一個Dog表。這裏申明一點,在數據庫執行這些操做的時候@Insert、@Delete、@Update,不能直接把返回類型寫成RxJava返回,類型,否則會直接報

錯誤: Methods annotated with @Insert can return either void, long, Long, long[], Long[] or List.

因此如今好多網上關於這部分,也沒有講清楚。若是有清楚的同窗請指正。請看Dao類:

@Dao
public interface DogDao {
    //返回值是插入成功的行id
    @Insert
    List<Long> insert(Dog... dogs);

    @Delete
    void delete(Dog... dogs);

    //返回刪除的行id
    @Delete
    int delete(Dog dog);


    @Update
    void update(Dog... dogs);

    @Update
    int update(Dog dog);


    //查詢全部對象 且 觀察數據。用背壓Flowable能夠實現,若是須要一次性查詢,能夠用別的類型
    @Query("Select * from dog")
    Flowable<List<Dog>> getAll();


    //刪除所有數據
    @Query("DELETE FROM dog")
    void deleteAll();

    
    //根據字段去查找數據
    @Query("SELECT * FROM dog WHERE id= :id")
    Single<Dog> getDogById(int id);

}
複製代碼

讓咱們在代碼裏,用可觀察的背壓,去實時查詢咱們的所有dog。這裏只要調用一次,以後數據有更新的時候,會自動走這個觀察者回調。

DBInstance.getInstance().getDogDao().getAll().subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<List<Dog>>() {
                    @Override
                    public void accept(List<Dog> dogs) throws Exception {
                        binding.txtAll.setText("當前狗狗總數" + dogs.size());
                    }
                });
複製代碼

那不少人問了。@Insert、@Delete、@Update這些該怎麼辦。不少博客都是把返回值寫在Dao裏。真實運行起來,直接報錯。因此這裏要在代碼中使用RxJava。用於項目的時候最好封裝起來。好比用Single插入數據:(這裏用哪一個類型呢,徹底根據你的需求而定,好比插入數據後,我要知道插入的行id的是多少,就不能用Completable,由於他沒有返回值,這個仍是靈活運用的)

Single.fromCallable(new Callable<List<Long>>() {
                    @Override
                    public List<Long> call() throws Exception {

                        Dog dog = new Dog();
                        return DBInstance.getInstance().getDogDao().insert(dog);
                    }
                }).subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(new SingleObserver<List<Long>>() {
                            @Override
                            public void onSubscribe(Disposable d) {


                            }
                            //一次插入多條數據,返回的行id的集合
                            @Override
                            public void onSuccess(List<Long> o) {
                                for (Long data : o) {
                                    LogUtils.i("使用Single插入數據", "onSuccess ==> " + data);
                                }

                            }

                            @Override
                            public void onError(Throwable e) {
                                LogUtils.i("使用Single插入數據", "onError");

                            }
                        });
複製代碼

若是你不須要觀察者回調,能夠直接。

Single.fromCallable(new Callable<List<Long>>() {
                    @Override
                    public List<Long> call() throws Exception {

                        Dog dog = new Dog();
                        return DBInstance.getInstance().getDogDao().insert(dog);
                    }
                }).subscribeOn(Schedulers.io())
                        .subscribe();
複製代碼

效果以下(查詢一次後,更新數據庫,都是獲得數據庫裏最新數據):


7、Room 結合 LiveData使用

這裏咱們在DogDao中添加LiveData的返回值,(查詢範圍id裏dog的值)

@Query("SELECT * FROM dog WHERE id>= :minId AND id<= :maxId")
    LiveData<List<Dog>> getToLiveData(int minId, int maxId);
複製代碼

Activity裏的代碼:

DBInstance.getInstance().getDogDao().getToLiveData(2, 12).observe(this, new Observer<List<Dog>>() {
                    @Override
                    public void onChanged(List<Dog> dogs) {
                        ToastUtils.showToast("查出來的當前值 ==> " + dogs.size());
                    }
                });
複製代碼

還記得咱們以前講的LiveData嗎。這個時候,LiveData跟隨生命週期的。onChanged只會在激活狀態下回調,若是銷燬了,那麼將會取消觀察者。

至此這裏對簡單的Room介紹完了。不得不說網上不少資料很無腦,處處是官網譯文。本文是做者本身的理解,若有錯誤請指正。看到這裏請給我點個贊吧

本文demo地址

相關文章
相關標籤/搜索