蘭陵N梓記

一指流沙,程序年华


  • 首页

  • 归档

  • 关于

  • 搜索
close

飞哥讲代码17:写好代码就要深入细节

时间: 2020-11-29   |   分类: 技术     |   阅读: 4058 字 ~9分钟

案例

案例代码来源我们某产品:

public void rollbackOrgPackage(Map<String, Object> oldOrgPackage, String orgName) throw ApigwException, ParseException {
    if (StringUtil.isEmpyt(orgName)) {
        throw new BackParameterException(...);
    }

    for (Entry<String, Object> orgPackage: oldOrgPackage.entrySet()) {
        switch ( orgPackage.getKey() ) {
            case "orgAssets":
                List<TApigwOrgAsset> orgAssets = (List<ApigwOrgAsset>) orgPackage.getValue();
                List<TApigwAssetContent> orgAssetContents = (List<TApigwAssetContent>) oldOrgPackage.get('orgAssetContents');
                try {
                    if (orgAssets != null) {
                        for (TApigwOrgAsset orgAsset: orgAssets) {
                            aTApigwAssetContentMapper.deleteByPrimaryKey(orgAsset.getAstid());
                            aTApigwOrgAssetMapp.deleteByOrgName(orgName, orgAsset.getZone());
                        }
                    }

                    if (orgAssets == null || orgAssetContents == null) {
                        break;
                    }
                    for(TApigwOrgAsset aTApigwOrgAsset: orgAssets){
                        aTApigwOrgAssetMapper.insert(aTApigwOrgAsset);
                    }
                    for(TApigwAssetContent orgAssetConent: orgAssetContents){
                        aTApigwAssetConentMapper.insert(orgAssetConent);
                    }
                } catch (Excetption e) {
                    threw new ApigwExcepiton(...)
                }
                break;
            case "orgService":
                ... // 省略
            case "orgConfigGroups":
                ... // 省略
            case "orgVariables":
                ... // 省略
        }
    }
}

上面的代码存在典型的switch惊悚的坏味道。每个Switch块较大,嵌套比较深,在Swith中又存在for循环。还存其它的坏味道:

  • 重复轮子:StringUtil是自己写的,并没有采用apache common包的StringUtils
  • 命名问题:oldOrgPackage存储多个对象建议采用复数oldOrgPackages,aTApigwAssetContentMapper建议是ApigwAssetContentMapper,因为a, T(ype)没有太多意义,对于类型接口,在Java,不建议加T,I前辍
  • 关联问题:一个swtich分支中同时处理orgAsset与orgAssetContent,而他们都是oldOrgPackage Map中的Key
  • 穷举问题:switch的case通常建议是Enum值,静态分析工具/IDE也能帮助扫描,从而避免case分支的遗落
  • 事务问题:rollbackOrgPackage方法操作多个Mapper,需要加事务保护,当然事务可以加在调用此方法的外层,但建议事务粒度最小化就近原则,避免上层使用时遗漏
  • 性能问题:采用for循环来insert,未采用批量插入,存在多条sql操作

背后的知识点

一看到switch语句,可能马上想到采用多态来替换解决(来自重构一书), 重构步骤如下:

  • switch语句常常根据type code(型别码)进行选择,需提取与该type code相关的函数或class
  • Extract Method(提炼函数)将switch语句提炼到一个独立函数中, 如handleOrgAsset, hanleOrgService等独立方法
  • Move Method(搬移函数)将它搬移到需要多态性的那个类里,如把方法搬移一个RollbackOrgPackageHandler类中
  • Replace Type Code with Subclass(以子类取代类型码)或 Replace Type Code with State/Strategy(以状态/策略取代类型码),如每个typeCode的子类RollbackOrgAssetHandler, RollbackOrgServiceHandler等,都继承RollbackOrgPackageHandler的handle方法。
  • 完成这样继承结构后,有一个新类聚合所有RollbackOrgPackageHandler子类,遍历所有oldOrgPackages,分发给各个子类的handle方法处理

采用多态对于案例中的代码也显得有些厚重了(代码过多),解决此Switch问题,提取相应的函数即可,本文不再展开。

批量插入

前面提到采用for循环来insert,未采用批量插入,存在多条sql操作,这种操作是低效,那有没有办法解决?

案例代码是采用MyBatis框架,那在对应的Mapper定义中增加saveAll方法来支持批量插入。

