網易工程師親歷:回顧一次sql缺乏where條件的慘案…

話說那是一個愉快的週五的下午,剛經歷雙11雙黑五12大促連環迎戰,一週的工做也接近結束,任務也都要走向提測的節點了,內心美滋滋,能夠早點回家啦~html

巴特,popo彈出一份:xxx master 的單元測試靜態代碼檢查失敗,單元測試失敗用例x個。請在1小時內修復並從新執行,不然可能會打回提測,單測失敗詳情xxx。java

看看失敗緣由吧~spring

org.springframework.jdbc.UncategorizedSQLException: 
### Error updating database.  Cause: java.sql.SQLException: It is not allowed to execute a(n) DELETE without where condition, sql:delete from mt_flash_sale_nav
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: delete from mt_flash_sale_nav
### Cause: java.sql.SQLException: It is not allowed to execute a(n) DELETE without where condition, sql:delete from mt_flash_sale_nav
; uncategorized SQLException for SQL []; SQL state [HY000]; error code [0]; It is not allowed to execute a(n) DELETE without where condition, sql:delete from mt_flash_sale_nav; nested exception is java.sql.SQLException: It is not allowed to execute a(n) DELETE without where condition, sql:delete from mt_flash_sale_nav
複製代碼

瓦特,怎麼可能,以前仍是好好的呀~ 我沒動過這塊代碼呀~必定是加班太多晃了個人狗眼~本地跑一次仍是這樣~! GGsql

趕忙聯繫DBAbaba們,原來是測試環境qs升級啦!
xxxmarkdown

好吧,扯犢子就到這裏了mybatis

前段時間測試環境ddb開始限制不帶where條件的update/delete的sql語句的執行,單測各類失敗,且後續還會在生產環境也會這樣,因而開始在工程中各類搜索,人工處理不免有遺漏的可能,怎麼地也要用程序所有掃描下才放心呀!app

那mybatis是如何解析xml和生成sql的呢,好比這樣的sql是如何解析的呢?ide

<select id="selectByCond" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from test_db
    <where>
      <if test="a != null">
        and `a` = #{a}
      </if>
      <if test="list != null">
        and b in
        <foreach collection="list" open="(" close=")" item="item" separator=",">
          #{item}
        </foreach>
      </if>
      order by db_update_time
    </where>
  </select>
複製代碼

經過分析mybatis的初始化SqlSessionFactoryBean過程,能夠一探究竟。oop

/**
   * Build a {@code SqlSessionFactory} instance.
   *
   * The default implementation uses the standard MyBatis {@code XMLConfigBuilder} API to build a
   * {@code SqlSessionFactory} instance based on an Reader.
   * Since 1.3.0, it can be specified a {@link Configuration} instance directly(without config file).
   *
   * @return SqlSessionFactory
   * @throws IOException if loading the config file failed
   */
  protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
    // ...

    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          // 這裏解析xml
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }

    return this.sqlSessionFactoryBuilder.build(configuration);
  }
複製代碼

而sql最終如何生成呢?主要靠SqlSource Sql源接口,表明從xml文件或註解映射的sql內容,主要就是用於建立BoundSql,有實現類DynamicSqlSource(動態Sql源),StaticSqlSource(靜態Sql源)等:單元測試

/**
 * Represents the content of a mapped statement read from an XML file or an annotation. 
 * It creates the SQL that will be passed to the database out of the input parameter received from the user.
 *
 * @author Clinton Begin
 */
public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}
複製代碼

如此,想要打印工程項目中全部sql並判斷是否帶有where條件就比較明晰了,直接上代碼:

