SqlMapClientTemplate.java

/*
 * SPDX-License-Identifier: Apache-2.0
 * See LICENSE file for details.
 *
 * Copyright 2015-2026 the original author or authors.
 */
package org.springframework.orm.ibatis;

import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.client.SqlMapExecutor;
import com.ibatis.sqlmap.client.SqlMapSession;
import com.ibatis.sqlmap.client.event.RowHandler;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.jdbc.support.JdbcAccessor;
import org.springframework.util.Assert;

/**
 * Helper class that simplifies data access via the iBATIS {@link com.ibatis.sqlmap.client.SqlMapClient} API, converting
 * checked SQLExceptions into unchecked DataAccessExceptions, following the {@code org.springframework.dao} exception
 * hierarchy. Uses the same {@link org.springframework.jdbc.support.SQLExceptionTranslator} mechanism as
 * {@link org.springframework.jdbc.core.JdbcTemplate}.
 * <p>
 * The main method of this class executes a callback that implements a data access action. Furthermore, this class
 * provides numerous convenience methods that mirror {@link com.ibatis.sqlmap.client.SqlMapExecutor}'s execution
 * methods.
 * <p>
 * It is generally recommended to use the convenience methods on this template for plain query/insert/update/delete
 * operations. However, for more complex operations like batch updates, a custom SqlMapClientCallback must be
 * implemented, usually as anonymous inner class. For example:
 *
 * <pre class="code">
 * getSqlMapClientTemplate().execute(new SqlMapClientCallback() {
 *   public Object doInSqlMapClient(SqlMapExecutor executor) throws SQLException {
 *     executor.startBatch();
 *     executor.update("insertSomething", "myParamValue");
 *     executor.update("insertSomethingElse", "myOtherParamValue");
 *     executor.executeBatch();
 *     return null;
 *   }
 * });
 * </pre>
 *
 * The template needs a SqlMapClient to work on, passed in via the "sqlMapClient" property. A Spring context typically
 * uses a {@link SqlMapClientFactoryBean} to build the SqlMapClient. The template can additionally be configured with a
 * DataSource for fetching Connections, although this is not necessary if a DataSource is specified for the SqlMapClient
 * itself (typically through SqlMapClientFactoryBean's "dataSource" property).
 *
 * @author Juergen Hoeller
 *
 * @since 24.02.2004
 *
 * @see #execute
 * @see #setSqlMapClient
 * @see #setDataSource
 * @see #setExceptionTranslator
 * @see SqlMapClientFactoryBean#setDataSource
 * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource
 * @see com.ibatis.sqlmap.client.SqlMapExecutor
 *
 * @deprecated as of Spring 3.2, in favor of the native Spring support in the Mybatis follow-up project
 *             (https://mybatis.org/)
 */
@Deprecated
@SuppressWarnings("rawtypes")
public class SqlMapClientTemplate extends JdbcAccessor implements SqlMapClientOperations {

  private SqlMapClient sqlMapClient;

  /**
   * Create a new SqlMapClientTemplate.
   */
  public SqlMapClientTemplate() {
  }

  /**
   * Create a new SqlMapTemplate.
   *
   * @param sqlMapClient
   *          iBATIS SqlMapClient that defines the mapped statements
   */
  public SqlMapClientTemplate(SqlMapClient sqlMapClient) {
    setSqlMapClient(sqlMapClient);
    afterPropertiesSet();
  }

  /**
   * Create a new SqlMapTemplate.
   *
   * @param dataSource
   *          JDBC DataSource to obtain connections from
   * @param sqlMapClient
   *          iBATIS SqlMapClient that defines the mapped statements
   */
  public SqlMapClientTemplate(DataSource dataSource, SqlMapClient sqlMapClient) {
    setDataSource(dataSource);
    setSqlMapClient(sqlMapClient);
    afterPropertiesSet();
  }

  /**
   * Set the iBATIS Database Layer SqlMapClient that defines the mapped statements.
   *
   * @param sqlMapClient
   *          the SqlMapClient to use
   */
  public void setSqlMapClient(SqlMapClient sqlMapClient) {
    this.sqlMapClient = sqlMapClient;
  }

