Room Persistence Library(官方介紹)
官方ORM(Object Relational Mapping)架構專題
Google官方推出的Android架構元件系列文章(六)Room持久化庫
Room 的官方API 可以檢視這裡

##介紹
Room是谷歌官方的資料庫ORM(對象關系映射)架構,使用起來非常友善。
Room提供了一個SQLite之上的抽象層,使得在充分利用SQLite功能的前提下順暢的通路資料庫。
對于需要處理大量結構化資料的App來說,把這些資料做本地持久化會帶來很大的好處。常見的用例是緩存重要資料塊。這樣當裝置無法連網的時候,使用者仍然可以浏覽内容。而使用者對内容做出的任何改動都在網絡恢複的時候同步到服務端。
##引入
###1、項目build.gradle中添加如下代碼倉庫
allprojects {
repositories {
jcenter()
google()
}
}
###2、app Module中引入
// Room依賴
implementation 'android.arch.persistence.room:runtime:1.1.0'
annotationProcessor "android.arch.persistence.room:compiler:1.1.0"
##組成
Room中有三個主要的元件:
###1、Database
資料庫元件,底層連接配接的主要入口,主要作用:
- 建立database holder
- 使用注解定義實體類
- 實體類定義了從資料庫中擷取資料的對象(DAO)
這個被注解的類是一個繼承RoomDatabase的抽象類。在運作時,可以通過調用Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()來得到它的執行個體。
###2、Entity
實體類元件, 一個類表示資料庫的一個表。
注意
- 1、你必須在Database類中的entities數組中引用這些entity類
- 2、entity中的每一個field都将被持久化到資料庫,除非使用了@Ignore注解。
###3、DAO
DAO查詢元件,DAO(Data Access Object) 資料通路對象是一個面向對象的資料庫接口。
DAO是Room的主要元件,負責定義查詢(添加或者删除等)資料庫的方法。
###4、示意圖
##示例代碼
###1、User.java ---- 實體類元件(Entity)
- **建表:**當一個類用@Entity注解并且被@Database注解中的entities屬性所引用時(
),Room就會在資料庫中為那個entity建立一張表。@Database(entities = {User.class}, version = 1)
- **表名:**Room預設把類名作為資料庫的表名,自定義表名需要使用@Entity注解的tableName屬性,@Entity(tableName = “users”)。
- **建列:**預設Room會為實體類中定義的每一個字段(field)都建立一個資料表列(column)。
- **列名:**預設使用字段名作為列名,如果想指定列名,可以使用 @ColumnInfo(name = “your_name”)。
- **持久化:**要持久化一個字段(字段資料寫入資料庫),Room必須有擷取它的管道。你可以把字段寫成public,也可以為它提供一個setter和getter。如果你使用setter和getter的方式,記住它們要基于Room的Java Bean規範。
- **忽略:**如果一個實體類中有你不想持久化的字段,那麼你可以使用@Ignore來注釋它們。
- **主鍵:**每個實體類Entity都必須至少定義一個field作為主鍵(primary key),主鍵自增需要使用@PrimaryKey的autoGenerate屬性。
- **組合主鍵:**需要使用@Entity注解的primaryKeys屬性,比如@Entity(primaryKeys = {“firstName”, “lastName”}),多個字段聯合形成一個主鍵組合,保證主鍵的唯一性
@Entity(tableName = "users")
public class User {
@PrimaryKey(autoGenerate = true)
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
@Ignore
Bitmap picture;
}
###2、UserDao.java ---- 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(User... users);
@Delete
void delete(User user);
}
###3、AppDatabase.java ---- 資料庫元件
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
###4、擷取database執行個體
擷取database執行個體的時候應該保持單例模式,因為資料庫的執行個體對記憶體的開銷是比較大的,而且程式内一般也不需要多個database的執行個體。
AppDatabase db = Room.databaseBuilder(getApplicationContext(),AppDatabase.class, "database-name").build();
##相關概念
###1、索引(Indices )
為了提高查詢的效率,可能給特定的字段建立索引。
要為一個entity添加索引,在@Entity注解中添加indices屬性,列出你想放在索引或者組合索引中的字段。
代碼示例:
@Entity(indices = {@Index("name"), @Index("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;
}
###2、唯一性(uniqueness)
指定某個字段或者幾個字段的值必須是唯一的,比如使用者名或手機号之類的賬戶唯一辨別字段。
可以通過把@Index注解的unique屬性設定為true來實作唯一性
@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;
}
###3、外鍵限制(Foreign Key)
一個表中的外鍵(Foreign Key) 指向另一個表中的主鍵(Primary Key),在更新和删除時起到限制的作用。比如,如果你想在删除主鍵表中的一條資料時可以同時删除外鍵限制表中相對應的資料,你可以在@ForeignKey注解中加上onDelete=CASCADE。
下面的代碼就指定了Book表中的user_id字段為User表的外鍵,與User表的id字段一一對應,使用Entity的foreignKeys屬性指定,寫法如下:
@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;
}
-
注意
**替換沖突『 @Insert(onConfilict=REPLACE) 』**不适用于外鍵限制,onConfilict不是單獨的sql指令,可以了解為一組REMOVE和REPLACE的操作,請參見SQLite文檔的ON_CONFLICT語句,onConfilict有如下五種沖突解決算法。
###4、對象嵌套
就是一個實體類中嵌入另一個實體類,可以多層嵌套。比如你在User中嵌套Address,如果你使用@Embedded注解Address的話,那麼User表中就擁有了Address的所有字段了。
為了防止多個實體嵌套造成字段重複,你可以通過設定prefix屬性來保持每列的唯一性。Room會将提供的值添加到嵌入對象的每個列名的開頭。
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
##資料查詢(DAO)
DAO抽象出了一種操作資料庫的簡便方法。下面介紹一下常見的查詢方式。
###1、新增、插入(Insert)
建立一個DAO方法并使用@Insert注解,Room就會在工作線程中将所有參數插入到資料庫。
如果@Insert方法僅僅接收一個參數,那它可以傳回一個long,表示插入項的rowId。如果參數是一個數組或集合,它會傳回long []或List。
@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);
}
###2、修改、更新(Update)
根據每個entity的主鍵作為更新的依據,此方法可以傳回一個int值,訓示資料庫中更新的行數。
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
###3、删除(Delete)
使用主鍵找到要删除的entity,此方法可以傳回一個int值,訓示資料庫中被删除的行數。
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
###4、查詢(Query)
@Query(查詢)是
DAO
類中使用的主要注解。可以讓你執行資料庫讀/寫操作。每個
@Query
方法都會在編譯時驗證,如果查詢語句有問題,會發生編譯錯誤而不是運作時故障。
Room還會檢查查詢的傳回值,如果傳回的對象字段名和查詢結果的相應字段名不比對,Room将以下面兩種方式提醒你:
- 如果是某些字段名不比對會給出警告。
- 如果沒有比對的字段名則會給出錯誤提示。
####4-1、簡單查詢
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
####4-2、條件查詢 (傳參)
注意查詢方法的入參在查詢語句中的寫法“:minAge”,另外,在使用in操作符進行查詢時,别忘記加上“()”哦!
@Dao
public interface MyDao {
//查詢User表中年齡大于入參的資料集合
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
//查詢User表中年齡介于入參之間的資料集合
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
//查詢User表中名字中入參關鍵字的資料集合
@Query("SELECT * FROM user WHERE first_name LIKE :search "
+ "OR last_name LIKE :search")
public List<User> findUserWithName(String search);
//查詢User表中id符合入參集合中的資料集合
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
}
####4-3、部分查詢
有時候我們僅僅需要的是資料表中的部分資料,這個時候我們可以指定DAO的查詢方法隻傳回我們需要的字段,這樣不僅節約資源而且查詢更快。
方法是在查詢語句中指定需要擷取的字段,然後指定對應的實體類來擷取查詢的傳回資料。
例如,User的實際字段有如下四個,但是我們隻需要其中的first_name和last_name,那麼我們可以重新定義一個實體類UserName,然後在查詢方法中指定隻查詢first_name和last_name字段,并使用UserName實體來擷取查詢語句的傳回資料。
注:這些“裁剪”的實體類也是可以使用@Embedded注解的。
@Entity(tableName = "users")
public class User {
@PrimaryKey(autoGenerate = true)
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
@Ignore
Bitmap picture;
}
public class UserName{
@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();
}
####4-4、原始查詢
RawQuery
我們可以利用
RawQuery
進行原始SQL語句查詢,示例代碼:
@Dao
interface RawDao {
@RawQuery
User getUserViaQuery(SupportSQLiteQuery query);
}
SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1", new Object[]{userId});
User user2 = rawDao.getUserViaQuery(query);
1、Room将根據函數的傳回類型("User ")生成代碼,如果未能通過正确的查詢,将導緻運作時異常。
2、RawQuery方法隻能用于讀取查詢。對于寫入查詢,請使用
RoomDatabase.getOpenHelper().getWritableDatabase()
###5、可觀察的查詢(Observable queries)
####5-1、Query
當執行查詢的時候,你通常希望app的UI能自動在資料更新的時候更新。為此,在query方法中使用 LiveData 類型的傳回值。當資料庫變化的時候,Room會生成所有的必要代碼來更新LiveData。
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public LiveData<List<User>> loadAllUsersBetweenAges(int minAge, int maxAge);
}
使用步驟:
- 建立一個LiveData執行個體來儲存某種類型的資料。 這通常在您的ViewModel類中完成。
public class UserViewModel extends ViewModel {
private LiveData<List<User>> mUsers;
public LiveData<List<User>> getUsers() {
if (mUsers== null) {
mUsers= MyDao.loadAllUsersBetweenAges(10,30);
}
return mUsers;
}
...
}
- 建立一個Observer對象,該對象定義onChanged()方法,該方法在LiveData對象的資料發生變化時回調, 通常是在UI控制器中建立Observer對象,比如Activity或Fragment。
public class UserActivity extends AppCompatActivity {
private UserViewModel mModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mModel = ViewModelProviders.of(this).get(UserViewModel.class);
final Observer<List<User>> userObserver = new Observer<List<User>>() {
@Override
public void onChanged(@Nullable List<User> users) {
//update the ui
}
};
mModel.getCurrentName().observe(this, userObserver);
}
}
- 使用observe()方法将Observer對象附加到LiveData對象。 observe()方法使用LifecycleOwner對象。 這将Observer對象訂閱到LiveData對象,以便通知其更改。 您通常将Observer對象附加到UI控制器中,比如Activity或Fragment。
####5-2、RawQuery
RawQuery方法可以傳回可觀察的類型,但您需要使用注釋中的observedEntities()字段指定在查詢中通路哪些表。
代碼示例:
@Dao
interface RawDao {
@RawQuery(observedEntities = User.class)
LiveData<List<User>> getUsers(SupportSQLiteQuery query);
}
LiveData<List<User>> liveUsers = rawDao.getUsers(
new SimpleSQLiteQuery("SELECT * FROM User ORDER BY name DESC"));
##Rxjava
Room還可以讓你定義的查詢傳回RxJava2的Publisher和Flowable對象。要使用這個功能,在Gradle dependencies中添加android.arch.persistence.room:rxjava2。然後你就可以傳回RxJava2中定義的對象類型了,如下面的代碼所示:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
##Cursor
如果你的app需要直接獲得傳回的行,你可以在查詢中傳回Cursor對象。但是非常不鼓勵使用Cursor ,因為它無法保證行是否存在,或者行包含什麼值。
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
##多表查詢
一些查詢可能要求查詢多張表來計算結果。Room允許你書寫任何查詢,是以表連接配接(join)也是可以的。
而且如果響應是一個可觀察的資料類型,比如Flowable或者LiveData,Room将觀察查詢中涉及到的所有表,檢測出所有的無效表。
下面的代碼示範了如何執行一個表連接配接查詢來查出借閱圖書的user與被借出圖書之間的資訊。
**邏輯:**入參userName ---- (user.name LIKE :userName) ----> user ---- (user.id = loab.userid) ----> loab ---- (loan.book_id = book.id) ----> book
**語句:**book <---- (loan.book_id = book.id) ---- loab <---- (user.id = loab.userid) ---- user <---- (user.name LIKE :userName) ---- userName
@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);
}
- INNER JOIN:内連接配接,顯示左表和右表符合連接配接條件的記錄
- JOIN: 如果表中有至少一個比對,則傳回行
- LEFT JOIN: 即使右表中沒有比對,也從左表傳回所有的行
- RIGHT JOIN: 即使左表中沒有比對,也從右表傳回所有的行
- FULL JOIN: 隻要其中一個表中存在比對,就傳回行
##類型轉換
1、Room對Java的基本資料類型以及其包裝類型都提供了支援
2、但是有時候你可能使用了一個自定義的資料類型,并且你想将此類型的資料存儲到資料庫表中的字段裡。
為了實作自定義資料類型的轉換,你需要一個類型轉換器TypeConverter,它将負責處理自定義資料類和Room可以儲存的已知類型之間的轉換。
比如,如果我們想要儲存Date執行個體,那麼第一步
- 寫一個TypeConverter類,實作Date類型和Long類型的資料轉換
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類型和Long類型的資料互相轉換。
-
使用TypeConverter類,實作持久化Date類型的資料
這裡就需要把這個轉換類
添加到我們的資料庫元件Converters
中了AppDatabase
@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
資料庫更新
-
基本使用
實作就是在對應的Database中通過Migration進行更新,使用的方式是:
1、利用Migration 建構資料庫更新語句
//資料庫更新
private static final Migration migration_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
}
};
2、在資料庫的構造方法中通過addMigrations()方法傳入對應的Migration
public synchronized static AppDatabase getInstance(byte[] passphrase) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room
.databaseBuilder(mContext.getApplicationContext(), AppDatabase.class, sDbPath)
.openHelperFactory(new HelperFactory(passphrase))
.allowMainThreadQueries()
.addMigrations(migration_1_2)
.build();
}
}
}
return INSTANCE;
}
3、資料庫版本(version )+1
@Database(entities = {MarkBook.class}, version = 2, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
....
**總結:**就是把資料庫的變化通過SQL語句傳到資料庫的構造方法中。
- 常用的幾種資料庫更新
新增表
//資料庫更新
private static final Migration migration_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS SYMBOL_SETTING (_id INTEGER primary key NOT NULL, dbName TEXT, symbolJson TEXT)");
}
};
增加字段
//資料庫更新
private static final Migration migration_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE SURVEY_RECORD ADD STATUS INTEGER DEFAULT 0");
}
};
資料庫多次更新
//資料庫更新,SURVEY_RECORD新增SJBZ、SZFHZZZB字段
private static final Migration migration_2_3 = new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE SURVEY_RECORD ADD SJBZ TEXT");
database.execSQL("ALTER TABLE SURVEY_RECORD ADD SZFHZZZB TEXT");
}
};
//資料庫更新
private static final Migration migration_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE SURVEY_RECORD ADD STATUS INTEGER DEFAULT 0");
}
};
public synchronized static TaskDatabase getInstance(byte[] passphrase) {
if (INSTANCE == null) {
synchronized (TaskDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room
.databaseBuilder(mContext.getApplicationContext(), TaskDatabase.class, sDbPath)
.openHelperFactory(new HelperFactory(passphrase))
.allowMainThreadQueries()
.addMigrations(migration_1_2,migration_2_3)
.build();
}
}
}
return (INSTANCE);
}
**備注:**資料庫更新的時候最好文字說明一下這次更新了什麼,不要因為有SQL語句就不寫注解了,這樣不好。