1. 程式人生 > >mybatis+postgresql insert, update or delete returning *問題

mybatis+postgresql insert, update or delete returning *問題

由於各種原因,可能存在諸多不足,歡迎斧正!

       最近DBA說資料庫DB log插入insert語句時返回returning *,佔用網路頻寬,希望優化掉。其實本沒有時間檢視mybatis原始碼的,今天看了下,造成returning *的原因和解決方案如下,希望可以幫助解決相同的問題。

       先盜圖一張,說明mybatis的執行時呼叫順序,原圖出處 ,在此表示感謝:


配置:mybatis+postgresql.version 9.4-1201-jdbc4


1、下面是SimpleStatementHandler的update方法:

在MappedStatement中,有如下方法:

   public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
            this.mappedStatement.configuration = configuration;
            this.mappedStatement.id = id;
            this.mappedStatement.sqlSource = sqlSource;
            this.mappedStatement.statementType = StatementType.PREPARED;
            this.mappedStatement.parameterMap = (new org.apache.ibatis.mapping.ParameterMap.Builder(configuration, "defaultParameterMap", (Class)null, new ArrayList())).build();
            this.mappedStatement.resultMaps = new ArrayList();
            this.mappedStatement.timeout = configuration.getDefaultStatementTimeout();
            this.mappedStatement.sqlCommandType = sqlCommandType;
            //當useGeneratedKeys="true"且標籤為INSERT時,使用keyGenerator=Jdbc3KeyGenerator,後面在某些條件滿足時會導致重寫sql;否則keyGenerator=NoKeyGenerator
            this.mappedStatement.keyGenerator = (KeyGenerator)(configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)?new Jdbc3KeyGenerator():new NoKeyGenerator());
            String logId = id;
            if(configuration.getLogPrefix() != null) {
                logId = configuration.getLogPrefix() + id;
            }

            this.mappedStatement.statementLog = LogFactory.getLog(logId);
            this.mappedStatement.lang = configuration.getDefaultScriptingLanuageInstance();
        }
      如上註釋所描述的,當useGeneratedKeys="true"且標籤為INSERT時,使用keyGenerator=Jdbc3KeyGenerator,後面在某些條件滿足時會導致重寫sql;否則keyGenerator=NoKeyGenerator。
 public int update(Statement statement) throws SQLException {
        String sql = this.boundSql.getSql();
        Object parameterObject = this.boundSql.getParameterObject();
        KeyGenerator keyGenerator = this.mappedStatement.getKeyGenerator();
        int rows;
        //(keyGenerator instanceof Jdbc3KeyGenerator)滿足時,重寫sql,加上returning *
        if(keyGenerator instanceof Jdbc3KeyGenerator) {
            //點進去,會發現會重寫sql,具體語句: sql = addReturning(connection, sql, new String[]{"*"}, false);
            statement.execute(sql, 1);
            rows = statement.getUpdateCount();
            keyGenerator.processAfter(this.executor, this.mappedStatement, statement, parameterObject);
        } 
        //(keyGenerator instanceof SelectKeyGenerator)滿足時,會在執行原sql後,執行processAfter選擇keyProperty屬性
        else if(keyGenerator instanceof SelectKeyGenerator) {
            statement.execute(sql);
            rows = statement.getUpdateCount();
            keyGenerator.processAfter(this.executor, this.mappedStatement, statement, parameterObject);
        } 
        //否則,只執行原sql
        else {
            statement.execute(sql);
            rows = statement.getUpdateCount();
        }

        return rows;
    }
    Jdbc3KeyGenerator對應的statement是AbstractJdbc3Statement,上面方法中對應statement.execute(sql, 1)的方法如下:
  /**
     * Executes the given SQL statement, which may return multiple results,
     * and signals the driver that any
     * auto-generated keys should be made available
     * for retrieval.  The driver will ignore this signal if the SQL statement
     * is not an <code>INSERT</code> statement.
     * <P>
     * In some (uncommon) situations, a single SQL statement may return
     * multiple result sets and/or update counts.  Normally you can ignore
     * this unless you are (1) executing a stored procedure that you know may
     * return multiple results or (2) you are dynamically executing an
     * unknown SQL string.
     * <P>
     * The <code>execute</code> method executes an SQL statement and indicates the
     * form of the first result.  You must then use the methods
     * <code>getResultSet</code> or <code>getUpdateCount</code>
     * to retrieve the result, and <code>getMoreResults</code> to
     * move to any subsequent result(s).
     *
     * @param sql any SQL statement
     * @param autoGeneratedKeys a constant indicating whether auto-generated
     *    keys should be made available for retrieval using the method
     *    <code>getGeneratedKeys</code>; one of the following constants:
     *    <code>Statement.RETURN_GENERATED_KEYS</code> or
     *    <code>Statement.NO_GENERATED_KEYS</code>
     * @return <code>true</code> if the first result is a <code>ResultSet</code>
     *     object; <code>false</code> if it is an update count or there are
     *     no results
     * @exception SQLException if a database access error occurs
     * @see #getResultSet
     * @see #getUpdateCount
     * @see #getMoreResults
     * @see #getGeneratedKeys
     *
     * @since 1.4
     */
    public boolean execute(String sql, int autoGeneratedKeys) throws SQLException
    {
        if (autoGeneratedKeys == Statement.NO_GENERATED_KEYS)
            return execute(sql);

        sql = addReturning(connection, sql, new String[]{"*"}, false);
        wantsGeneratedKeysOnce = true;

        return execute(sql);
    }

