网易新闻客户端对数据库的依赖是比较重的,很多数据都存在了客户端数据库,因此我们很早之前就想将数据库模块进行改造。于是我们对目前 Android 上使用比较广泛的数据库框架做了一个调研,并写了一个 demo 比较了一下各个框架的性能和易用性。

Android 上常用的数据库框架

调研了一下目前 android 上常用的数据库框架,主要有:

  • SQLite android 原生自带的数据库
  • WCDB 微信团队前一段时间开源的数据库。基于 android 原生的 SQLite 数据的优化,并能支持数据库的加密。
  • ORMLite 基于 ORM(对象关系映射) 概念的数据库框架。基于反射和注解的原理实现的 ORM 数据库框架。官网 http://ormlite.com/
  • GreenDao 基于 ORM(对象关系映射) 概念的数据库框架。基于编译期注解的 ORM 数据库框架。官网https://github.com/greenrobot/greenDAO
  • Realm 与 SQLite 没有任何关系的移动应用的轻量级数据库。性能优越,传说中想要干掉 SQLite 成移动端的一方霸主
    下表是对上述集中数据库在指定机型是上操作2000条简单数据的对比结果:
DB/Item SQLite WCDB ORMLite GreenDao Realm
版本 - 1.0.2 5.0 3.2.2 3.4.0
insert 4805ms 4753ms 200ms 80ms 40ms
query 20.4ms 11ms 23.2ms 4.6ms 0.2ms
体积 - 900KB 396KB 140KB 1.7M(仅 armeabi-v7a so 体积),其他平台体积更大
优缺点 Android 原生自带 基于原生sqlite 的优化,支持加密 使用方便,但基于反射和注解效率低 使用方便,基于编译器注解,效率较高 优点:速度快,速度快,速度快。缺点:1.体积大;2不支持id自增长;3不支持线程切换;4不支持limit; android 上还有其他一些坑

综合上表,确定数据库框架的选型:GreenDao

舍弃 Realm 数据库的原因

最初没有具体查看体积,故准备使用 Realm 数据库,同时也是为了能和 iOS 客户端保持一致,因为 iOS 最近刚刚切换到 Realm 数据库。 当写完 realm 数据库 demo 的时候,我准备放弃它了,原因有几个方面: a. 体积太大了,我们刚刚做完瘦身的工作,使用它就一下回到解放前了。而如果采用下发 so 的方式减小包体积又不太可行,因为客户端一启动时就需要调用数据库。b. 坑太多了,有其自身的缺陷和 android 上的一些坑,大家 google 一下就能发现。

选择 GreenDao 数据库

目前只剩下 WCDB, ORMLite和 GreenDao。 基于性能,开发效率和体积最终选择了 GreenDao 框架。
先上 GreenDao 官方的性能对比图,虽然官方图片有点夸张,但是也能说明一些问题。

GreenDao 优缺点

在使用的过程中总结了一些 GreenDao数据库框架的优缺点:
优点:

  1. 性能最大化(官方词汇)。 虽然不能和 Realm 相比较,但是基于 ORM 的框架中算是最好的。做了比较好的数据库缓存,并且使用的是基于编译时的注解。
  2. API 非常易用,提升了开发效率
  3. 最小的内存开销(内存方面目前没有和原生 SQLite 做对比,不敢多讲)
  4. 较小的文件体积
  5. 可支持原生语句,从android 原生 SQLite 过度到 greenDao 相对还是比较容易
  6. 数据表结构和 Entity 数据结构convert 支持,Entity 的不同数据结构和数据库存储结构之间做一个灵活的转换

