1. 程式人生 > >Android開發筆記(八十五)手機資料庫Realm

Android開發筆記(八十五)手機資料庫Realm

Realm應用背景

Android自帶的SQLite資料庫,在多數場合能夠滿足我們的需求,但隨著app廣泛使用,SQLite也暴露了幾個不足之處:
1、開發者編碼比較麻煩,而且還要求開發者具備SQL語法知識;
2、SQLite預設沒有加密功能,手機一旦丟失容易導致資料庫被破解;
3、SQLite底層採用java程式碼,導致效能提升存在瓶頸;


基於以上幾點,Android上的各種ORM應運而生(ORM全稱Object Relational Mapping,即物件關係對映),最常見的便是greenDAO了。greenDAO是一個將物件對映到SQLite資料庫中的ORM解決方案,它在github上的地址是https://github.com/greenrobot/greenDAO,下面是greenDAO相比直接使用SQLite的幾個改進點:
1、簡化資料庫操作的編碼,開發者可以不用熟悉SQL語法;
2、使用靈活,可在實體類中自定義類和列舉型別;
3、號稱是基於SQLite的ORM框架中效能最好的;(博主沒對比greenDAO與直接使用SQLite的效能差異,所以只能是跟其他ORM框架比較,比如ORMLite、sugarORM等等)
但是greenDAO使用的資料庫引擎還是SQLite,因此某些方面並沒有本質的改善,比如資料庫的加密、資料庫操作的效能等等。


對於Realm來說,這些改善就是可能的了,因為Realm有自己的資料庫引擎,而且引擎使用C++編寫,效能比java引擎的SQLite有數倍提升。Realm使用C++引擎還有一個好處,就是可以跨平臺使用,不但能用於Android,也能用於IOS。Realm的第三個好處是,它具有很多移動裝置專用資料庫的特性,比如支援JSON、流式api、資料變更通知,以及加密支援,這些都為開發者帶來了方便。


Realm環境搭建

Realm支援Eclipse的最後版本是0.87.5,更新的版本只支援AndroidStudio,不再支援Eclipse了,所以這裡就以0.87.5為例進行說明。


0.87.5的Realm下載頁面是https://realm.io/docs/java/0.87.5/#eclipse,github上最新版本的地址是https://github.com/realm/realm-java。把Realm加入到工程,除了引用realm-android-0.87.5.jar,還得加入armeabi目錄下的so檔案librealm-jni.so。現在編譯通過了,可是執行時又坑爹了,居然報錯“java.lang.IllegalArgumentException: Country is not part of the schema for this Realm”,原因是Realm採用了註解Annotation方式,所以得先讓我們的Eclipse支援註解才行。類似的情況,也存在於ButterKnife這個注入框架。


按照Realm官網的說明步驟,竟然發現我們最新的ADT,在“Properties”——“Java Compiler”下並沒有“Annotation Processing”。網上轉悠了一圈,找到了如下解決步驟:
1、依次選擇“Help”——“Install New Software”
2、下拉選擇Juno,即“Juno - http://download.eclipse.org/releases/juno”
3、在Name列表中點開“Programming Languages”,然後勾選“Eclipse Java Development Tools”(最新版本是3.8.2)
4、點選“Next”按鈕,執行安裝操作
5、安裝完畢重啟ADT,就可以在“Java Compiler”下找到“Annotation Processing”了


裝好Annotation外掛,只是萬里長征的第一步,接下來我們還得配置Eclipse,使之支援Annotation,具體步驟如下:
1、右擊我們的工程,依次選擇“Properties”——“Java Compiler”——“Annotation Processing”,勾選“Enable project specific settings”,並點選“Apply”按鈕,然後工程會重新編譯;
2、繼續開啟“Annotation Processing”——“Factory Path”,勾選“Enable project specific settings”,然後點選“Click Add JARs”按鈕,選擇工程libs目錄下的realm-android-0.87.5.jar,點選“OK”按鈕,然後工程又會重新編譯;
3、為了確保註解的處理器一直工作,我們得在所有RealmObject派生類的前一行加上註解:@RealmClass


另外,正式的app都會進行程式碼混淆處理,為了避免混淆操作影響Realm的使用,我們要在proguard-project.txt增加如下配置:
[java]
view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. -keep class io.realm.annotations.RealmModule  
  2. -keep @io.realm.annotations.RealmModule class *  
  3. -keep class io.realm.internal.Keep  
  4. -keep @io.realm.internal.Keep class * { *; }  
  5. -dontwarn javax.**  
  6. -dontwarn io.realm.**  
