jpa+mybatis 组合 数据持久化 实践


背景:公司技术框架为 spring boot +jpa,jpa 可以处理正常的 业务,但是对于复杂的统计业务(尤其涉及对个聚合的复杂业务),jpa 过于消耗性能,所以研究在 spring boot 项目中,jpa+mybatis 组合的可能性


jpa && mybatis
  • jpa(Java Persistence API):大大简化数据访问层代码的编码,无需手动维护数据的持久化,是面向对象的
  • mybatis:支持定制化 SQL、存储过程以及高级映射,是面向关系的

jpa+mybatis 组合 demo实例

实战

分页查询用户职位信息 业务需求如下图:


需求效果
入参查询条件涉及聚合:user,position,dept
出参数据来源涉及聚合:user,position,dept,admin,userLastLoginRec

jpa 实现代码

 private Page<StatisticsUserDTO> queryStatisticsUserInfo(StatisticsSearchDTO searchDTO, Pageable pageable, boolean isPaging, Long tenantId) {
      QUser user = QUser.user;
      QPosition position = QPosition.position;
      BooleanExpression expression = user.id.isNotNull();
      BooleanExpression positionExpression = position.id.isNotNull();

      positionExpression = positionExpression.and(position.tenantId.eq(tenantId));
      if (searchDTO != null) {
          if (searchDTO.getGroupIds() != null && searchDTO.getGroupIds().length > 0) {
              positionExpression = positionExpression.and(position.deptId.in((Number[]) searchDTO.getGroupIds()));
          }
          if (!StringUtils.isEmpty(searchDTO.getJobCode())) {
              positionExpression = positionExpression.and(position.jobCode.contains(searchDTO.getJobCode()));
          }
          if (!StringUtils.isEmpty(searchDTO.getRealName())) {
              expression = expression.and(user.realName.contains(searchDTO.getRealName()));
          }
          if (!StringUtils.isEmpty(searchDTO.getPhoneNum())) {
              expression = expression.and(user.phoneNum.contains(searchDTO.getPhoneNum()));
          }

          List<PositionClassificationRec> attrs = searchDTO.getAttrs();
          //根据职位筛选信息
          if (!CollectionUtils.isEmpty(attrs)) {
              for (PositionClassificationRec attr : attrs) {
                  List<Long> positionIds = positionRepository.findAllByAttrsOptions(attr.getClassificationId(), attr.getOptions())
                          .stream().map(Position::getId).collect(Collectors.toList());
                  positionExpression = positionExpression.and(position.id.in(QueryPlanCacheOptimizeUtil.convertList(positionIds)));
              }
          }
      }

      Iterable<Position> positions = positionRepository.findAll(positionExpression);
      List<Long> userIds = new ArrayList<>();
      positions.forEach(position1 -> userIds.add(position1.getUserId()));

      expression = expression.and(user.id.in(userIds));

      Page<User> users;
      if (isPaging) {
          users = userRepository.findAll(expression, pageable);
      } else {
          Pageable pageable1 = null;
          users = userRepository.findAll(expression, pageable1);
      }

      Set<Long> newUserIds = users.getContent().stream().map(AbstractEntity::getId).collect(Collectors.toSet());
      List<Position> positionsList = positionRepository.findAllByUserIdIn(QueryPlanCacheOptimizeUtil.convertList(newUserIds));
      Map<Long, Position> positionMap = positionsList.stream().collect(Collectors.toMap(Position::getUserId, c -> c));
      Set<Long> deptIds = positionsList.stream().map(Position::getDeptId).collect(Collectors.toSet());
      Map<Long, Dept> deptMap = deptRepository.findAll(deptIds).stream().collect(Collectors.toMap(Dept::getId, c -> c));
      Map<Long, Long> userLastLoginMap = userLastLoginRecRepository.findAllByUserIdIn(newUserIds)
              .stream().collect(Collectors.toMap(UserLastLoginRec::getUserId, UserLastLoginRec::getLastLogin));

      return users.map(user1 -> {
          Position curPosition = positionMap.get(user1.getId());
          Dept curDept = deptMap.get(curPosition.getDeptId());
          return new StatisticsUserDTO(curPosition, curDept.getName(), user1.getRealName(), user1.getPhoneNum(), userLastLoginMap.get(user1.getId()), user1.getStartWorkTime());
      });
  }

disadvantage:

  • 消耗不必要的时间和性能
  • 难以在现有的框架上进行量级优化
  • 代码复杂度高,维护不方便
  • jpa 无法控制生成的sql,OneToMany 导致生成的sql 过度,消耗 datasource 连接数
    (此处 jpa 生成的 sql 数量 成百上千条)

mybatis 实现代码

