为什么不推荐使用 GreenDao 的多表关联特性

本文目录:

    • 1. 概述
    • 2. 舍弃关联特性
    • 3. 数据一致性问题

1. 概述

GreenDao 是一个很成熟的 ORM 框架 ,帮助我们不用去写繁琐的 SQL 去实现 CURD,还有数据库的升级等繁琐的操作都被模板化,让我们更加关注于业务实现上。

在平时的使用中,多表关联会经常遇到。

多表关联主要有这几种:

  • 一对一关联,@ToOne
  • 一对多关联,@ToMany
  • 多对多关联,比较复杂,而且少见,一般用中间表实现

如果有多表关联,在被关联的实体的获取的地方,GreenDao 模板会主动帮我们加入数据库的联表查询,增强了 Getter 操作。

以一对一关联举例。假设我们现在有一个实体 MediaBean,里面的 UserBean 属于另外一个表的。使用 GreenDao 的注解,可以帮助我们去实现这个关联,并且在获取 UserBean 的地方加入数据操作。

我们这样创建 MediaBean,用注解 @ToOne 去设置一对一关联:

@Entity
public class MediaBean extends BaseBean {
    @Id
    private Long id;
    ...
    @ToOne(joinProperty = "uid")
    private UserBean user;
    ...
}

在 Gradle 编译结束后,可以看到获取 UserBean 的方法 getUser,被 GreenDao 增强,加入数据库多表查询操作。

增强的关联查询操作如下:

public UserBean getUser() {
    Long __key = this.uid;
    if (user__resolvedKey == null || !user__resolvedKey.equals(__key)) {
        final DaoSession daoSession = this.daoSession;
        if (daoSession == null) {
            return user;
        }
        UserBeanDao targetDao = daoSession.getUserBeanDao();
        UserBean userNew = targetDao.load(__key);
        synchronized (this) {
            user = userNew;
            user__resolvedKey = __key;
        }
    }
    return user;
}

这个地方我们可以体会到,经过了 GreenDao 增强操作后,MediaBean 的 Getter 方法已经带上业务了。好处是强一致性,每次读取 UserBean 一定会从数据库中获取最新的。但是和我们的设计有冲突。因为一开始 MediaBean 的设计,只是个数据的载体,一个纯粹的 Bean。经过 GreenDao 增强后,不再纯粹了。

这里隐藏着两个比较严重的问题:

  • 主线程卡顿 。每次获取 UserBean 都会进行一次数据库检索,如果是在主线程的话,调用者只是单纯地认为自己调用 getUser 方法,其实还包含数据库操作,有引起主线程卡顿的风险。数据库操作本质上也是 IO 操作,在数据库结构复杂,数据量大的话,这个耗时是很大的。
  • DaoException 异常 。在读取数据 UserBean 的时候,如果没有经过数据库操作,没有注入 DaoSession 的话,按模板的写法会直接抛出异常。具体的发生场景有,网络传递过来的,没有经过数据库操作的话,DaoSession 没有注入,这个异常就会暴露。但实际上,UserBean 已经载入了内存,这个地方是可以返回值而不再去数据库操作的。

2. 舍弃关联特性

这里提出一个解决方案,是比较粗暴的方案, 那就是不使用 GreenDao 的多表关联的特性。

因为我们需要更纯粹的 JavaBean,更干净的 Setter 和 Getter,不再里面带上数据库操作的业务。

那什么地方取做关联查询呢?

我们自己创建一个 MediaDBHelper,进行 MediaBean 的增删改查的具体实现,把对其他表的关联查询封装起来。本质上,是把 GreenDao 原本在 Getter 做的关联查询转移一个工具类中处理。这样子,多表关联的业务,就由我们自己去接管,自己决定什么时候去做,做到对数据库操作的最大的灵活度。

整个流程如下

  • 不入库。对 UserBean 类型的成员变量 user 加入 @Transient 注解,标记该字段不入库。
  • 创建外键。要进行多表查询,外键还是少不了的。这里我们创建字段 userId,对应 UserBean 的 id,作为 UserBean 表的外键。后续代码会使用该外键进行多表查询,找到关联的 UserBean。
  • 实现关联。创建工具类 MediaDBHelper,在对 MediaBean 增删改查的地方,对 UserBean 做关联查询。可以把关联的 UserBean 入库,也可以查询出来。
public class MediaDBHelper {    
    ...

    public void addMediaBean(MediaBean mediaBean) {
        MediaBeanDao mediaBeanDao = getMediaBeanDao();
        UserBean userBean = mediaBean.getUser();
        if (userBean != null) {
            getUserBeanDao.insertOrReplace(userBean);
        }
        mediaBeanDao.insertOrReplace(mediaBean);
    }

    public MediaBean getMediaBean(long mid) {
        MediaBean mediaBean = getMediaBeanDao().load(mid);
        // 补充用户信息
        long userId = mediaBean.getUserId();
        UserBean userBean = getUserBeanDao().load(userId);
        if (userBean != null) {
            mediaBean.setUser(userBean);
        }
        return mediaBean;
    }
    ...
}    

有个关键的地方,就是外键 userId 什么时候注入 MediaBean。

疑问 userId 是客户端自己创建的字段,服务端接口并没有返回。我们可以在解析完服务端数据后,主动去设置该 userId。如果使用了 Gson 库,可以使用 JsonDeserializer 来做。

UserBean user = mediaBean.getUser();
if (user != null) {
    mediaBean.setUserId(user.getId);
}

总之,从服务端取完数据后,需要主动去补充一些信息,比如它的 user 字段的外键。

3. 数据一致性问题

ORM 框架帮我们减少了一些体力活,但是因为高度封装的框架,在灵活性会有一定的损失。

我理解 GreenDao 把关联查询放到 Getter 中,是为了数据的强一致性,确保每次读到的都是从数据库中拿出来的最新数据。但实际开发中,这样子反而增加了数据库 IO,移动平台资源本来就紧张,这样会增加程序卡顿的风险。

我的方案有一个缺点,那就是数据一致性问题。如果获取了数据,期间应用其他地方修改了数据,就会出现不一致。按 GreenDao 的设计,数据修改和读取都会经过数据库,所以不会有一致性问题。但我舍弃了它的多表关联,在一次读取之后,基本都是内存操作,所以一致性的问题得另外解决。

这里可以配合其他方案解决,比如消息总线的通知。

具体的做法在数据修改的地方,都会通过消息总线(比如 EventBus)发出通知数据,把已经载入内存中,其他地方还在用的脏数据全部更新。

通过这样的方式,既减少数据库 IO,又保证了数据一致性。

总结,不推荐使用多表关联,然后替代方案可以归纳为三:

  • 去掉 JavaBean 的多表关联注解,加入外键 Id。
  • 加一层 CURD 工具类或者管理类,用来实现关联。
  • 使用消息总线,数据发生变化通知内存中的数据进行更新,来保证一致性。

你可能感兴趣的:(Android)