-keep class io.realm.annotations.RealmModule
-keep @io.realm.annotations.RealmModule class *
-keep class io.realm.internal.Keep
-keep @io.realm.internal.Keep class * { *; }
-dontwarn javax.**
-dontwarn io.realm.**


Realm編碼開發

資料庫配置RealmConfiguration

RealmConfiguration是Realm的配置工具類,它採用了建造者模式來構建,下面是RealmConfiguration類的常用方法:
Builder(context) : 初始化RealmConfiguration的建造器。
Builder.name : 指定資料庫的名稱。如不指定預設名為default。
Builder.encryptionKey : 指定資料庫的金鑰。金鑰可由SecureRandom的nextBytes方法獲得,如不指定金鑰則預設不加密。一旦建立加密的資料庫,如果訪問時金鑰不正確,則Realm會扔出異常“java.lang.IllegalArgumentException: Illegal Argument: Invalid format of Realm file”。
Builder.schemaVersion : 指定資料庫的版本號。如果不指定預設版本號為0,若原版本號與現版本號不一致,Realm會丟擲異常“io.realm.exceptions.RealmMigrationNeededException: RealmMigration must be provided”。
Builder.migration : 指定遷移操作的遷移類,當Realm發現新舊版本號不一致時,會自動使用該遷移類完成遷移操作。
Builder.deleteRealmIfMigrationNeeded : 宣告版本衝突時自動刪除原資料庫。
Builder.inMemory : 宣告資料庫只在記憶體中持久化。這意味著插入資料庫後不能立即關閉資料庫,因為一旦關閉資料庫則記憶體中的資料馬上丟失。若資料採用在檔案中持久化,則無需擔心關閉資料庫導致資料丟失的問題。

build : 完成配置構建。
getRealmFolder : 獲取資料庫的持有者,返回File物件。
getRealmFileName : 獲取資料庫的檔名字串。
getEncryptionKey : 獲取資料庫的加密金鑰。
getSchemaVersion : 獲取資料庫的版本號。
getMigration : 獲取遷移操作的遷移類。
shouldDeleteRealmIfMigrationNeeded : 判斷是否宣告版本衝突時自動刪除原資料庫。
getDurability : 返回資料持久化的方式


資料表物件RealmObject

RealmObject是資料表的實體基類,所有Realm的實體類都要從RealmObject派生而來。Realm實體類除了欄位宣告與set方法、get方法之外,還要加上若干必要的註解,舉例如下:
@RealmClass : 加在類名前面,表示這是一個Realm實體類。
@PrimaryKey : 加在欄位前面,表示該欄位是主鍵。
@Required : 加在欄位前面,表示該欄位非空。
@Ignore: 加在欄位前面,表示該欄位不是Realm表的欄位。因為有時我們需要處理一些額外的資訊,但又不需要把這些資訊儲存到資料庫。


下面是宣告一個實體類的程式碼例子:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. import io.realm.RealmObject;  
  2. import io.realm.annotations.Ignore;  
  3. import io.realm.annotations.PrimaryKey;  
  4. import io.realm.annotations.RealmClass;  
  5. import io.realm.annotations.Required;  
  6. @RealmClass
  7. publicclass Country extends RealmObject {  
  8.     @PrimaryKey
  9.     private String code;  
  10.     @Required
  11.     private String name;  
  12.     @Required
  13.     privateint population;  
  14.     @Ignore
  15.     private String remark;  
  16.     public Country() {  
  17.     }  
  18.     public String getCode() {  
  19.         return code;  
  20.     }  
  21.     publicvoid setCode(String code) {  
  22.         this.code = code;  
  23.     }  
  24.     public String getName() {  
  25.         return name;  
  26.     }  
  27.     publicvoid setName(String name) {  
  28.         this.name = name;  
  29.     }  
  30.     publicint getPopulation() {  
  31.         return population;  
  32.     }  
  33.     publicvoid setPopulation(int population) {  
  34.         this.population = population;  
  35.     }  
  36.     public String getRemark() {  
  37.         return remark;  
  38.     }  
  39.     publicvoid setRemark(String remark) {  
  40.         this.remark = remark;  
  41.     }  
  42. }  