pubic interface ApigwOrgAssetMapper {
    void insert(ApigwOrgAsset apigwOrgAsset);
    void saveAll(List<ApigwOrgAsset>) assets);
}

saveAll对应的SQL如下。为了呈现SQL,还是以xml的方式配置来讲解,建议新项目采用注解或Provider动态SQL的方式。

<insert id="saveAll" parameterType="list" >
      INSERT INTO ORG_ASSET (field1, field2) 
      VALUES
      <foreach collection="list" item="it" separator=",">
          (#{it.field1},#{it.field2})
      </foreach>
</insert>

VALUES后面可以跟多个(a_field1, a_field2), (b_field1, b_field2)是MySQL的私有写法,并不是SQL标准,并且有一次批量插入个数上限:语句的长度默认是不能超过4M。

Oracle有两种写法,也是非SQL标准:

<insert id="saveAll" parameterType="java.util.List" useGeneratedKeys="false">
        INSERT ALL
        <foreach item="it" index="index" collection="list">
        INTO ORG_ASSET (field1, field2) VALUES (#{it.field1},#{it.field2})
        </foreach>
        SELECT 1 FROM DUAL
</insert>

另一种写法SQL是:insert into table(...) (select ... from dual) union all (select ... from dual)

缺点:采用MyBatis就会面临不同数据库之间需要采用不同的xml。

我们再来看一下JDBC的Statement接口是怎么支持批量操作:

  • Statement.addBatch(sql):添加要批量执行SQL语句
  • Statement.executeBatch():执行批处理SQL语句
  • Statement.clearBatch():清除批处理命令

PreparedStatement也支持批量操作,示例如下:

conn = JdbcUtils.getConnection();
String sql = "INSERT INTO ORG_ASSET(field1, field2) values(?,?)";
st = conn.prepareStatement(sql);
for (int i=1;i< 2000;i++){  
    st.setInt(1, i);
    st.setString(2, "field2_" + i);
    st.addBatch();
    if (i%1000==0) {
        st.executeBatch();
        st.clearBatch();
    }
}
st.executeBatch();

Mybatis执行SQL底层接口是Executor,两个实现类:

  • BaseExecutor:用于一级缓存及基础的操作,又分为三个子类
    • SimpleExecutor:一次执行一条SQL
    • BatchExecutor:通过批量操作来优化性能,即采用上面JDBC的executeBatch执行
    • ReuseExecutor:会缓存同一个sql的Statement,省去Statement的重新创建,优化性能
  • CachingExecutor:用于二级缓存

若在Mybatis中采用Batch方式,则采用如下方式:

try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    ApigwOrgAssetMapper mapper = session.getMapper(ApigwOrgAssetMapper.class);
    for (ApigwOrgAsset apigwOrgAsset: orgAssets) {
        mapper.insert(apigwOrgAsset);
    }
    session.flushStatements();
}

若在Mybatis采用动态Provider方式,可参见:Batch Insert Support中的样例

try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
    List<SimpleTableRecord> records = getRecordsToInsert(); // not shown

    BatchInsert<SimpleTableRecord> batchInsert = insert(records)
            .into(simpleTable)
            .map(id).toProperty("id")
            .map(firstName).toProperty("firstName")
            .map(lastName).toProperty("lastName")
            .map(birthDate).toProperty("birthDate")
            .map(employed).toProperty("employed")
            .map(occupation).toProperty("occupation")
            .build()
            .render(RenderingStrategies.MYBATIS3);

    batchInsert.insertStatements().forEach(mapper::insert);
    session.commit();
}

现有我们也有不少项目采用JPA来做ORM,JPA由于底层默认采用Hibernate,而Hibernate相比MyBatis封装对数据库更解耦,对于批量操作来得更简单:

public interface XXEntityRepository extends JpaRepository<XXEntity, String> {
}

// 拿到Repository之后,则可以批量插入,更新
repository.saveAll(list);

默认情况下,它并不会采用JDBC的executeBatch执行,还需要配置:

spring.jpa.properties.hibernate.jdbc.batch_size=500
spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

另对于MySQL,还可以配置rewriteBatchedStatements=true,实现高性能的批量插入。

  • MySQL JDBC连接URL字符串中需要新增一个参数rewriteBatchedStatements=true ,5.1.13以上版本才支持
  • MySQL JDBC驱动在默认情况下会无视executeBatch()语句,只有把rewriteBatchedStatements参数置为true, 驱动才会帮你批量执行SQL