@Resource
    private SqlSessionFactory sqlSessionFactory;

    @Test
    public void test_check() {
        Configuration configuration = sqlSessionFactory.getConfiguration();
        System.out.println("#sql.size#" + configuration.getMappedStatements().size());
        Set<String> errors = Sets.newHashSet();
        int i = 1;
        for (Object obj : configuration.getMappedStatements()) {
            if (obj instanceof MappedStatement) {
                MappedStatement mappedStatement = (MappedStatement) obj;
                String sql = mappedStatement.getSqlSource().getBoundSql(new SqlParamMap()).getSql();
                sql = sql.replaceAll("\n", "");
                sql = sql.replaceAll("\\s+", " ");
                System.out.println(String.format("#sql,#%02d #%s #%s", i++, mappedStatement.getSqlCommandType(), sql));

                if (!sql.toLowerCase().startsWith("insert") && !sql.toLowerCase().startsWith("select")
                        && !sql.toLowerCase().startsWith("replace")) {
                    if (!sql.toLowerCase().contains("where")) {
                        errors.add(sql);
                    }
                }
            }
        }

        System.err.println("#error#" + errors.size());
        for (String errorSql : errors) {
            System.err.println(errorSql);
        }
    }

    // 這裏爲了方便生成sql時,解析入參對象的
    public static class SqlParamMap extends AbstractMap<String, Object> implements Map<String, Object> {

        @Override
        public Set<Entry<String, Object>> entrySet() {
            return Collections.emptySet();
        }

        @Override
        public Object get(Object key) {
            return new Object[] {1, 2};
        }
    }
複製代碼

如此即可打印出不符合條件的sql語句了。好比在咱們haitao-matter工程裏搜索出:

#error#21

delete from mt_baby_article_config
delete from mt_coupon_center_nav
update mt_auction_goods set price_check_status = 4
delete from mt_spring_label
delete from mt_scene_brand
delete FROM mt_newgoods_content_config
update mt_auction_goods_edit set price_check_status = 4
DELETE from mt_coupon_center_coupon_info
delete from mt_spring_label_goods
delete from mt_goods_stock_rel
delete from mt_flash_sale_nav
delete from mt_auction_homeshow_inferior
delete from matter_switcher_param_center
delete from mt_mission_award
delete from `mt_newgoods_category_tab_config`
delete from mt_goods_stock_rel_edit
delete from mt_album_label
delete from matter_app_channel_relations
delete from element_user_baby_coupon_info_log
delete from mt_album_label_category
複製代碼

此外還能打印出工程全部sql出來,好比:

#sql,#1974 #DELETE #delete from mt_auction_homeshow_inferior_edit where id=?
#sql,#1975 #SELECT #select id,title,begin_time,end_time,type,status,db_update_time,content from matter_common_schedule WHERE type =? and begin_time <=? and end_time >=? and type = ? order by begin_time limit ?,?
#sql,#1976 #UPDATE #update element_user_baby_coupon_info_log SET award_info=?, create_time=?, update_time=? where id=?
#sql,#1977 #SELECT #SELECT id from mt_auction_goods where show_status!=2
#sql,#1978 #INSERT #insert into TB_ACTIVITY_SHOW_DETAIL_EDIT (id, zone_id, activity_show_id, related_type, related_id, image_url, image_link, sort_order, config,ui_data ) values
#sql,#1979 #SELECT #select id, related_id, goods_id, goods_title, category_id, category_name, advance_price, question_mark_pos, config, sort_order, db_create_time, db_update_time from mt_advance_price_goods_edit where id = ?
#sql,#1980 #SELECT #select id, apply_category_id, apply_brand_id, import_type, db_create_time,db_update_time, status, goods_qa_scheme_edit_id from goods_qa_category_scope_edit where apply_category_id = ? and apply_brand_id = ? and import_type = ? and goods_qa_scheme_edit_id <> ?
#sql,#1981 #SELECT #select id, skin_scheme_id, skin_order, skin_name, skin_introduce, skin_img_config, skin_gif_config, skin_status, operator, db_create_time, db_update_time from mt_private_custom_skin_config_edit where skin_status = ? order by skin_order asc
複製代碼

如此,算是放心不會又遺漏了。整個思路簡單直接,其中涉及到mybatis解析xml和生產動態sql的原理和過程的東西有待分析,這裏先留個坑,往後來填。

Reference:

有任何問題歡迎留言交流~

看到這裏的小夥伴,若是你喜歡這篇文章的話,別忘了轉發、收藏、留言互動

若是對文章有任何問題,歡迎在留言區和我交流~

最近我新整理了一些Java資料,包含面經分享、模擬試題、和視頻乾貨,若是你須要的話,歡迎留言or私信我

還有,關注我!關注我!關注我!

相關文章
相關標籤/搜索