  /**
   * Return the iBATIS Database Layer SqlMapClient that this template works with.
   *
   * @return the SqlMapClient
   */
  public SqlMapClient getSqlMapClient() {
    return this.sqlMapClient;
  }

  /**
   * If no DataSource specified, use SqlMapClient's DataSource.
   *
   * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource()
   */
  @Override
  public DataSource getDataSource() {
    DataSource ds = super.getDataSource();
    return (ds != null ? ds : this.sqlMapClient.getDataSource());
  }

  @Override
  public void afterPropertiesSet() {
    if (this.sqlMapClient == null) {
      throw new IllegalArgumentException("Property 'sqlMapClient' is required");
    }
    super.afterPropertiesSet();
  }

  /**
   * Execute the given data access action on a SqlMapExecutor.
   *
   * @param action
   *          callback object that specifies the data access action
   * @param <T>
   *          the result type
   *
   * @return a result object returned by the action, or {@code null}
   *
   * @throws DataAccessException
   *           in case of SQL Maps errors
   */
  public <T> T execute(SqlMapClientCallback<T> action) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");
    Assert.notNull(this.sqlMapClient, "No SqlMapClient specified");

    // We always need to use a SqlMapSession, as we need to pass a Spring-managed
    // Connection (potentially transactional) in. This shouldn't be necessary if
    // we run against a TransactionAwareDataSourceProxy underneath, but unfortunately
    // we still need it to make iBATIS batch execution work properly: If iBATIS
    // doesn't recognize an existing transaction, it automatically executes the
    // batch for every single statement...

    SqlMapSession session = this.sqlMapClient.openSession();
    if (logger.isDebugEnabled()) {
      logger.debug("Opened SqlMapSession [" + session + "] for iBATIS operation");
    }
    Connection ibatisCon = null;

    try {
      Connection springCon = null;
      DataSource dataSource = getDataSource();
      boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy);

      // Obtain JDBC Connection to operate on...
      try {
        ibatisCon = session.getCurrentConnection();
        if (ibatisCon == null) {
          springCon = (transactionAware ? dataSource.getConnection() : DataSourceUtils.doGetConnection(dataSource));
          session.setUserConnection(springCon);
          if (logger.isDebugEnabled()) {
            logger.debug("Obtained JDBC Connection [" + springCon + "] for iBATIS operation");
          }
        } else {
          if (logger.isDebugEnabled()) {
            logger.debug("Reusing JDBC Connection [" + ibatisCon + "] for iBATIS operation");
          }
        }
      } catch (SQLException ex) {
        throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
      }

      // Execute given callback...
      try {
        return action.doInSqlMapClient(session);
      } catch (SQLException ex) {
        throw getExceptionTranslator().translate("SqlMapClient operation", null, ex);
      } finally {
        try {
          if (springCon != null) {
            if (transactionAware) {
              springCon.close();
            } else {
              DataSourceUtils.doReleaseConnection(springCon, dataSource);
            }
          }
        } catch (Throwable ex) {
          logger.debug("Could not close JDBC Connection", ex);
        }
      }