缺点:

  1. 不支持组合主键。 未找到好的解决方案,只能是自己创建表时不使用组合主键。
  2. 不支持组合唯一约束。 解决方案: 组合唯一约束可以使用组合索引的方式来解决

    1
    2
    3
    4
    5
    6
    @Entity( nameInDb = CommentFollow.TableInfo.TABLE_NAME,
    generateConstructors = false,
    //设置loginUserid和followUserId的多行索引,并设置唯一,代替unique(a,b) on conflict replace
    indexes = {@Index(value = "loginUserId, followUserId", unique = true)} )
    public class CommentFollow {
    }

    上述例子中使用 loginUserId和 followUserId 组成的唯一索引来代替组合唯一约束

  3. 编译期注解的常量设置只能支持当前类的内部常量,外部常量值无法识别。 例如

    1
    2
    3
    4
    5
    6
    7
    @Entity(nameInDb = Ask.TableInfo.TABLE_NAME, generateConstructors = false)
    public class Ask {
    public interface TableInfo {
    String TABLE_NAME = "ask_list"
    }
    }
    上述代码中 Entity 注解中的 value 值nameInDb 只能支持当前类中定义的常量或者直接在此处写字符串,不能支持此类以外的常量。即上如例子中 TableInfo 只能是当前类内部定义的常量
  4. 对于 SQL 中的 max(), min()等函数的支持不友好。解决方案: a. 使用raw sql 语句;b. 使用排序等变通方式达到相同效果

  5. 缓存处理容易产生坑。 例如:不使用 greenDao 提供的 api 进行数据库update or insert操作,而是使用 raw sql 语句进行的update or insert时,需要手工清理缓存数据,防止数据异常
  6. Entity 实体类不支持类的继承。即Entity 类中继承自父类的成员变量不能直接存储到数据库中
  7. 目前暂时不支持UNIQUE ON CONFLICT IGNORE, 只能通过先查询再插入解决。查看最新 master 代码已经解决(根据主键设置忽略),但是还未发布。详情: https://github.com/greenrobot/greenDAO/pull/145/commits/9d656c1f5eb2e74210fc1f430ad31d357a5a2aeb

GreenDao注解

常见注解

  • @Entity 用于标识这是一个需要Greendao帮我们生成代码的bean, 可以指定自动生成构造,set/get等方法

    1
    2
    3
    4
    5
    @Entity(  schema = "myschema", 
    active = true,//TODO 是标明是否支持实体类之间update,refresh,delete等操作
    nameInDb = "table_user",
    indexes = { @Index(value = "name DESC", unique = true) },
    createInDb = true )
  • @Id 标明主键,括号里可以指定是否自增

  • @Property 用于设置属性在数据库中的列名(默认不写就是保持一致)
  • @NotNull 非空
  • @Transient 标识这个字段是自定义的不会创建到数据库表里
  • @Generated 这个是build后greendao自动生成的,这个注解为防止重复,每一块代码生成后会加个hash作为标记。 官方不建议你去碰这些代码,改动会导致里面代码与hash值不符。

    关系注解

  • @ToOne 是将自己的一个属性与另一个表建立关联
    外键关联支持与另一个表的主键建立关联,目前没找到与另一个表的唯一非主键建立关联的方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Entity
    public class Order {
    @Id private Long id;
    private long customerId;
    @ToOne(joinProperty = "customerId")
    private Customer customer;
    }
    @Entity
    public class Customer {
    @Id
    private Long id;
    }
  • @ToMany 的使用场景有些多 以下几种不常用
    @ToMany的属性referencedJoinProperty,类似于外键约束。 查询时使用深度查询即可多表查询

  • @JoinProperty 对于更复杂的关系,可以使用这个注解标明目标属性的源属性。
  • @JoinEntity 如果你在做多对多的关系,有其他的表或实体参与,可以给目标属性添加这个额外的注解(感觉不常用吧)
    注意: 想要一次性查询出关联表的数据,需要使用OrderDao.queryDeep()进行查询,因为默认情况下是志勇调用了Order.getCustomer的时候才去查找Customer表中的数据,这一点可以从自动生成的getter方法看到。getter方法如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /** To-one relationship, resolved on first access. */
    @Generated(hash = 1566736934)
    public AskSupport getMAskSupport() {
    String __key = this.askSupportId;
    if (mAskSupport__resolvedKey == null || mAskSupport__resolvedKey != __key) {
    final DaoSession daoSession = this.daoSession;
    if (daoSession == null) {
    throw new DaoException("Entity is detached from DAO context");
    }
    AskSupportDao targetDao = daoSession.getAskSupportDao();
    AskSupport mAskSupportNew = targetDao.load(__key);
    synchronized (this) {
    mAskSupport = mAskSupportNew;
    mAskSupport__resolvedKey = __key;
    }
    }
    return mAskSupport;
    }

转换注解

  • convert 注解负责在 Entity 实体中数据结构和数据表中存储的数据结构进行转换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        @Convert(converter = StringListGreenConverter.class, columnType = String.class) //columnType 为数据存储的类型,支持 java 的基本类型和 String
    private List<String> pics;

    public class StringListGreenConverter implements PropertyConverter<List<String>, String> {
    @Override
    public List<String> convertToEntityProperty(String databaseValue) {
    if (TextUtils.isEmpty(databaseValue)) {
    return new ArrayList<String>();
    }
    return JsonUtils.fromJson(databaseValue, new TypeToken<ArrayList<String>>() {
    });
    }

    @Override
    public String convertToDatabaseValue(List<String> entityProperty) {
    return JsonUtils.toJson(entityProperty);
    }
    }