美文网首页
嵌入式Sqlite数据库(三):实战注解ORM数据库框架

嵌入式Sqlite数据库(三):实战注解ORM数据库框架

作者: bug喵喵 | 来源:发表于2021-01-15 22:07 被阅读0次

    前言

    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表,实现下图效果。

    img

    增删修改效果图.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直接传入保存到对应的表里面

    三种方式如下:

    img

    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表查询结果

    img

    查询结果.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文档。

    相关文章

      网友评论

          本文标题:嵌入式Sqlite数据库(三):实战注解ORM数据库框架

          本文链接:https://www.haomeiwen.com/subject/nfkgaktx.html