jpa+mybatis 組合 數(shù)據(jù)持久化 實(shí)踐


背景:公司技術(shù)框架為 spring boot +jpa长窄,jpa 可以處理正常的 業(yè)務(wù)光涂,但是對(duì)于復(fù)雜的統(tǒng)計(jì)業(yè)務(wù)(尤其涉及對(duì)個(gè)聚合的復(fù)雜業(yè)務(wù))文黎,jpa 過(guò)于消耗性能,所以研究在 spring boot 項(xiàng)目中状共,jpa+mybatis 組合的可能性


jpa && mybatis
  • jpa(Java Persistence API):大大簡(jiǎn)化數(shù)據(jù)訪(fǎng)問(wèn)層代碼的編碼蚓哩,無(wú)需手動(dòng)維護(hù)數(shù)據(jù)的持久化,是面向?qū)ο蟮?/li>
  • mybatis:支持定制化 SQL件已、存儲(chǔ)過(guò)程以及高級(jí)映射牍疏,是面向關(guān)系的

jpa+mybatis 組合 demo實(shí)例

實(shí)戰(zhàn)

分頁(yè)查詢(xún)用戶(hù)職位信息 業(yè)務(wù)需求如下圖:


需求效果
入?yún)⒉樵?xún)條件涉及聚合:user,position,dept
出參數(shù)據(jù)來(lái)源涉及聚合:user,position,dept,admin,userLastLoginRec

jpa 實(shí)現(xiàn)代碼

 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();
          //根據(jù)職位篩選信息
          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:

  • 消耗不必要的時(shí)間和性能
  • 難以在現(xiàn)有的框架上進(jìn)行量級(jí)優(yōu)化
  • 代碼復(fù)雜度高,維護(hù)不方便
  • jpa 無(wú)法控制生成的sql,OneToMany 導(dǎo)致生成的sql 過(guò)度拨齐,消耗 datasource 連接數(shù)
    (此處 jpa 生成的 sql 數(shù)量 成百上千條)

mybatis 實(shí)現(xiàn)代碼

<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:

  • 根據(jù)復(fù)雜統(tǒng)計(jì)業(yè)務(wù)建模鳞陨,在數(shù)據(jù)庫(kù) 層面 只查詢(xún)有用數(shù)據(jù)
  • 降低 datasource 連接數(shù)
    (此處sql 數(shù)量 近 百條,OneToMany 關(guān)系 映射 產(chǎn)生的 過(guò)度sql 仍然沒(méi)有解決)

mybatis 實(shí)現(xiàn)代碼(降低 datasource 連接數(shù)版)

映射關(guān)系map 去除 internalClassRecs

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

增加 批量查詢(xún) 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>

然后 業(yè)務(wù)層面進(jìn)行數(shù)據(jù)組裝:

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()));
        }

此方案 查詢(xún)數(shù)據(jù) sql 數(shù)量只有兩條瞻惋;

優(yōu)化效果(源數(shù)據(jù)相同厦滤,查詢(xún)條件相同,查詢(xún)結(jié)果一致)

  • jpa 耗時(shí) 約 4.5s
  • mybatis 耗時(shí) 0.27s

:smirk: 日常數(shù)據(jù)的增刪改查 推薦jpa,無(wú)需手動(dòng)進(jìn)行數(shù)據(jù)就持久化歼狼,復(fù)雜統(tǒng)計(jì)業(yè)務(wù) 推薦 mybatis,根據(jù) 需求 設(shè)計(jì) 模型掏导,查詢(xún)需要的數(shù)據(jù),提示系統(tǒng)響應(yīng)速度羽峰,降低性能消耗


popo先生的博客


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末趟咆,一起剝皮案震驚了整個(gè)濱河市添瓷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌值纱,老刑警劉巖鳞贷,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異虐唠,居然都是意外死亡搀愧,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)疆偿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)咱筛,“玉大人,你說(shuō)我怎么就攤上這事杆故⊙嘎幔” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵处铛,是天一觀(guān)的道長(zhǎng)饲趋。 經(jīng)常有香客問(wèn)我,道長(zhǎng)罢缸,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任投队,我火速辦了婚禮枫疆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘敷鸦。我一直安慰自己息楔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布扒披。 她就那樣靜靜地躺著值依,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碟案。 梳的紋絲不亂的頭發(fā)上愿险,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音价说,去河邊找鬼辆亏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鳖目,可吹牛的內(nèi)容都是我干的扮叨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼领迈,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彻磁!你這毒婦竟也來(lái)了碍沐?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤衷蜓,失蹤者是張志新(化名)和其女友劉穎累提,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體恍箭,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刻恭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扯夭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鳍贾。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖交洗,靈堂內(nèi)的尸體忽然破棺而出骑科,到底是詐尸還是另有隱情,我是刑警寧澤构拳,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布咆爽,位于F島的核電站,受9級(jí)特大地震影響置森,放射性物質(zhì)發(fā)生泄漏斗埂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一凫海、第九天 我趴在偏房一處隱蔽的房頂上張望呛凶。 院中可真熱鬧,春花似錦行贪、人聲如沸漾稀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)崭捍。三九已至,卻和暖如春啰脚,著一層夾襖步出監(jiān)牢的瞬間殷蛇,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工橄浓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晾咪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓贮配,卻偏偏與公主長(zhǎng)得像谍倦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子泪勒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354