Jdbc3KeyGenerator.java

  1. /*
  2.  *    Copyright 2009-2024 the original author or authors.
  3.  *
  4.  *    Licensed under the Apache License, Version 2.0 (the "License");
  5.  *    you may not use this file except in compliance with the License.
  6.  *    You may obtain a copy of the License at
  7.  *
  8.  *       https://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  *    Unless required by applicable law or agreed to in writing, software
  11.  *    distributed under the License is distributed on an "AS IS" BASIS,
  12.  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  *    See the License for the specific language governing permissions and
  14.  *    limitations under the License.
  15.  */
  16. package org.apache.ibatis.executor.keygen;

  17. import java.sql.ResultSet;
  18. import java.sql.ResultSetMetaData;
  19. import java.sql.SQLException;
  20. import java.sql.Statement;
  21. import java.util.ArrayList;
  22. import java.util.Arrays;
  23. import java.util.Collection;
  24. import java.util.HashMap;
  25. import java.util.Iterator;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.Map.Entry;
  29. import java.util.Set;

  30. import org.apache.ibatis.binding.MapperMethod.ParamMap;
  31. import org.apache.ibatis.executor.Executor;
  32. import org.apache.ibatis.executor.ExecutorException;
  33. import org.apache.ibatis.mapping.MappedStatement;
  34. import org.apache.ibatis.reflection.ArrayUtil;
  35. import org.apache.ibatis.reflection.MetaObject;
  36. import org.apache.ibatis.reflection.ParamNameResolver;
  37. import org.apache.ibatis.session.Configuration;
  38. import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;
  39. import org.apache.ibatis.type.JdbcType;
  40. import org.apache.ibatis.type.TypeHandler;
  41. import org.apache.ibatis.type.TypeHandlerRegistry;
  42. import org.apache.ibatis.util.MapUtil;

  43. /**
  44.  * @author Clinton Begin
  45.  * @author Kazuki Shimizu
  46.  */
  47. public class Jdbc3KeyGenerator implements KeyGenerator {

  48.   private static final String SECOND_GENERIC_PARAM_NAME = ParamNameResolver.GENERIC_NAME_PREFIX + "2";

  49.   /**
  50.    * A shared instance.
  51.    *
  52.    * @since 3.4.3
  53.    */
  54.   public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();

  55.   private static final String MSG_TOO_MANY_KEYS = "Too many keys are generated. There are only %d target objects. "
  56.       + "You either specified a wrong 'keyProperty' or encountered a driver bug like #1523.";

  57.   @Override
  58.   public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
  59.     // do nothing
  60.   }

  61.   @Override
  62.   public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
  63.     processBatch(ms, stmt, parameter);
  64.   }

  65.   public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
  66.     final String[] keyProperties = ms.getKeyProperties();
  67.     if (keyProperties == null || keyProperties.length == 0) {
  68.       return;
  69.     }
  70.     try (ResultSet rs = stmt.getGeneratedKeys()) {
  71.       final ResultSetMetaData rsmd = rs.getMetaData();
  72.       final Configuration configuration = ms.getConfiguration();
  73.       if (rsmd.getColumnCount() < keyProperties.length) {
  74.         // Error?
  75.       } else {
  76.         assignKeys(configuration, rs, rsmd, keyProperties, parameter);
  77.       }
  78.     } catch (Exception e) {
  79.       throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
  80.     }
  81.   }

  82.   @SuppressWarnings("unchecked")
  83.   private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
  84.       Object parameter) throws SQLException {
  85.     if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
  86.       // Multi-param or single param with @Param
  87.       assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
  88.     } else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
  89.         && ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
  90.       // Multi-param or single param with @Param in batch operation
  91.       assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
  92.     } else {
  93.       // Single param without @Param
  94.       assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
  95.     }
  96.   }

  97.   private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
  98.       String[] keyProperties, Object parameter) throws SQLException {
  99.     Collection<?> params = collectionize(parameter);
  100.     if (params.isEmpty()) {
  101.       return;
  102.     }
  103.     List<KeyAssigner> assignerList = new ArrayList<>();
  104.     for (int i = 0; i < keyProperties.length; i++) {
  105.       assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
  106.     }
  107.     Iterator<?> iterator = params.iterator();
  108.     while (rs.next()) {
  109.       if (!iterator.hasNext()) {
  110.         throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
  111.       }
  112.       Object param = iterator.next();
  113.       assignerList.forEach(x -> x.assign(rs, param));
  114.     }
  115.   }

  116.   private void assignKeysToParamMapList(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
  117.       String[] keyProperties, ArrayList<ParamMap<?>> paramMapList) throws SQLException {
  118.     Iterator<ParamMap<?>> iterator = paramMapList.iterator();
  119.     List<KeyAssigner> assignerList = new ArrayList<>();
  120.     long counter = 0;
  121.     while (rs.next()) {
  122.       if (!iterator.hasNext()) {
  123.         throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
  124.       }
  125.       ParamMap<?> paramMap = iterator.next();
  126.       if (assignerList.isEmpty()) {
  127.         for (int i = 0; i < keyProperties.length; i++) {
  128.           assignerList
  129.               .add(getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, false)
  130.                   .getValue());
  131.         }
  132.       }
  133.       assignerList.forEach(x -> x.assign(rs, paramMap));
  134.       counter++;
  135.     }
  136.   }

  137.   private void assignKeysToParamMap(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
  138.       String[] keyProperties, Map<String, ?> paramMap) throws SQLException {
  139.     if (paramMap.isEmpty()) {
  140.       return;
  141.     }
  142.     Map<String, Entry<Iterator<?>, List<KeyAssigner>>> assignerMap = new HashMap<>();
  143.     for (int i = 0; i < keyProperties.length; i++) {
  144.       Entry<String, KeyAssigner> entry = getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i],
  145.           keyProperties, true);
  146.       Entry<Iterator<?>, List<KeyAssigner>> iteratorPair = MapUtil.computeIfAbsent(assignerMap, entry.getKey(),
  147.           k -> MapUtil.entry(collectionize(paramMap.get(k)).iterator(), new ArrayList<>()));
  148.       iteratorPair.getValue().add(entry.getValue());
  149.     }
  150.     long counter = 0;
  151.     while (rs.next()) {
  152.       for (Entry<Iterator<?>, List<KeyAssigner>> pair : assignerMap.values()) {
  153.         if (!pair.getKey().hasNext()) {
  154.           throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
  155.         }
  156.         Object param = pair.getKey().next();
  157.         pair.getValue().forEach(x -> x.assign(rs, param));
  158.       }
  159.       counter++;
  160.     }
  161.   }

  162.   private Entry<String, KeyAssigner> getAssignerForParamMap(Configuration config, ResultSetMetaData rsmd,
  163.       int columnPosition, Map<String, ?> paramMap, String keyProperty, String[] keyProperties, boolean omitParamName) {
  164.     Set<String> keySet = paramMap.keySet();
  165.     // A caveat : if the only parameter has {@code @Param("param2")} on it,
  166.     // it must be referenced with param name e.g. 'param2.x'.
  167.     boolean singleParam = !keySet.contains(SECOND_GENERIC_PARAM_NAME);
  168.     int firstDot = keyProperty.indexOf('.');
  169.     if (firstDot == -1) {
  170.       if (singleParam) {
  171.         return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
  172.       }
  173.       throw new ExecutorException("Could not determine which parameter to assign generated keys to. "
  174.           + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
  175.           + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
  176.           + keySet);
  177.     }
  178.     String paramName = keyProperty.substring(0, firstDot);
  179.     if (keySet.contains(paramName)) {
  180.       String argParamName = omitParamName ? null : paramName;
  181.       String argKeyProperty = keyProperty.substring(firstDot + 1);
  182.       return MapUtil.entry(paramName, new KeyAssigner(config, rsmd, columnPosition, argParamName, argKeyProperty));
  183.     }
  184.     if (singleParam) {
  185.       return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
  186.     } else {
  187.       throw new ExecutorException("Could not find parameter '" + paramName + "'. "
  188.           + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
  189.           + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
  190.           + keySet);
  191.     }
  192.   }

  193.   private Entry<String, KeyAssigner> getAssignerForSingleParam(Configuration config, ResultSetMetaData rsmd,
  194.       int columnPosition, Map<String, ?> paramMap, String keyProperty, boolean omitParamName) {
  195.     // Assume 'keyProperty' to be a property of the single param.
  196.     String singleParamName = nameOfSingleParam(paramMap);
  197.     String argParamName = omitParamName ? null : singleParamName;
  198.     return MapUtil.entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty));
  199.   }

  200.   private static String nameOfSingleParam(Map<String, ?> paramMap) {
  201.     // There is virtually one parameter, so any key works.
  202.     return paramMap.keySet().iterator().next();
  203.   }

  204.   private static Collection<?> collectionize(Object param) {
  205.     if (param instanceof Collection) {
  206.       return (Collection<?>) param;
  207.     }
  208.     if (param instanceof Object[]) {
  209.       return Arrays.asList((Object[]) param);
  210.     } else {
  211.       return Arrays.asList(param);
  212.     }
  213.   }

  214.   private static class KeyAssigner {
  215.     private final Configuration configuration;
  216.     private final ResultSetMetaData rsmd;
  217.     private final TypeHandlerRegistry typeHandlerRegistry;
  218.     private final int columnPosition;
  219.     private final String paramName;
  220.     private final String propertyName;
  221.     private TypeHandler<?> typeHandler;

  222.     protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName,
  223.         String propertyName) {
  224.       this.configuration = configuration;
  225.       this.rsmd = rsmd;
  226.       this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  227.       this.columnPosition = columnPosition;
  228.       this.paramName = paramName;
  229.       this.propertyName = propertyName;
  230.     }

  231.     protected void assign(ResultSet rs, Object param) {
  232.       if (paramName != null) {
  233.         // If paramName is set, param is ParamMap
  234.         param = ((ParamMap<?>) param).get(paramName);
  235.       }
  236.       MetaObject metaParam = configuration.newMetaObject(param);
  237.       try {
  238.         if (typeHandler == null) {
  239.           if (!metaParam.hasSetter(propertyName)) {
  240.             throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
  241.                 + metaParam.getOriginalObject().getClass().getName() + "'.");
  242.           }
  243.           Class<?> propertyType = metaParam.getSetterType(propertyName);
  244.           typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
  245.               JdbcType.forCode(rsmd.getColumnType(columnPosition)));
  246.         }
  247.         if (typeHandler == null) {
  248.           // Error?
  249.         } else {
  250.           Object value = typeHandler.getResult(rs, columnPosition);
  251.           metaParam.setValue(propertyName, value);
  252.         }
  253.       } catch (SQLException e) {
  254.         throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
  255.             e);
  256.       }
  257.     }
  258.   }
  259. }