? ? ? ?Room是一個對象關(guān)系映射(ORM)庫。Room抽象了SQLite的使用,可以在充分利用SQLite的同時訪問流暢的數(shù)據(jù)庫。
? ? ? ?Room官方文檔介紹 https://developer.android.com/training/data-storage/room/
? ? ? ?Room由三個重要的組件組成:Database、Entity、DAO。
- Database:包含數(shù)據(jù)庫持有者,并作為與應(yīng)用持久關(guān)聯(lián)數(shù)據(jù)的底層連接的主要訪問點(diǎn)。而且Database對應(yīng)的類必須滿足下面幾個條件:
? ? ? ?1. 必須是abstract類而且的extends RoomDatabase。
? ? ? ?2. 必須在類頭的注釋中包含與數(shù)據(jù)庫關(guān)聯(lián)的實(shí)體列表(Entity對應(yīng)的類)。
? ? ? ?3. 包含一個具有0個參數(shù)的抽象方法,并返回用@Dao注解的類。
? ? ? ?在運(yùn)行時,你可以通過Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()獲取Database實(shí)例。
Entity:代表數(shù)據(jù)庫中某個表的實(shí)體類。
DAO:包含用于訪問數(shù)據(jù)庫的方法。
? ? ? ?Room的使用也是需要添加依賴的
dependencies {
...
// add for room
implementation "android.arch.persistence.room:runtime:1.1.1"
// room 配合 RxJava
implementation "android.arch.persistence.room:rxjava2:1.1.1"
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
}
annotationProcessor 用于編譯期間根據(jù)注解(Annotation)獲取相關(guān)數(shù)據(jù)。
一、Entity(實(shí)體)
? ? ? ?每個Entity代表數(shù)據(jù)庫中某個表的實(shí)體類。默認(rèn)情況下Room會把Entity里面所有的字段對應(yīng)到表上的每一列。如果需要制定某個字段不作為表中的一列需要添加@Ignore注解。
@Entity
public class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
? ? ? ?比如上面的picture字段代碼因?yàn)槭褂昧薂Ignore所以該字段不會映射到User表中。
? ? ? ?Entity的實(shí)體類都需要添加@Entity注解。而且Entity類中需要映射到表中的字段需要保證外部能訪問到這些字段(你要么把字段什么為public、要不實(shí)現(xiàn)字段的getter和setter方法)。
? ? ? ?@Entity注解包含的屬性有:
- tableName:設(shè)置表名字。默認(rèn)是類的名字。
- indices:設(shè)置索引。
- inheritSuperIndices:父類的索引是否會自動被當(dāng)前類繼承。
- primaryKeys:設(shè)置主鍵。
- foreignKeys:設(shè)置外鍵。
1.1、設(shè)置表的名字
? ? ? ?默認(rèn)情況下Entity類的名字就是表的名字(不區(qū)分大小寫)。但是我們也可以通過@Entity的tableName屬性來自定義表名字。如下代碼所示users表對應(yīng)的實(shí)體類。
@Entity(tableName = "users")
public class User {
...
}
1.2,設(shè)置列的名字
? ? ? ?默認(rèn)情況下Entity類中字段的名字就是表中列的名字。我們也是可以通過@ColumnInfo注解來自定義表中列的名字。如下代碼users表中first_name列對應(yīng)firstName字段,last_name列對應(yīng)lastName字段。
@Entity(tableName = "users")
public class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
}
1.3、設(shè)置主鍵
? ? ? ?每個Entity都需要至少一個字段設(shè)置為主鍵。即使這個Entity只有一個字段也需要設(shè)置為主鍵。Entity設(shè)置主鍵的方式有兩種
- 通過@Entity的primaryKeys屬性來設(shè)置主鍵(primaryKeys是數(shù)組可以設(shè)置單個主鍵,也可以設(shè)置復(fù)合主鍵)。
@Entity(primaryKeys = {"firstName",
"lastName"})
public class User {
public String firstName;
public String lastName;
}
- 同@PrimaryKey注解來設(shè)置主鍵。
@Entity
public class User {
@PrimaryKey
public String firstName;
@PrimaryKey
public String lastName;
}
? ? ? ?如果你希望Room給entity設(shè)置一個自增的字段,可以設(shè)置@PrimaryKey的autoGenerate屬性。
1.4、設(shè)置索引
? ? ? ?數(shù)據(jù)庫索引用于提高數(shù)據(jù)庫表的數(shù)據(jù)訪問速度的。數(shù)據(jù)庫里面的索引有單列索引和組合索引。Room里面可以通過@Entity的indices屬性來給表格添加索引。
@Entity(indices = {@Index("firstName"),
@Index(value = {"last_name", "address"})})
public class User {
@PrimaryKey
public int id;
public String firstName;
public String address;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
? ? ? ?索引也是分兩種的唯一索引和非唯一索引。唯一索引就想主鍵一樣重復(fù)會報(bào)錯的。可以通過的@Index的unique數(shù)學(xué)來設(shè)置是否唯一索引。
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
public class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
1.5、設(shè)置外鍵
? ? ? ?因?yàn)镾QLite是關(guān)系形數(shù)據(jù)庫,表和表之間是有關(guān)系的。這也就是我們數(shù)據(jù)庫中常說的外鍵約束(FOREIGN KEY約束)。Room里面可以通過@Entity的foreignKeys屬性來設(shè)置外鍵。我們用一個具體的例子來說明。
正常情況下,數(shù)據(jù)庫里面的外鍵約束。子表外鍵于父表。當(dāng)父表中某條記錄子表有依賴的時候父表這條記錄是不能刪除的,刪除會報(bào)錯。一般大型的項(xiàng)目很少會采用外鍵的形式。一般都會通過程序依賴業(yè)務(wù)邏輯來保證的。
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
public class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
public class Book {
@PrimaryKey
public int bookId;
public String title;
@ColumnInfo(name = "user_id")
public int userId;
}
? ? ? ?上述代碼通過foreignKeys之后Book表中的userId來源于User表中的id。
? ? ? ?@ForeignKey屬性介紹:
entity:parent實(shí)體類(引用外鍵的表的實(shí)體)。
parentColumns:parent外鍵列(要引用的外鍵列)。
childColumns:child外鍵列(要關(guān)聯(lián)的列)。
onDelete:默認(rèn)NO_ACTION,當(dāng)parent里面有刪除操作的時候,child表可以做的Action動作有:
? ? ? ?1. NO_ACTION:當(dāng)parent中的key有變化的時候child不做任何動作。
? ? ? ?2. RESTRICT:當(dāng)parent中的key有依賴的時候禁止對parent做動作,做動作就會報(bào)錯。
? ? ? ?3. SET_NULL:當(dāng)paren中的key有變化的時候child中依賴的key會設(shè)置為NULL。
? ? ? ?4. SET_DEFAULT:當(dāng)parent中的key有變化的時候child中依賴的key會設(shè)置為默認(rèn)值。
? ? ? ?5. CASCADE:當(dāng)parent中的key有變化的時候child中依賴的key會跟著變化。onUpdate:默認(rèn)NO_ACTION,當(dāng)parent里面有更新操作的時候,child表需要做的動作。Action動作方式和onDelete是一樣的。
deferred:默認(rèn)值false,在事務(wù)完成之前,是否應(yīng)該推遲外鍵約束。這個怎么理解,當(dāng)我們啟動一個事務(wù)插入很多數(shù)據(jù)的時候,事務(wù)還沒完成之前。當(dāng)parent引起key變化的時候??梢栽O(shè)置deferred為ture。讓key立即改變。
1.6、創(chuàng)建嵌套對象
? ? ? ?有些情況下,你會需要將多個對象組合成一個對象。對象和對象之間是有嵌套關(guān)系的。Room中你就可以使用@Embedded注解來表示嵌入。然后,您可以像查看其他單個列一樣查詢嵌入字段。比如有一個這樣的例子,User表包含的列有:id, firstName, street, state, city, and post_code。這個時候我們的嵌套關(guān)系可以用如下代碼來表示。
public class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
@Entity
public class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
? ? ? ?@Embedded注解屬性:
- prefix:如果實(shí)體具有多個相同類型的嵌入字段,則可以通過設(shè)置前綴屬性來保持每個列的唯一性。然后Room會將提供的值添加到嵌入對象中每個列名的開頭。
二、DAO(數(shù)據(jù)訪問對象)
? ? ? ?這個組件代表了作為DAO的類或者接口。DAO是Room的主要組件,負(fù)責(zé)定義訪問數(shù)據(jù)庫的方法。Room使用過程中一般使用抽象DAO類來定義數(shù)據(jù)庫的CRUD操作。DAO可以是一個接口也可以是一個抽象類。如果它是一個抽象類,它可以有一個構(gòu)造函數(shù),它將RoomDatabase作為其唯一參數(shù)。Room在編譯時創(chuàng)建每個DAO實(shí)。
? ? ? ?DAO里面所有的操作都是依賴方法來實(shí)現(xiàn)的。
2.1、Insert(插入)
? ? ? ?當(dāng)DAO里面的某個方法添加了@Insert注解。Room會生成一個實(shí)現(xiàn),將所有參數(shù)插入到數(shù)據(jù)庫中的一個單個事務(wù)。
? ? ? ?@Insert注解可以設(shè)置一個屬性:
- onConflict:默認(rèn)值是OnConflictStrategy.ABORT,表示當(dāng)插入有沖突的時候的處理策略。OnConflictStrategy封裝了Room解決沖突的相關(guān)策略:
? ? ? ?1. OnConflictStrategy.REPLACE:沖突策略是取代舊數(shù)據(jù)同時繼續(xù)事務(wù)。
? ? ? ?2. OnConflictStrategy.ROLLBACK:沖突策略是回滾事務(wù)。
? ? ? ?3. OnConflictStrategy.ABORT:沖突策略是終止事務(wù)。
? ? ? ?4. OnConflictStrategy.FAIL:沖突策略是事務(wù)失敗。
? ? ? ?5. OnConflictStrategy.IGNORE:沖突策略是忽略沖突。
? ? ? ?一個簡單的實(shí)例如下:
@Dao
public interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertUsers(User... users);
}
? ? ? ?當(dāng)@Insert注解的方法只有一個參數(shù)的時候,這個方法也可以返回一個long,當(dāng)@Insert注解的方法有多個參數(shù)的時候則可以返回long[]或者r List<Long>。long都是表示插入的rowId。
2.2、Update(更新)
? ? ? ?當(dāng)DAO里面的某個方法添加了@Update注解。Room會把對應(yīng)的參數(shù)信息更新到數(shù)據(jù)庫里面去(會根據(jù)參數(shù)里面的primary key做更新操作)。
? ? ? ?@Update和@Insert一樣也是可以設(shè)置onConflict來表明沖突的時候的解決辦法。
@Dao
public interface UserDao {
@Update(onConflict = OnConflictStrategy.REPLACE)
int updateUsers(User... users);
}
? ? ? ?@Update注解的方法也可以返回int變量。表示更新了多少行。
2.3、Delete(刪除)
? ? ? ?當(dāng)DAO里面的某個方法添加了@Delete注解。Room會把對應(yīng)的參數(shù)信息指定的行刪除掉(通過參數(shù)里面的primary key找到要刪除的行)。
? ? ? ?@Delete也是可以設(shè)置onConflict來表明沖突的時候的解決辦法。
@Dao
public interface UserDao {
@Delete
void deleteUsers(User... users);
}
? ? ? ?@Delete對應(yīng)的方法也是可以設(shè)置int返回值來表示刪除了多少行。
2.4、Query(查詢)
? ? ? ?@Query注解是DAO類中使用的主要注釋。它允許您對數(shù)據(jù)庫執(zhí)行讀/寫操作。@Query在編譯的時候會驗(yàn)證準(zhǔn)確性,所以如果查詢出現(xiàn)問題在編譯的時候就會報(bào)錯。
? ? ? ?Room還會驗(yàn)證查詢的返回值,如果返回對象中的字段名稱與查詢響應(yīng)中的相應(yīng)列名稱不匹配的時候,Room會通過以下兩種方式之一提醒您:
- 如果只有一些字段名稱匹配,它會發(fā)出警告。
- 如果沒有字段名稱匹配,它會發(fā)生錯誤。
? ? ? ?@Query注解value參數(shù):查詢語句,這也是我們查詢操作最關(guān)鍵的部分。
2.4.1、簡單的查詢
? ? ? ?查詢所有的信息。
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
User[] loadAllUsers();
}
返回結(jié)果可以是數(shù)組,也可以是List。
2.4.2、帶參數(shù)的查詢
? ? ? ?大多數(shù)情況下我們都需要查詢滿足特定的查詢條件的信息。
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE firstName == :name")
User[] loadAllUsersByFirstName(String name);
}
? ? ? ?查詢需要多個參數(shù)的情況
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
User[] loadAllUsersBetweenAges(int minAge, int maxAge);
@Query("SELECT * FROM user WHERE firstName LIKE :search " + "OR lastName LIKE :search")
List<User> findUserWithName(String search);
}
2.4.3、查詢返回列的子集
? ? ? ?有的時候可能指向返回某些特定的列信息。
下來的例子只查詢user表中的firstName和lastName信息。
@Entity
public class User {
@PrimaryKey
public String firstName;
@PrimaryKey
public String lastName;
public int age;
}
public class NameTuple {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
@Dao
public interface UserDao {
@Query("SELECT firstName, lastName FROM user")
List<NameTuple> loadFullName();
}
2.4.4、查詢的時候傳遞一組參數(shù)
? ? ? ?在查詢的時候您可能需要傳遞一組(數(shù)組或者List)參數(shù)進(jìn)去。
@Dao
public interface UserDao {
@Query("SELECT firstName, lastName FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}
2.4.5、Observable的查詢
? ? ? ?意思就是查詢到結(jié)果的時候,UI能夠自動更新。Room為了實(shí)現(xiàn)這一效果,查詢的返回值的類型為LiveData。
@Dao
public interface UserDao {
@Query("SELECT firstName, lastName FROM user WHERE region IN (:regions)")
LiveData<List<NameTuple>> loadUsersFromRegionsSync(List<String> regions);
}
關(guān)于LiveData的具體用法,我們這里就不做過多的討論了。
2.4.6、使用RxJava作為查詢的返回值
? ? ? ?Room的查詢也可以返回RxJava2的Publisher或者Flowable對象。當(dāng)然了想要使用這一功能需要在build.gradle文件添加 implementation "android.arch.persistence.room:rxjava2:1.1.1" 依賴。
@Dao
public interface UserDao {
@Query("SELECT * from user")
Flowable<List<User>> loadUser();
}
? ? ? ?拿到Flowable<List<User>>之后就可以去調(diào)用
mAppDatabase.userDao()
.loadUser()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<User>>() {
@Override
public void accept(List<User> entities) {
}
});
2.4.7、查詢結(jié)果直接返回Cursor
? ? ? ?查詢結(jié)果直接返回cursor。然后通過cursor去獲取具體的結(jié)果信息。
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
Cursor loadRawUsersOlderThan(int minAge);
}
關(guān)于怎么從Cursor里面去獲取結(jié)果,大家肯定都非常熟悉。
2.4.8、多表查詢
? ? ? ?有的時候可能需要通過多個表才能獲取查詢結(jié)果。這個就涉及到數(shù)據(jù)的多表查詢語句了。
@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);
}
? ? ? ?也可以查詢指定的某些列。
@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;
}
}
三、Database(數(shù)據(jù)庫)
? ? ? ?@Database注解可以用來創(chuàng)建數(shù)據(jù)庫的持有者。該注解定義了實(shí)體列表,該類的內(nèi)容定義了數(shù)據(jù)庫中的DAO列表。這也是訪問底層連接的主要入口點(diǎn)。注解類應(yīng)該是抽象的并且擴(kuò)展自RoomDatabase。
? ? ? ?Database對應(yīng)的對象(RoomDatabase)必須添加@Database注解,@Database包含的屬性:
- entities:數(shù)據(jù)庫相關(guān)的所有Entity實(shí)體類,他們會轉(zhuǎn)化成數(shù)據(jù)庫里面的表。
- version:數(shù)據(jù)庫版本。
- exportSchema:默認(rèn)true,也是建議傳true,這樣可以把Schema導(dǎo)出到一個文件夾里面。同時建議把這個文件夾上次到VCS。
? ? ? ?在運(yùn)行時,你可以通過調(diào)用Room.databaseBuilder()或者Room.inMemoryDatabaseBuilder()獲取實(shí)例。因?yàn)槊看蝿?chuàng)建Database實(shí)例都會產(chǎn)生比較大的開銷,所以應(yīng)該將Database設(shè)計(jì)成單例的,或者直接放在Application中創(chuàng)建。
兩種方式獲取Database對象的區(qū)別:
- Room.databaseBuilder():生成Database對象,并且創(chuàng)建一個存在文件系統(tǒng)中的數(shù)據(jù)庫。
- Room.inMemoryDatabaseBuilder():生成Database對象并且創(chuàng)建一個存在內(nèi)存中的數(shù)據(jù)庫。當(dāng)應(yīng)用退出的時候(應(yīng)用進(jìn)程關(guān)閉)數(shù)據(jù)庫也消失。
? ? ? ?我們用一個簡單的實(shí)例來說明Database的創(chuàng)建。先定義一個abstract類AppDatabase繼承RoomDatabase。
@Database(entities = {User.class, Book.class}, version = 3)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
public abstract BookDao bookDao();
}
? ? ? ?創(chuàng)建RoomDatabase實(shí)例(AppDatabase)。這里我們把RoomDatabase實(shí)例的創(chuàng)建放在Application里面。
public class AppApplication extends Application {
private AppDatabase mAppDatabase;
@Override
public void onCreate() {
super.onCreate();
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build();
}
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
/**
* 數(shù)據(jù)庫版本 1->2 user表格新增了age列
*/
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE User ADD COLUMN age integer");
}
};
/**
* 數(shù)據(jù)庫版本 2->3 新增book表格
*/
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)");
}
};
}
? ? ? ?創(chuàng)建RoomDatabase實(shí)例的時候,RoomDatabase.Builder類里面主要方法的介紹:
/**
* 默認(rèn)值是FrameworkSQLiteOpenHelperFactory,設(shè)置數(shù)據(jù)庫的factory。比如我們想改變數(shù)據(jù)庫的存儲路徑可以通過這個函數(shù)來實(shí)現(xiàn)
*/
public RoomDatabase.Builder<T> openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory);
/**
* 設(shè)置數(shù)據(jù)庫升級(遷移)的邏輯
*/
public RoomDatabase.Builder<T> addMigrations(@NonNull Migration... migrations);
/**
* 設(shè)置是否允許在主線程做查詢操作
*/
public RoomDatabase.Builder<T> allowMainThreadQueries();
/**
* 設(shè)置數(shù)據(jù)庫的日志模式
*/
public RoomDatabase.Builder<T> setJournalMode(@NonNull JournalMode journalMode);
/**
* 設(shè)置遷移數(shù)據(jù)庫如果發(fā)生錯誤,將會重新創(chuàng)建數(shù)據(jù)庫,而不是發(fā)生崩潰
*/
public RoomDatabase.Builder<T> fallbackToDestructiveMigration();
/**
* 設(shè)置從某個版本開始遷移數(shù)據(jù)庫如果發(fā)生錯誤,將會重新創(chuàng)建數(shù)據(jù)庫,而不是發(fā)生崩潰
*/
public RoomDatabase.Builder<T> fallbackToDestructiveMigrationFrom(int... startVersions);
/**
* 監(jiān)聽數(shù)據(jù)庫,創(chuàng)建和打開的操作
*/
public RoomDatabase.Builder<T> addCallback(@NonNull RoomDatabase.Callback callback);
? ? ? ?RoomDatabase除了必須添加@Database注解也可以添加@TypeConverter注解。用于提供一個把自定義類轉(zhuǎn)化為一個Room能夠持久化的已知類型的,比如我們想持久化日期的實(shí)例,我們可以用如下代碼寫一個TypeConverter去存儲相等的Unix時間戳在數(shù)據(jù)庫中。
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();
}
}
四、數(shù)據(jù)遷移(升級)
? ? ? ?大部分情況下設(shè)計(jì)的數(shù)據(jù)庫在版本的迭代過程中經(jīng)常是會有變化的。比如突然某個表需要新加一個字段,需要新增一個表。這個時候我們又不想失去之前的數(shù)據(jù)。Room里面以Migration類的形式提供可一個簡化SQLite遷移的抽象層。Migration提供了從一個版本到另一個版本遷移的時候應(yīng)該執(zhí)行的操作。
? ? ? ?當(dāng)數(shù)據(jù)庫里面表有變化的時候(不管你是新增了表,還是改變了某個表)有如下幾個場景。
- 如果database的版本號不變。app操作數(shù)據(jù)庫表的時候會直接crash掉。(錯誤的做法)
- 如果增加database的版本號。但是不提供Migration。app操作數(shù)據(jù)庫表的時候會直接crash掉。(錯誤的做法)
- 如果增加database的版本號。同時啟用fallbackToDestructiveMigration。這個時候之前的數(shù)據(jù)會被清空掉。如下fallbackToDestructiveMigration()設(shè)置。(不推薦的做法)
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.fallbackToDestructiveMigration()
.build();
- 增加database的版本號,同時提供Migration。這要是Room數(shù)據(jù)遷移的重點(diǎn)。(最正確的做法)
? ? ? ?所以在數(shù)據(jù)庫有變化的時候,我們?nèi)魏螘r候都應(yīng)該盡量提供Migrating。Migrating讓我們可以自己去處理數(shù)據(jù)庫從某個版本過渡到另一個版本的邏輯。我們用一個簡單的實(shí)例來說明。有這么個情況,數(shù)據(jù)庫開始設(shè)計(jì)的時候我們就一個user表(數(shù)據(jù)庫版本 1),第一次變化來了我們需要給user表增加一個age的列(數(shù)據(jù)庫版本 2),過了一段時間又有變化了我們需要新增加一個book表。三個過程版本1->2->3。
? ? ? ?數(shù)據(jù)庫版本為1的時候的代碼
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
public class AppApplication extends Application {
private AppDatabase mAppDatabase;
@Override
public void onCreate() {
super.onCreate();
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.build();
}
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
}
? ? ? ?數(shù)據(jù)庫版本為2的時候的代碼,User增加了age列
@Entity
public class User {
@PrimaryKey(autoGenerate = true)
private long uid;
private String name;
private String address;
private String phone;
private Integer age;
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
@Database(entities = {User.class}, version = 2)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
public class AppApplication extends Application {
private AppDatabase mAppDatabase;
@Override
public void onCreate() {
super.onCreate();
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2)
.build();
}
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
/**
* 數(shù)據(jù)庫版本 1->2 user表格新增了age列
*/
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE user " + " ADD COLUMN age INTEGER");
}
};
}
? ? ? ?數(shù)據(jù)庫版本為3的時候的代碼,新增了一個Book表
@Entity
public class Book {
@PrimaryKey(autoGenerate = true)
private Long uid;
private String name;
private Date time;
private Long userId;
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
}
@Database(entities = {User.class, Book.class}, version = 3)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
public abstract BookDao bookDao();
}
public class AppApplication extends Application {
private AppDatabase mAppDatabase;
@Override
public void onCreate() {
super.onCreate();
mAppDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "android_room_dev.db")
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build();
}
public AppDatabase getAppDatabase() {
return mAppDatabase;
}
/**
* 數(shù)據(jù)庫版本 1->2 user表格新增了age列
*/
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE User ADD COLUMN age integer");
}
};
/**
* 數(shù)據(jù)庫版本 2->3 新增book表格
*/
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `book` (`uid` INTEGER PRIMARY KEY autoincrement, `name` TEXT , `userId` INTEGER, 'time' INTEGER)");
}
};
}
? ? ? ?Migrating使用過程中也有碰到一些坑,這里告誡大家一個經(jīng)驗(yàn)Entity中能用Integer的時候不用int。
五、數(shù)據(jù)庫信息的導(dǎo)出
? ? ? ?Room也允許你會將你數(shù)據(jù)庫的表信息導(dǎo)出為一個json文件。你應(yīng)該在版本控制系統(tǒng)中保存該文件,該文件代表了你的數(shù)據(jù)庫表歷史記錄,這樣允許Room創(chuàng)建舊版本的數(shù)據(jù)庫用于測試。只需要在build.gradle文件中添加如下配置。編譯的時候就會導(dǎo)出json文件。
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
// 用于測試
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
json文件文件會導(dǎo)出在工程目錄下的schemas文件夾下面。里面會有各個版本數(shù)據(jù)庫的信息。
? ? ? ?本文涉及到的例子下載地址
? ? ? ?以上對Room做了一個簡單的介紹,如果大家使用Room的過程中有什么疑問,也可以留言。