前言
上篇我們分析了對于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的基本使用
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
還有兩個屬性onDelete
和onUpdate
@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。
異步查詢(返回LiveData
或RxJava 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(從入門到進階)
總結(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
感謝您的閱讀和支持!