      // Processing finished - potentially session still to be closed.
    } finally {
      // Only close SqlMapSession if we know we've actually opened it
      // at the present level.
      if (ibatisCon == null) {
        session.close();
      }
    }
  }

  /**
   * Execute the given data access action on a SqlMapExecutor, expecting a List result.
   *
   * @param action
   *          callback object that specifies the data access action
   *
   * @return the List result
   *
   * @throws DataAccessException
   *           in case of SQL Maps errors
   *
   * @deprecated as of Spring 3.0 - not really needed anymore with generic {@link #execute} method
   */
  @Deprecated
  public List executeWithListResult(SqlMapClientCallback<List> action) throws DataAccessException {
    return execute(action);
  }

  /**
   * Execute the given data access action on a SqlMapExecutor, expecting a Map result.
   *
   * @param action
   *          callback object that specifies the data access action
   *
   * @return the Map result
   *
   * @throws DataAccessException
   *           in case of SQL Maps errors
   *
   * @deprecated as of Spring 3.0 - not really needed anymore with generic {@link #execute} method
   */
  @Deprecated
  public Map executeWithMapResult(SqlMapClientCallback<Map> action) throws DataAccessException {
    return execute(action);
  }

  @Override
  public Object queryForObject(String statementName) throws DataAccessException {
    return queryForObject(statementName, null);
  }

  @Override
  public Object queryForObject(final String statementName, final Object parameterObject) throws DataAccessException {
    return execute(executor -> executor.queryForObject(statementName, parameterObject));
  }

  @Override
  public Object queryForObject(final String statementName, final Object parameterObject, final Object resultObject)
      throws DataAccessException {
    return execute(executor -> executor.queryForObject(statementName, parameterObject, resultObject));
  }

  @Override
  public List queryForList(String statementName) throws DataAccessException {
    return queryForList(statementName, null);
  }

  @Override
  public List queryForList(final String statementName, final Object parameterObject) throws DataAccessException {
    return execute(executor -> executor.queryForList(statementName, parameterObject));
  }

  @Override
  public List queryForList(String statementName, int skipResults, int maxResults) throws DataAccessException {
    return queryForList(statementName, null, skipResults, maxResults);
  }

  @Override
  public List queryForList(final String statementName, final Object parameterObject, final int skipResults,
      final int maxResults) throws DataAccessException {
    return execute(executor -> executor.queryForList(statementName, parameterObject, skipResults, maxResults));
  }

  @Override
  public void queryWithRowHandler(String statementName, RowHandler rowHandler) throws DataAccessException {
    queryWithRowHandler(statementName, null, rowHandler);
  }

  @Override
  public void queryWithRowHandler(final String statementName, final Object parameterObject, final RowHandler rowHandler)
      throws DataAccessException {
    execute(executor -> {
      executor.queryWithRowHandler(statementName, parameterObject, rowHandler);
      return null;
    });
  }

  @Override
  public Map queryForMap(final String statementName, final Object parameterObject, final String keyProperty)
      throws DataAccessException {
    return execute(executor -> executor.queryForMap(statementName, parameterObject, keyProperty));
  }

  @Override
  public Map queryForMap(final String statementName, final Object parameterObject, final String keyProperty,
      final String valueProperty) throws DataAccessException {
    return execute(executor -> executor.queryForMap(statementName, parameterObject, keyProperty, valueProperty));
  }

  @Override
  public Object insert(String statementName) throws DataAccessException {
    return insert(statementName, null);
  }

  @Override
  public Object insert(final String statementName, final Object parameterObject) throws DataAccessException {
    return execute(executor -> executor.insert(statementName, parameterObject));
  }

  @Override
  public int update(String statementName) throws DataAccessException {
    return update(statementName, null);
  }

  @Override
  public int update(final String statementName, final Object parameterObject) throws DataAccessException {
    Integer result = execute(executor -> executor.update(statementName, parameterObject));
    return (result != null ? result : 0);
  }

  @Override
  public void update(String statementName, Object parameterObject, int requiredRowsAffected)
      throws DataAccessException {
    int actualRowsAffected = update(statementName, parameterObject);
    if (actualRowsAffected != requiredRowsAffected) {
      throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(statementName, requiredRowsAffected,
          actualRowsAffected);
    }
  }

  @Override
  public int delete(String statementName) throws DataAccessException {
    return delete(statementName, null);
  }

  @Override
  public int delete(final String statementName, final Object parameterObject) throws DataAccessException {
    Integer result = execute(executor -> executor.delete(statementName, parameterObject));
    return (result != null ? result : 0);
  }

  @Override
  public void delete(String statementName, Object parameterObject, int requiredRowsAffected)
      throws DataAccessException {
    int actualRowsAffected = delete(statementName, parameterObject);
    if (actualRowsAffected != requiredRowsAffected) {
      throw new JdbcUpdateAffectedIncorrectNumberOfRowsException(statementName, requiredRowsAffected,
          actualRowsAffected);
    }
  }

}