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中,有如下方法:
如上註釋所描述的,當useGeneratedKeys="true"且標籤為INSERT時,使用keyGenerator=Jdbc3KeyGenerator,後面在某些條件滿足時會導致重寫sql;否則keyGenerator=NoKeyGenerator。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(); }
Jdbc3KeyGenerator對應的statement是AbstractJdbc3Statement,上面方法中對應statement.execute(sql, 1)的方法如下: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; }
/**
* 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"這三個就可以了
路漫漫其修遠兮,很多時候感覺想法比較幼稚。首先東西比較簡單,其次工作也比較忙,還好週末可以抽時間處理這個。由於相關知識積累有限,歡迎大家提意見斧正,在此表示感謝!後續版本會持續更新…