AbstractJdbc3Statement會重寫sq,新增RETURNING *,AbstractJdbc3Statement的派生類也會未覆蓋的話也會重寫。


 (keyGenerator instanceof SelectKeyGenerator)滿足時,會在執行原sql後,執行processAfter選擇keyProperty屬性,對應方法原始碼如下:

  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        if(!this.executeBefore) {
            this.processGeneratedKeys(executor, ms, parameter);
        }

    }

    private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
        try {
            //成立條件之一:this.keyStatement.getKeyProperties() != null,即對應配置keyProperty不等於null
            if(parameter != null && this.keyStatement != null && this.keyStatement.getKeyProperties() != null) {
                String[] e = this.keyStatement.getKeyProperties();
                Configuration configuration = ms.getConfiguration();
                MetaObject metaParam = configuration.newMetaObject(parameter);
                if(e != null) {
                    Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
                    //默默的就執行查詢,比較耗費效能
                    List values = keyExecutor.query(this.keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
                    if(values.size() == 0) {
                        throw new ExecutorException("SelectKey returned no data.");
                    }

                    if(values.size() > 1) {
                        throw new ExecutorException("SelectKey returned more than one value.");
                    }

                    MetaObject metaResult = configuration.newMetaObject(values.get(0));
                    if(e.length == 1) {
                        if(metaResult.hasGetter(e[0])) {
                            this.setValue(metaParam, e[0], metaResult.getValue(e[0]));
                        } else {
                            this.setValue(metaParam, e[0], values.get(0));
                        }
                    } else {
                        this.handleMultipleProperties(e, metaParam, metaResult);
                    }
                }
            }

        } catch (ExecutorException var10) {
            throw var10;
        } catch (Exception var11) {
            throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + var11, var11);
        }
    }
 上述主要針對如下配置檔案才會選擇的SelectKeyGenerator
<insert id="add" parameterType="EStudent">
  //返回當前插入記錄的主鍵值
  <selectKey resultType="long" keyProperty="id" order="AFTER">
    select @@IDENTITY as id
  </selectKey>
  insert into TStudent(name, age) values(#{name}, #{age})
</insert>

2、在StatementHandler為PreparedStatementHandle時

 protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() != null) {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    } else {
      return connection.prepareStatement(sql);
    }
  }
 public PreparedStatement prepareStatement(String sql, String columnNames[])
    throws SQLException
    {
        if (columnNames != null && columnNames.length != 0)
            sql = AbstractJdbc3Statement.addReturning(this, sql, columnNames, true);

        PreparedStatement ps = prepareStatement(sql);

        if (columnNames != null && columnNames.length != 0)
            ((AbstractJdbc3Statement)ps).wantsGeneratedKeysAlways = true;

        return ps;
    }
 public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
    throws SQLException
    {
        checkClosed();
        if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS)
            sql = AbstractJdbc3Statement.addReturning(this, sql, new String[]{"*"}, false);

        PreparedStatement ps = prepareStatement(sql);

        if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS)
            ((AbstractJdbc3Statement)ps).wantsGeneratedKeysAlways = true;

        return ps;
    }
 
    針對PreparedStatementHandle,如果只想返回id,useGeneratedKeys="true" keyColumn="id" keyProperty="id"既可以。

    綜上所述,基本postgresql jdbc返回的都是*,即插入的完整資料。這對於網路頻寬和磁碟io都有損耗,可以說是pg jdbc的bug吧,指定的屬性keyProperty只是在返回的完整資料中選擇出來的,並不是只返回keyProperty欄位。所以說啊,要返回指定欄位首先要控制不讓mybatis自動返回,然後在sql語句後面新增returnkeyProperty

    只要刪除db機器上的insertreturning *,下面任選都可以一種 :

1、在StatementHandler為SimpleStatementHandler的前提下,任何一種都行。

     1.1、將<insert>標籤改成<update>或者其他的

     1.2、useGeneratedKeys="false",強制不返回(預設就是false)

2、在StatementHandler為CallableStatementHandler不會走到重寫sql這一步,所以不會出現returning *問題。

3、在StatementHandler為PreparedStatementHandle時,useGeneratedKeys="true" keyColumn="id" keyProperty="id"這三個就可以了

   路漫漫其修遠兮,很多時候感覺想法比較幼稚。首先東西比較簡單,其次工作也比較忙,還好週末可以抽時間處理這個。由於相關知識積累有限,歡迎大家提意見斧正,在此表示感謝!後續版本會持續更新…