import io.realm.RealmObject;
import io.realm.annotations.Ignore;
import io.realm.annotations.PrimaryKey;
import io.realm.annotations.RealmClass;
import io.realm.annotations.Required;

@RealmClass
public class Country extends RealmObject {

	@PrimaryKey
	private String code;

	@Required
	private String name;
	
	@Required
	private int population;

	@Ignore
	private String remark;

	public Country() {
	}

	public String getCode() {
		return code;
	}

	public void setCode(String code) {
		this.code = code;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getPopulation() {
		return population;
	}

	public void setPopulation(int population) {
		this.population = population;
	}

	public String getRemark() {
		return remark;
	}

	public void setRemark(String remark) {
		this.remark = remark;
	}

}


資料庫管理Realm

Realm是資料庫管理工具類,可完成DDL和DML操作,下面是Realm類的常用方法:
getInstance : 獲得一個數據庫例項。可傳入RealmConfiguration物件,若沒有傳入RealmConfiguration,則預設操作名為default.realm的資料庫檔案。
setDefaultConfiguration : 設定預設的RealmConfiguration配置。
deleteRealm : 刪除指定配置的資料庫。
isClosed : 判斷資料庫是否關閉。
close : 關閉資料庫。
beginTransaction : 開始事務,需與commitTransaction配合使用。
commitTransaction : 結束事務,需與beginTransaction配合使用。
createObject : 從RealmObject類建立一條資料庫記錄,後面直接使用該類的設定方法即可寫入欄位值。
copyToRealm : 把指定RealmObject類插入資料庫,如已存在主鍵相同的記錄則扔出異常。
copyToRealmOrUpdate : 把指定RealmObject類插入資料庫,如已存在主鍵相同的記錄則更新原記錄。
remove : 刪除指定資料庫記錄。
executeTransaction : 單獨對指定Realm執行事務,用於需要對事務失敗進行處理的場合。
where : 查詢指定表。返回RealmQuery物件。
distinct : 查詢指定表指定記錄的去重佇列。返回RealmResults佇列。


下面是Realm插入記錄的程式碼示例:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. mRealm = Realm.getInstance(mConfig);  
  2. mRealm.beginTransaction();  
  3. Country country1 = mRealm.createObject(Country.class);  
  4. country1.setName("北京");  
  5. country1.setPopulation(5165800);  
  6. country1.setCode("beijing");  
  7. Country country2 = new Country();  
  8. country2.setName("上海");  
  9. country2.setPopulation(5999800);  
  10. country2.setCode("shanghai");  
  11. mRealm.copyToRealm(country2);  
  12. Country country3 = new Country();  
  13. country3.setName("福州");  
  14. country3.setPopulation(876580);  
  15. country3.setCode("fuzhou");  
  16. mRealm.copyToRealmOrUpdate(country3);  
  17. mRealm.commitTransaction();  
  18. mRealm.close();  
			mRealm = Realm.getInstance(mConfig);
			mRealm.beginTransaction();

			Country country1 = mRealm.createObject(Country.class);
			country1.setName("北京");
			country1.setPopulation(5165800);
			country1.setCode("beijing");

			Country country2 = new Country();
			country2.setName("上海");
			country2.setPopulation(5999800);
			country2.setCode("shanghai");
			mRealm.copyToRealm(country2);

			Country country3 = new Country();
			country3.setName("福州");
			country3.setPopulation(876580);
			country3.setCode("fuzhou");
			mRealm.copyToRealmOrUpdate(country3);