<resultMap id="PositionStatisticsInfoMap" type="ky.edu.server.tenant.domain.model.PositionStatisticsInfo">
        <result property="deptName" column="deptName"></result>
        <result property="phoneNum" column="phoneNum"></result>
        <result property="realName" column="realName"></result>
        <result property="lastLogin" column="lastLogin"></result>
        <result property="startWorkTime" column="startWorkTime"></result>
        <association property="position" javaType="ky.edu.server.tenant.domain.model.Position">
            <id property="id" column="id"></id>
            <result property="createTime" column="createTime"/>
            <result property="lastModified" column="lastModified"/>
            <result property="jobCode" column="jobCode"/>
            <result property="tenantId" column="tenantId"/>
            <result property="deptId" column="deptId"/>
            <result property="accountId" column="accountId"/>
            <result property="userId" column="userId"/>
            <result property="admin" column="admin"/>
            <collection property="internalClassRecs" column="id" select="getInternalClassRecs">
                <result property="classificationId" column="classificationId"></result>
                <result property="optionId" column="optionId"></result>
            </collection>
        </association>
    </resultMap>
    
    <select id="findStatisticsPositionInfos" resultMap="PositionStatisticsInfoMap">
            select distinct
            up.id,d.deptName,ru.phoneNum,ru.realName,ullr.lastLogin,ru.startWorkTime,up.id,up.createTime,up.lastModified,up.jobCode,up.tenantId,
            up.deptId,up.accountId,up.userId,up.admin,pcr.classificationId,pcr.optionId
            from user_position up
            inner join reg_user ru on ru.accountId=up.accountId
            left join position_class_rec pcr on up.id=pcr.positionId
            inner join dept d on up.deptId=d.id
            left join user_last_login_rec ullr on ullr.userId=up.userId
            where up.tenantId= #{tenantId}
            <if test="statisticsSearch.realName !=null and statisticsSearch.realName != '' ">
                and ru.realName REGEXP #{statisticsSearch.realName}
            </if>
            <if test="statisticsSearch.phoneNum !=null and  statisticsSearch.phoneNum != '' ">
                and ru.phoneNum REGEXP #{statisticsSearch.phoneNum}
            </if>
            <if test="statisticsSearch.jobCode !=null and statisticsSearch.jobCode != '' ">
                and up.jobCode REGEXP #{statisticsSearch.jobCode}
            </if>
            <if test="statisticsSearch.groupIds !=null and statisticsSearch.groupIds.size !=0">
                AND up.deptId in
                (<foreach collection="statisticsSearch.groupIds" item="groupId" separator=",">#{groupId}</foreach>)
            </if>
            <if test="statisticsSearch.internalClassRecs !=null and statisticsSearch.internalClassRecs.size !=0">
                AND (pcr.classificationId,pcr.optionId) in
                (<foreach collection="statisticsSearch.internalClassRecs" item="attr" separator=",">
                (#{attr.classificationId},#{attr.optionId})</foreach>)
            </if>
            order by ru.lastModified desc
            <if test="offset !=null and perPage!=null">
                limit #{offset}, #{perPage}
            </if>
        </select>

<select id="getInternalClassRecs" resultType="ky.edu.server.common.domain.model.InternalClassRec">
                SELECT classificationId,optionId  FROM position_class_rec
                WHERE positionId = #{id}
            </select>

advantage:

  • 根据复杂统计业务建模,在数据库 层面 只查询有用数据
  • 降低 datasource 连接数
    (此处sql 数量 近 百条,OneToMany 关系 映射 产生的 过度sql 仍然没有解决)

mybatis 实现代码(降低 datasource 连接数版)

映射关系map 去除 internalClassRecs

<!--<collection property="internalClassRecs" column="id" select="getInternalClassRecs">-->
                    <!--<result property="classificationId" column="classificationId"></result>-->
                    <!--<result property="optionId" column="optionId"></result>-->
    <!--</collection>-->

增加 批量查询 internalClassRecs 方法:

<select id="queryInternalClassRecsByPositionIds" resultType="ky.edu.server.common.domain.model.InternalClassRec">
        SELECT classificationId,optionId,positionId  FROM position_class_rec
        WHERE positionId in
        (<foreach collection="positionIds" item="positionId" separator=",">#{positionId}</foreach>)
    </select>

然后 业务层面进行数据组装:

List<PositionStatisticsInfo> positionInfos = iPositionDao.findStatisticsPositionInfos(searchDTO, tenantId, (long) offset, (long) size);

        if(CollectionUtils.isEmpty(positionInfos)){
            return new PageImpl<>(positionInfos, pageRequest, 0);
        }
    
        Set<Long> positionIds = new HashSet<>();
        positionInfos.forEach(positionStatisticsInfo -> positionIds.add(positionStatisticsInfo.getPosition().getId()));

        List<InternalClassRec> internalClassRecs = iPositionDao.queryInternalClassRecsByPositionIds(positionIds);
        Map<Long, Set<InternalClassRec>> internalClassRecMap = MapAssembler.markInternalClassRecAsPositionIdKey(internalClassRecs);

        Position position;
        for (PositionStatisticsInfo positionInfo : positionInfos) {
            position = positionInfo.getPosition();
            position.initAttrs(internalClassRecMap.get(position.getId()));
        }

此方案 查询数据 sql 数量只有两条;

优化效果(源数据相同,查询条件相同,查询结果一致)

  • jpa 耗时 约 4.5s
  • mybatis 耗时 0.27s

:smirk: 日常数据的增删改查 推荐jpa,无需手动进行数据就持久化,复杂统计业务 推荐 mybatis,根据 需求 设计 模型,查询需要的数据,提示系统响应速度,降低性能消耗


popo先生的博客


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