前言
Android
的ORM
框架有很多,比如Realm
,greenDAO
,LitePal
,DBFlow
,afinal
,SugarORM
,ORMLite
和LiteORM
,還有Anko
的ManagedSqliteOpenHelper
。其中Realm
和GreenDAO
在2017年百大框架排行榜里面排名最高,27名和28名。下面就來簡單的說說這些框架并且說說Realm
的使用和封裝。
ORM框架
如果使用Android SQLite
創建一個數據庫需要實現下面的步驟:
- 創建一個
DBHelper
類實現SqliteOpenHelper
,傳入context
,數據庫名稱和初始版本,并實現OnCreate
和OnUpgrade
方法 - 實現
Dao
層,通過getReadableDatabase
和getWritableDatabase
結合ContentValue
和Cursor
實現增刪修改
原生操作復雜,寫SQL
語句容易出錯。各種ORM
的出現,使它的變得操作更加簡單。
Realm
- 數據庫大?。?52kb左右)
- 改用
C++
實現的數據庫存儲引擎 - 支持
JSON
,流式API
,數據變更通知,自動數據同步,訪問控制,事件處理,簡單的數據庫加密 - 操作比原生
Android ORM
快 - 不支持多庫和
SQL
語句執行 - 跨平臺,支持
Java
,OC
,Swift
,RN
,JS
等等
Github
路徑Realm-Java,下面進一步講解具體使用
GreenDAO
- 輕量級數據庫(<150kb)
- 快,可能是Android最快的
ORM
- 可通過“生成器工程”生成
DaoMaster
,DaoSession
,對應的數據表和Dao
層 - 通過
QueryBuilder
操作,查詢得到需要的表數據 - 支持跨實體查詢
- 支持數據庫加密
使用教程和源碼參考greenDAO-Github
LitePal
- 輕量級數據庫(大小176kb左右)
-
xml
形式配置數據庫名,數據庫版本號和數據庫表 - 1.4.0版本以上支持多庫
-
API
操作簡單,支持原生SQL
語句執行 - 不支持打開自建數據庫的,需使用用
litepal
創建
更多功能和使用文檔查看郭霖大神的博客以及LitePal-Github
DBFlow
- 輕量級數據庫(大小70kb左右)
- 性能高,不是基于反射
- 操作速度快,基于
annotationProcessor
- 使用
apt
技術,在編譯過程中生成操作類 - 支持多個數據庫
- 可基于
ModelContainer
類解析像JSON
這樣的數據
使用文檔參考DBFlow和DBFlow-Github
afinal
- 輕量級數據庫(大小153kb左右)
- 具有
xml
的id綁定和事件綁定功能 - 網絡圖片的顯示功能(里面包含了強大的緩存框架)
- 數據庫sqlite的操作功能,通過
FinalDb
一行代碼即可搞定 - http數據的讀取功能(支持ajax方式讀?。?/li>
使用文檔參考afinal和afinal-Github
SugarORM
- 輕量級數據庫(大小93kb左右)
- 集成簡單,
API
使用簡單 - 通過反射自動創建表和列命名
- 支持表的一對多
使用文檔參考SugarORM-Github
ORMLite
- 輕量級數據庫(大小388kb左右)
- 繼承
OrmLiteOpenHelper
實現 - 需要通過
TableUtils
手動創建數據庫表和處理數據庫升級 - 通過注解方式映射數據庫表
- 獲取
Dao
對象進行增刪修改
使用文檔參考鴻洋大神介紹介紹ORMLite博客和ORMLite-Android-Github
LiteORM
- 輕量級的數據庫(大小122kb左右)
- 比原生數據庫快1倍,
Github
上有測試數據 - 無須額外配置,自動檢測升級數據庫版本和
Model
的變化 - 支持多庫
-
API
操作簡單,支持save(replace), insert, update, delete, query, mapping
等等操作 - 查詢支持
in, columns, where, order, limit, having group
- 不支持原生
SQL
數據庫的執行,貌似最近沒有維護了
更多功能和使用文檔查看LiteORM-Github
Anko-SQLite
通過kotlin
+anko
簡化了創建原生Android
數據庫表操作,詳情使用文檔參考Anko-SQLite
從Github
的Star
來說,則Realm
和Green
占優勢,同時這兩個的功能十分強大。
從ORM
庫大小來說,則GreenDao
,LitePal
和LiteORM
等輕量級的占優勢。
從ORM
的使用配置簡單程度來說,則LitePal
,afinal
和LiteORM
占優勢。
綜上所述,從穩定性,安全性,功能的強大性選Realm
,GreenDao
,ORMLite
似乎更好,從輕量程度性,配置簡單化來說選LitePal
,afinal
,LiteORM
,SugarORM
和DBFlow
似乎更好。當然,如果想不依賴框架,使用Anko-SQLite
來實現就再好不過了。
Realm基礎
集成
project
里面的build.gradle
加入
classpath "io.realm:realm-gradle-plugin:4.1.1"
然后在app
的build.gradle
加入
apply plugin: 'realm-android'
同時在defaultConfig
里面加入
ndk{ abiFilters "armeabi"}
可減小Realm
庫的大小
數據庫表
下面簡單定義一個User
表
public class User extends RealmObject {
@PrimaryKey
private int id;
private String name;
private int age;
@Ignore
private int sessionId;
public boolean IsEmptyName(){
return name.isEmpty();
}
//----------下面是Set和Get方法,此處省略-----------
}
RealmObject
是一個抽象類,如果想使用接口形式,使用RealmModel
和注解@RealmClass
也是同樣的效果。屬性添加@PrimaryKey
注解即表示表的主鍵,使用@Ignore
即表示該屬性不添加到庫里面,同時也可以在User
表里面添加Public
和Protected
方法。如上面的IsEmptyName
方法,所以幾乎可以把User
表當PoJo
來使用
初始化Realm
在Application
里面的onCreate
方法里面執行
Realm.init(this)
增刪修改操作
結合上篇MVP
的封裝以及上面User
表,實現下圖效果。
- 同步增加
mvpView.mRealm.beginTransaction()
mvpView.mRealm.copyToRealmOrUpdate(createUser())
mvpView.mRealm.commitTransaction()
或者
mvpView.mRealm.executeTransaction {
realm ->
realm.copyToRealmOrUpdate(createUser())
}
其中mvpView.mRealm
是在BaseActivity/BaseFragment
實例化的一個Realm
即
val mRealm = Realm.getDefaultInstance()
- 異步增加
mvpView.mRealm.executeTransactionAsync({
it.copyToRealmOrUpdate(createUser())
},{},{}).bindTo(mvpView.realmAsyncList)
executeTransactionAsync
分別對應execute
,OnSuccess
和OnError
,其中OnSuccess
和OnError
也可不回調。
- 異步查詢
val results = mvpView.mRealm.where(User::class.java).equalTo("name","Android").findAllAsync()
mvpView.UpdateUI(results)
這種寫法類似Java
的Future
,查詢將會在后臺線程中被執行,當其完成時,之前返回的 RealmResults 實例會被更新。
- 刪除
val results = findAllUser()
mvpView.mRealm.executeTransaction {
_ ->
results.deleteFirstFromRealm()
}
- 刪除全部
results.deleteAllFromRealm()
最后的Presenter
就如下
class RealmPresenter:BasePresenter<RealmFragment>() {
private val names = arrayOf("Android","Java","Kotlin","JS","PHP")
private val idCount:AtomicInteger = AtomicInteger(0)
//同步增加或者修改
fun syncAddOrUpdateItem(){
//第一種方式,自己手動管理事務
mvpView.mRealm.beginTransaction()
mvpView.mRealm.copyToRealmOrUpdate(createUser())
mvpView.mRealm.commitTransaction()
mvpView.UpdateUI(findAllUser())
/*
//第二種方式,Realm自動管理事務
mvpView.mRealm.executeTransaction {
realm ->
realm.copyToRealmOrUpdate(createUser())
mvpView.UpdateUI(findAllUser())
}*/
}
//異步增加或者修改
fun asyncAddOrUpdateItem(){
mvpView.mRealm.executeTransactionAsync({
it.copyToRealmOrUpdate(createUser())
},{
mvpView.UpdateUI(findAllUser())
},{
}).bindTo(mvpView.realmAsyncList)
}
//異步查詢
fun asyncQueryItem(){
val results = mvpView.mRealm.where(User::class.java).equalTo("name","Android").findAllAsync()
mvpView.UpdateUI(results)
}
//刪除
fun removeItem(){
val results = findAllUser()
mvpView.mRealm.executeTransaction {
_ ->
results.deleteFirstFromRealm()
if(idCount.get()>1) idCount.decrementAndGet()
mvpView.UpdateUI(results)
}
}
//清除
fun removeAll(){
val results = findAllUser()
mvpView.mRealm.executeTransaction {
_ ->
results.deleteAllFromRealm()
idCount.set(0)
mvpView.UpdateUI(results)
}
}
private fun createUser():User{
val user = User()
user.id = idCount.get()
user.name = names[(Math.random()*4).toInt()]
user.age = (Math.random()*10).toInt()
idCount.incrementAndGet()
return user
}
//同步查詢
private fun findAllUser():RealmResults<User>{
return mvpView.mRealm.where(User::class.java).findAll()
}
}
JSON
Realm
是支持json
數據的,可以通過String
,InputStream
,JsonObject
直接傳入保存到對應的表里面
三種方式如下:
舉個例子,存儲一個全國城市列表的json
到City
表里面
首先準備一個city.json
文件放在raw
目錄下面,json
格式如下
[
{
"area": "010",
"code": "110000",
"level": "1",
"name": "北京市",
"prefix": "市"
},
{
"area": "010",
"code": "110101",
"level": "2",
"name": "東城區",
"prefix": "區"
},
{
"area": "010",
"code": "110102",
"level": "2",
"name": "西城區",
"prefix": "區"
},
//省略.....
]
然后我們定義一個City
表
public class City extends RealmObject {
@PrimaryKey
private String code;
private String area;
private String level;
private String name;
private String prefix;
//省略Get和Set方法
}
這里封裝一個創建Realm
對象的幫助類,支持多Realm
首先抽象出變化需要配置的RealmConfiguration
的參數,定義一個IRealmMigrate
接口,
interface IRealmMigrate {
fun src():InputStream?
fun realmName():String
fun schemaVersion():Int
fun migration(): RealmMigration
}
其中
src()
指的是需要本地資源遷移的時候傳入的InputStream
,
realmName()
指的是realm
自定義的realm
后綴的文件,默認存儲在data/data/<packagename>/file/
路徑下,
schemaVersion()
默認的數據庫版本,
migration()
實現RealmMigration
對升級數據庫的一些操作
實現一個RealmHelper
幫助類
object RealmHelper {
private val mMigrationMap:SparseArrayCompat<Realm> = SparseArrayCompat()
fun getRealmInstance(migration:IRealmMigrate):Realm{
val key = migration.realmName().hashCode()
var mRealm:Realm? = mMigrationMap.get(key)
if(mRealm==null||mRealm.isClosed||mMigrationMap.indexOfKey(key)<0){
val migrationConfig = RealmConfiguration.Builder()
.name(migration.realmName())
.schemaVersion(migration.schemaVersion().toLong())
.migration(migration.migration())
.build()
if(migration.src()!=null){
val file = File(migrationConfig!!.path)
if (!file.exists()||file.length() == 0L) {
file.delete()
file.createNewFile()
file.outputStream().use { out -> migration.src()!!.use { it.copyTo(out) } }
}
}
mRealm = Realm.getInstance(migrationConfig)
mMigrationMap.put(key,mRealm)
}
return mRealm!!
}
fun clear(){
mMigrationMap.clear()
}
}
外部傳入IRealmMigrate
,然后對RealmConfiguration
進行設置,同時通過SparseArrayCompat
進行保存,另外當退出app
的時候調用clear()
方法。
最后在application
里面調用initCity()
方法
private fun initCity():App{
val inStream = this.resources.openRawResource(R.raw.city)
val mRealm = RealmHelper.getRealmInstance(AppRealmMigrateImpl())
mRealm.use { it ->
it.executeTransaction {
realm ->
realm.createOrUpdateAllFromJson(City::class.java,inStream)
}
}
return this
}
這樣通過調用realm.createAllFromJson(xx)
對應的City
表就會有city.json
里面的數據了。在使用的時候通過查表就可以查到對應的城市數據。City
表查詢結果
注意創建一個Realm
,對應就要close
一次。所以上文的BaseActivity/BaseFragment
的Realm
可以改成
val mRealm = RealmHelper.getRealmInstance(AppRealmMigrateImpl())
然后在OnDestroy()
里面進行close
就行了。
遷移/升級
當數據庫表發生變化的時候,如果不進行處理,Realm
會拋出類似下面這樣的錯誤
io.realm.exceptions.RealmMigrationNeededException: Field count is less than expected - expected 4 but was 3
所以要對數據進行遷移,也就是數據庫升級
這里對上面的User
表增加一個字段
public class User extends RealmObject {
//同上...
@Required
private Integer sex;
//省略Get和Set方法
}
其中@Required是指sex
不能為null
,然后定義一個方法實現RealmMigration
@Suppress("INACCESSIBLE_TYPE")
class AppMigration: RealmMigration {
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
val schema = realm.schema as RealmSchema
if(oldVersion==0L&&newVersion==1L){
val userSchema = schema.get(User::class.java.simpleName)
if(userSchema!=null&&!userSchema.hasField("sex")){
userSchema.addField("sex",Int::class.java,FieldAttribute.REQUIRED)
}
oldVersion.plus(1)
}
}
}
同時對User
表增加數據的時候,需要設置sex
的值。這樣就ok了。
不過數據遷移的時候只能讀取Realm
后綴的文件,例如db
文件貌似不支持。
坑
結合
RxJava
使用的時候,Realm
只能在創建Realm
的線程使用,不能切換線程進行使用異常
Configurations cannot be different if used to open the same file. The most likely cause is that equals() and hashCode() are not overridden in the migration class: com.data.lib.impl.AppMigration
RealmConfiguration
相同的情況下,Realm.getInstance(migrationConfig)
不能獲取兩次
@Required annotation is unnecessary for primitive field "xxx".
只有Boolean, Byte, Short, Integer, Long, Float, Double, String, byte[], Date
這些數據類型才支持@Required
-
Realm.getDefaultConfiguration()
在模擬器上面會報錯,真機不會
總結
關于Realm
的加密功能,異步線程監聽功能,集合通知的一些注意事項,結合Gson
使用等等之類的功能都可以參考Realm
文檔
Realm
是一批好馬,操作查詢之類的確實是相當的快, 但是需要馴服得熟讀Realm
文檔。
最后給出一張各個ORM
的性能圖
[圖片上傳失敗...(image-3a9bcc-1510804091717)]
.png](http://upload-images.jianshu.io/upload_images/2148217-03cbb53711e69a07.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
可以參考參考。