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包含以下三個重要組成部分:
詳細的結(jié)構(gòu)關(guān)系可以看下圖:
其實這和傳統(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驰弄,可以使用@PrimaryKey
的autoGenerate屬性。如果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è)置唯一性(這個表中的firstName
和lastName
不能同時相同):
@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)
定義了REMOVE
和REPLACE
而不是簡單的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中Publisher 和 Flowable
格式的數(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 都指定了startVersion
和endVersion
苗沧。在運行的時候Room運行每個 Migration 的migrate() 方法,按正確的順序來遷移數(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(八)