			mRealm.commitTransaction();
			mRealm.close();


資料庫查詢RealmQuery

RealmQuery是資料庫查詢工具類,其物件由Realm的where方法獲得,下面是RealmQuery類的常用方法:

查詢條件
isNull : 指定欄位為空。
isNotNull : 指定欄位非空。
equalTo : 指定欄位等於多少。
notEqualTo : 指定欄位不等於多少。
greaterThan : 指定欄位大於多少。
greaterThanOrEqualTo : 指定欄位大等於多少。
lessThan : 指定欄位小於多少。
lessThanOrEqualTo : 指定欄位小等於多少。
between : 指定欄位位於什麼區間。
contains : 指定欄位包含什麼字串。
beginsWith : 指定欄位以什麼字串開頭。
endsWith : 指定欄位以什麼字串結尾。

返回結果集的運算結果
sum : 對指定欄位求和。
average : 對指定欄位求平均值。
min : 對指定欄位求最小值。
max : 對指定欄位求最大值。
count : 求結果集的記錄數量。
findAll : 返回結果集所有欄位,返回值為RealmResults佇列
findAllSorted : 排序返回結果集所有欄位,返回值為RealmResults佇列


下面是Realm查詢操作的程式碼示例:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. mRealm = Realm.getInstance(mConfig);  
  2. RealmResults<Country> results = mRealm.where(Country.class)  
  3.         .greaterThan("population"1000000).findAll();  
  4. String desc = String.format("找到%d條記錄", results.size());  
  5. for (int i = 0; i < results.size(); i++) {  
  6.     Country result = results.get(i);  
  7.     desc = String.format("%s\n其中城市%s的程式碼是%s,人口有%d", desc,  
  8.             result.getName(), result.getCode(),  
  9.             result.getPopulation());  
  10. }  
  11. tv_hello.setText(desc);  
  12. if (mRealm.isClosed() != true) {  
  13.     mRealm.close();  
  14. }  
			mRealm = Realm.getInstance(mConfig);
			RealmResults<Country> results = mRealm.where(Country.class)
					.greaterThan("population", 1000000).findAll();
			String desc = String.format("找到%d條記錄", results.size());
			for (int i = 0; i < results.size(); i++) {
				Country result = results.get(i);
				desc = String.format("%s\n其中城市%s的程式碼是%s,人口有%d", desc,
						result.getName(), result.getCode(),
						result.getPopulation());
			}
			tv_hello.setText(desc);
			if (mRealm.isClosed() != true) {
				mRealm.close();
			}


資料庫遷移RealmMigration

app升級時可能伴隨著資料庫升級,對於Realm來說,資料庫升級就是遷移操作,把原來的資料庫遷移到新結構的資料庫。編碼中應對資料庫遷移有三種方式:
1、構建RealmConfiguration時指定資料庫版本號,如果原版本號與現版本號不一致,Realm會丟擲異常RealmMigrationNeededException。程式碼中捕獲異常RealmMigrationNeededException後,呼叫migrateRealm方法執行遷移操作,示例程式碼如下:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. RealmConfiguration config0 = new RealmConfiguration.Builder(this)  
  2.         .name("default0").schemaVersion(3).build();  
  3. try {  
  4.     realm = Realm.getInstance(config0);  
  5. catch (RealmMigrationNeededException e) {  
  6.     e.printStackTrace();  
  7.     // You can then manually call Realm.migrateRealm().
  8.     Realm.migrateRealm(config0, new CustomMigration());  
  9.     realm = Realm.getInstance(config0);  
  10. }  
		RealmConfiguration config0 = new RealmConfiguration.Builder(this)
				.name("default0").schemaVersion(3).build();
		try {
			realm = Realm.getInstance(config0);
		} catch (RealmMigrationNeededException e) {
			e.printStackTrace();
			// You can then manually call Realm.migrateRealm().
			Realm.migrateRealm(config0, new CustomMigration());
			realm = Realm.getInstance(config0);
		}

2、構建RealmConfiguration時指定資料庫版本號,同時也指定遷移類,這樣如果原版本號與現版本號不一致,Realm會自動使用遷移類執行遷移操作。示例程式碼如下:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. RealmConfiguration config1 = new RealmConfiguration.Builder(this)  
  2.         .name("default1").schemaVersion(3)  
  3.         .migration(new CustomMigration()).build();  
  4. realm = Realm.getInstance(config1); // Automatically run migration if needed
		RealmConfiguration config1 = new RealmConfiguration.Builder(this)
				.name("default1").schemaVersion(3)
				.migration(new CustomMigration()).build();
		realm = Realm.getInstance(config1); // Automatically run migration if needed

3、構建RealmConfiguration時指定資料庫版本號,同時宣告版本衝突時自動刪除原資料庫,不過該方法一般不用,因為該方法會暴力刪除所有資料。示例程式碼如下:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. RealmConfiguration config2 = new RealmConfiguration.Builder(this)  
  2.         .name("default2").schemaVersion(3)  
  3.         .deleteRealmIfMigrationNeeded().build();  
  4. realm = Realm.getInstance(config2); // WARNING: This will delete all data in the Realm though.