Mybatis Or JPA

现在网络上有着不少Jpa和Mybatis的对比与争论,国内使用MyBatis的比较多。就我见到的项目而言,Mybatis在我司使用较早,并没有跟随MyBatis新发展,积累如下问题:

  • 早期SQL要写在xml中,维护不方便,未采用注解的方式
  • 早期没有采用XXXStatementProvider.build().render()风格来减少原生SQL,缺少灵活性,代码也较多,注:依赖 mybatis-dynamic-sql
  • 早期不少项目未使用mybatis-plus,连简单的 CRUD 操作也需要写SQL,存在相似SQL
  • 由于要支持多种数据库的Paging翻页,需要写不同的SQL,采用Provider中拼SQL,存在注入的风险

普通的CRUD:

  • MyBatis:可以采用mybatis-plus来简化
  • JPA:内置CrudRepository

对于动态SQL(如where条件不固定的场景),目前MyBatis与JPA都支持比较好:

  • MyBatis: 提供XXXStatementProvider.build()方式
  • JPA:提供Specification与ExampleMatcher两种方式

对于Paging翻页:

  • MyBatis:采用mybatis-plus的翻页插件,而mybatis的SelectStatementProvider的Limit与Offset方式需要数据库支持limit来offset,存在不同数据库的切换工作量
  • JPA:支持Repository的方法支持Pagable参数,底层通过hibernate dialect来实现不同数据库的适配

缓存,都提供接口来对缓存扩展,默认支持:

  • MyBatis:一级缓存(session缓存),二级缓存(mapper级别的缓存)
  • JPA:由Hibernate实现,一级缓存(session缓存),二级缓存(跨entityManager)

整体来说,我对JPA与MyBatis使用并没有倾向性,但无论使用谁,都建议多一些深入,使用最新特性:

  • JPA(Hibernate):比较复杂,重量型,功能齐全,与数据库高度解耦合,完整的ORM
  • MyBatis:比较简单,轻量型,它并非是完整的ORM,而是SQL Mapping

破窗效应

破窗效应理论认为环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。

前文对案例的代码例举几个坏味道,从风格来看都是一些小问题。但由于项目组人员流动的问题,不断会有新人的加入。而新人往往是从学习前辈的代码开始,而技术又不断地发展,过了一段时间之后,你会惊奇发现:那些看似较小且不重要的问题,会对你产生巨大的影响。

本文重点讲解了一下SQL的批量操作,以及MyBatis与JPA的公共点,本意是想让大家对使用的技术多一些深入,不断地采用它他们提供的新特性来简化我们的开发。旧的API或使用方式或许就像一扇破旧的窗户存在哪里。当发现第一扇破窗户,就需要赶快去修补,不然软件就会随着窗户一样,一扇扇的被打破,慢慢的腐化下去。

记住Later=Never,会使我们越来越缺少动力。受破窗效益、本身的惰性以及自我要求不严格都导致了代码质量的下降。通过“修复所有的破窗”,关注小的细节,清理老旧的代码,解决很多小看似不重要的细节问题。经过时间的积累,再从头来看,虽然我们并没有做大型的改动,但是软件像改头换面一样。

结语

本文通过案例中一处代码要采用批量插入来展开,介绍了MyBatis与JPA对批量操作如何支持,以及他们一些其它对比,引申到我们使用MyBatis由于时间的推移,受示范破窗效应,我们还停留在采用旧的技术方式来实现。我们的CleanCode目标不是那些冰冷的指标数据,而是要采用的技术逐渐迭代更新换代。也需要我们有意识地深入关注细节,搞清其中原理,才能有效地清理老旧的代码,作出采用新的技术特性的示范,避免破窗效应。

#软件开发# #java#
飞哥讲代码18:记一次问题定位分析
程序员编码技术栈
微信扫一扫交流

标题:飞哥讲代码17:写好代码就要深入细节
作者:兰陵子
关注:lanlingthink(览聆时刻)
声明:自由转载-非商用-非衍生-保持署名(创作共享3.0许可证)

  • 文章目录
  • 站点概览
兰陵子

兰陵子

Programmer & Architect

164 日志
4 分类
57 标签
GitHub 知乎
  • 案例
    • 背后的知识点
  • 批量插入
  • Mybatis Or JPA
  • 破窗效应
  • 结语
© 2009 - 2022 蘭陵N梓記
Powered by - Hugo v0.101.0
Theme by - NexT
0%