Android搭建应用框架系列之ORM数据库

前言

AndroidORM框架有很多,比如RealmgreenDAOLitePalDBFlowafinalSugarORMORMLiteLiteORM,还有AnkoManagedSqliteOpenHelper。其中RealmGreenDAO在2017年百大框架排行榜里面排名最高,27名和28名。下面就来简单的说说这些框架并且说说Realm的使用和封装。

ORM框架

如果使用Android SQLite创建一个数据库需要实现下面的步骤:

  • 创建一个DBHelper类实现SqliteOpenHelper,传入context,数据库名称和初始版本,并实现OnCreateOnUpgrade方法
  • 实现Dao层,通过getReadableDatabasegetWritableDatabase结合ContentValueCursor实现增删修改

原生操作复杂,写SQL语句容易出错。各种ORM的出现,使它的变得操作更加简单。

Realm

  • 数据库大小(152kb左右)
  • 改用C++实现的数据库存储引擎
  • 支持 JSON,流式API,数据变更通知,自动数据同步,访问控制,事件处理,简单的数据库加密
  • 操作比原生Android ORM
  • 不支持多库和SQL语句执行
  • 跨平台,支持JavaOCSwiftRNJS等等

Github路径Realm-Java,下面进一步讲解具体使用

GreenDAO

  • 轻量级数据库(<150kb)
  • 快,可能是Android最快的ORM
  • 可通过“生成器工程”生成DaoMasterDaoSession,对应的数据表和Dao
  • 通过QueryBuilder操作,查询得到需要的表数据
  • 支持跨实体查询
  • 支持数据库加密

使用教程和源码参考greenDAO-Github

LitePal

  • 轻量级数据库(大小176kb左右)
  • xml形式配置数据库名,数据库版本号和数据库表
  • 1.4.0版本以上支持多库
  • API操作简单,支持原生SQL语句执行
  • 不支持打开自建数据库的,需使用用litepal创建
    更多功能和使用文档查看郭霖大神的博客以及LitePal-Github

DBFlow

  • 轻量级数据库(大小70kb左右)
  • 性能高,不是基于反射
  • 操作速度快,基于annotationProcessor
  • 使用apt技术,在编译过程中生成操作类
  • 支持多个数据库
  • 可基于ModelContainer类解析像JSON这样的数据

使用文档参考DBFlowDBFlow-Github

afinal

  • 轻量级数据库(大小153kb左右)
  • 具有xml的id绑定和事件绑定功能
  • 网络图片的显示功能(里面包含了强大的缓存框架)
  • 数据库sqlite的操作功能,通过 FinalDb一行代码即可搞定
  • http数据的读取功能(支持ajax方式读取)

使用文档参考afinalafinal-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

GithubStar来说,则RealmGreen占优势,同时这两个的功能十分强大。
ORM库大小来说,则GreenDaoLitePalLiteORM等轻量级的占优势。
ORM的使用配置简单程度来说,则LitePalafinalLiteORM占优势。
综上所述,从稳定性,安全性,功能的强大性选RealmGreenDaoORMLite似乎更好,从轻量程度性,配置简单化来说选LitePalafinalLiteORMSugarORMDBFlow似乎更好。当然,如果想不依赖框架,使用Anko-SQLite来实现就再好不过了。

Realm基础

集成

project里面的build.gradle加入

classpath "io.realm:realm-gradle-plugin:4.1.1"

然后在appbuild.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表里面添加PublicProtected方法。如上面的IsEmptyName方法,所以几乎可以把User表当PoJo来使用

初始化Realm

Application里面的onCreate方法里面执行

 Realm.init(this)

增删修改操作
结合上篇MVP的封装以及上面User表,实现下图效果。

增删修改效果图.png
  • 同步增加
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分别对应executeOnSuccessOnError,其中OnSuccessOnError也可不回调。

  • 异步查询
   val results = mvpView.mRealm.where(User::class.java).equalTo("name","Android").findAllAsync()
    mvpView.UpdateUI(results)

这种写法类似JavaFuture,查询将会在后台线程中被执行,当其完成时,之前返回的 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数据的,可以通过StringInputStreamJsonObject直接传入保存到对应的表里面

三种方式如下:


json方式.png

举个例子,存储一个全国城市列表的jsonCity表里面

首先准备一个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表查询结果

查询结果.png

注意创建一个Realm,对应就要close一次。所以上文的BaseActivity/BaseFragmentRealm可以改成

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)

可以参考参考。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277

推荐阅读更多精彩内容