MapperMethod.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.binding;

  17. import java.lang.reflect.Array;
  18. import java.lang.reflect.Method;
  19. import java.lang.reflect.ParameterizedType;
  20. import java.lang.reflect.Type;
  21. import java.util.HashMap;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Optional;

  25. import org.apache.ibatis.annotations.Flush;
  26. import org.apache.ibatis.annotations.MapKey;
  27. import org.apache.ibatis.cursor.Cursor;
  28. import org.apache.ibatis.mapping.MappedStatement;
  29. import org.apache.ibatis.mapping.SqlCommandType;
  30. import org.apache.ibatis.mapping.StatementType;
  31. import org.apache.ibatis.reflection.MetaObject;
  32. import org.apache.ibatis.reflection.ParamNameResolver;
  33. import org.apache.ibatis.reflection.TypeParameterResolver;
  34. import org.apache.ibatis.session.Configuration;
  35. import org.apache.ibatis.session.ResultHandler;
  36. import org.apache.ibatis.session.RowBounds;
  37. import org.apache.ibatis.session.SqlSession;

  38. /**
  39.  * @author Clinton Begin
  40.  * @author Eduardo Macarron
  41.  * @author Lasse Voss
  42.  * @author Kazuki Shimizu
  43.  */
  44. public class MapperMethod {

  45.   private final SqlCommand command;
  46.   private final MethodSignature method;

  47.   public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
  48.     this.command = new SqlCommand(config, mapperInterface, method);
  49.     this.method = new MethodSignature(config, mapperInterface, method);
  50.   }

  51.   public Object execute(SqlSession sqlSession, Object[] args) {
  52.     Object result;
  53.     switch (command.getType()) {
  54.       case INSERT: {
  55.         Object param = method.convertArgsToSqlCommandParam(args);
  56.         result = rowCountResult(sqlSession.insert(command.getName(), param));
  57.         break;
  58.       }
  59.       case UPDATE: {
  60.         Object param = method.convertArgsToSqlCommandParam(args);
  61.         result = rowCountResult(sqlSession.update(command.getName(), param));
  62.         break;
  63.       }
  64.       case DELETE: {
  65.         Object param = method.convertArgsToSqlCommandParam(args);
  66.         result = rowCountResult(sqlSession.delete(command.getName(), param));
  67.         break;
  68.       }
  69.       case SELECT:
  70.         if (method.returnsVoid() && method.hasResultHandler()) {
  71.           executeWithResultHandler(sqlSession, args);
  72.           result = null;
  73.         } else if (method.returnsMany()) {
  74.           result = executeForMany(sqlSession, args);
  75.         } else if (method.returnsMap()) {
  76.           result = executeForMap(sqlSession, args);
  77.         } else if (method.returnsCursor()) {
  78.           result = executeForCursor(sqlSession, args);
  79.         } else {
  80.           Object param = method.convertArgsToSqlCommandParam(args);
  81.           result = sqlSession.selectOne(command.getName(), param);
  82.           if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
  83.             result = Optional.ofNullable(result);
  84.           }
  85.         }
  86.         break;
  87.       case FLUSH:
  88.         result = sqlSession.flushStatements();
  89.         break;
  90.       default:
  91.         throw new BindingException("Unknown execution method for: " + command.getName());
  92.     }
  93.     if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
  94.       throw new BindingException("Mapper method '" + command.getName()
  95.           + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  96.     }
  97.     return result;
  98.   }

  99.   private Object rowCountResult(int rowCount) {
  100.     final Object result;
  101.     if (method.returnsVoid()) {
  102.       result = null;
  103.     } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
  104.       result = rowCount;
  105.     } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
  106.       result = (long) rowCount;
  107.     } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
  108.       result = rowCount > 0;
  109.     } else {
  110.       throw new BindingException(
  111.           "Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
  112.     }
  113.     return result;
  114.   }

  115.   private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
  116.     MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
  117.     if (!StatementType.CALLABLE.equals(ms.getStatementType())
  118.         && void.class.equals(ms.getResultMaps().get(0).getType())) {
  119.       throw new BindingException(
  120.           "method " + command.getName() + " needs either a @ResultMap annotation, a @ResultType annotation,"
  121.               + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
  122.     }
  123.     Object param = method.convertArgsToSqlCommandParam(args);
  124.     if (method.hasRowBounds()) {
  125.       RowBounds rowBounds = method.extractRowBounds(args);
  126.       sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
  127.     } else {
  128.       sqlSession.select(command.getName(), param, method.extractResultHandler(args));
  129.     }
  130.   }

  131.   private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  132.     List<E> result;
  133.     Object param = method.convertArgsToSqlCommandParam(args);
  134.     if (method.hasRowBounds()) {
  135.       RowBounds rowBounds = method.extractRowBounds(args);
  136.       result = sqlSession.selectList(command.getName(), param, rowBounds);
  137.     } else {
  138.       result = sqlSession.selectList(command.getName(), param);
  139.     }
  140.     // issue #510 Collections & arrays support
  141.     if (!method.getReturnType().isAssignableFrom(result.getClass())) {
  142.       if (method.getReturnType().isArray()) {
  143.         return convertToArray(result);
  144.       }
  145.       return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
  146.     }
  147.     return result;
  148.   }

  149.   private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
  150.     Cursor<T> result;
  151.     Object param = method.convertArgsToSqlCommandParam(args);
  152.     if (method.hasRowBounds()) {
  153.       RowBounds rowBounds = method.extractRowBounds(args);
  154.       result = sqlSession.selectCursor(command.getName(), param, rowBounds);
  155.     } else {
  156.       result = sqlSession.selectCursor(command.getName(), param);
  157.     }
  158.     return result;
  159.   }

  160.   private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
  161.     Object collection = config.getObjectFactory().create(method.getReturnType());
  162.     MetaObject metaObject = config.newMetaObject(collection);
  163.     metaObject.addAll(list);
  164.     return collection;
  165.   }

  166.   @SuppressWarnings("unchecked")
  167.   private <E> Object convertToArray(List<E> list) {
  168.     Class<?> arrayComponentType = method.getReturnType().getComponentType();
  169.     Object array = Array.newInstance(arrayComponentType, list.size());
  170.     if (!arrayComponentType.isPrimitive()) {
  171.       return list.toArray((E[]) array);
  172.     }
  173.     for (int i = 0; i < list.size(); i++) {
  174.       Array.set(array, i, list.get(i));
  175.     }
  176.     return array;
  177.   }

  178.   private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
  179.     Map<K, V> result;
  180.     Object param = method.convertArgsToSqlCommandParam(args);
  181.     if (method.hasRowBounds()) {
  182.       RowBounds rowBounds = method.extractRowBounds(args);
  183.       result = sqlSession.selectMap(command.getName(), param, method.getMapKey(), rowBounds);
  184.     } else {
  185.       result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
  186.     }
  187.     return result;
  188.   }

  189.   public static class ParamMap<V> extends HashMap<String, V> {

  190.     private static final long serialVersionUID = -2212268410512043556L;

  191.     @Override
  192.     public V get(Object key) {
  193.       if (!super.containsKey(key)) {
  194.         throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
  195.       }
  196.       return super.get(key);
  197.     }

  198.   }

  199.   public static class SqlCommand {

  200.     private final String name;
  201.     private final SqlCommandType type;

  202.     public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
  203.       final String methodName = method.getName();
  204.       final Class<?> declaringClass = method.getDeclaringClass();
  205.       MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
  206.       if (ms == null) {
  207.         if (method.getAnnotation(Flush.class) == null) {
  208.           throw new BindingException(
  209.               "Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName);
  210.         }
  211.         name = null;
  212.         type = SqlCommandType.FLUSH;
  213.       } else {
  214.         name = ms.getId();
  215.         type = ms.getSqlCommandType();
  216.         if (type == SqlCommandType.UNKNOWN) {
  217.           throw new BindingException("Unknown execution method for: " + name);
  218.         }
  219.       }
  220.     }

  221.     public String getName() {
  222.       return name;
  223.     }

  224.     public SqlCommandType getType() {
  225.       return type;
  226.     }

  227.     private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass,
  228.         Configuration configuration) {
  229.       String statementId = mapperInterface.getName() + "." + methodName;
  230.       if (configuration.hasStatement(statementId)) {
  231.         return configuration.getMappedStatement(statementId);
  232.       }
  233.       if (mapperInterface.equals(declaringClass)) {
  234.         return null;
  235.       }
  236.       for (Class<?> superInterface : mapperInterface.getInterfaces()) {
  237.         if (declaringClass.isAssignableFrom(superInterface)) {
  238.           MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration);
  239.           if (ms != null) {
  240.             return ms;
  241.           }
  242.         }
  243.       }
  244.       return null;
  245.     }
  246.   }

  247.   public static class MethodSignature {

  248.     private final boolean returnsMany;
  249.     private final boolean returnsMap;
  250.     private final boolean returnsVoid;
  251.     private final boolean returnsCursor;
  252.     private final boolean returnsOptional;
  253.     private final Class<?> returnType;
  254.     private final String mapKey;
  255.     private final Integer resultHandlerIndex;
  256.     private final Integer rowBoundsIndex;
  257.     private final ParamNameResolver paramNameResolver;

  258.     public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
  259.       Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
  260.       if (resolvedReturnType instanceof Class<?>) {
  261.         this.returnType = (Class<?>) resolvedReturnType;
  262.       } else if (resolvedReturnType instanceof ParameterizedType) {
  263.         this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
  264.       } else {
  265.         this.returnType = method.getReturnType();
  266.       }
  267.       this.returnsVoid = void.class.equals(this.returnType);
  268.       this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
  269.       this.returnsCursor = Cursor.class.equals(this.returnType);
  270.       this.returnsOptional = Optional.class.equals(this.returnType);
  271.       this.mapKey = getMapKey(method);
  272.       this.returnsMap = this.mapKey != null;
  273.       this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
  274.       this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
  275.       this.paramNameResolver = new ParamNameResolver(configuration, method);
  276.     }

  277.     public Object convertArgsToSqlCommandParam(Object[] args) {
  278.       return paramNameResolver.getNamedParams(args);
  279.     }

  280.     public boolean hasRowBounds() {
  281.       return rowBoundsIndex != null;
  282.     }

  283.     public RowBounds extractRowBounds(Object[] args) {
  284.       return hasRowBounds() ? (RowBounds) args[rowBoundsIndex] : null;
  285.     }

  286.     public boolean hasResultHandler() {
  287.       return resultHandlerIndex != null;
  288.     }

  289.     public ResultHandler extractResultHandler(Object[] args) {
  290.       return hasResultHandler() ? (ResultHandler) args[resultHandlerIndex] : null;
  291.     }

  292.     public Class<?> getReturnType() {
  293.       return returnType;
  294.     }

  295.     public boolean returnsMany() {
  296.       return returnsMany;
  297.     }

  298.     public boolean returnsMap() {
  299.       return returnsMap;
  300.     }

  301.     public boolean returnsVoid() {
  302.       return returnsVoid;
  303.     }

  304.     public boolean returnsCursor() {
  305.       return returnsCursor;
  306.     }

  307.     /**
  308.      * return whether return type is {@code java.util.Optional}.
  309.      *
  310.      * @return return {@code true}, if return type is {@code java.util.Optional}
  311.      *
  312.      * @since 3.5.0
  313.      */
  314.     public boolean returnsOptional() {
  315.       return returnsOptional;
  316.     }

  317.     private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
  318.       Integer index = null;
  319.       final Class<?>[] argTypes = method.getParameterTypes();
  320.       for (int i = 0; i < argTypes.length; i++) {
  321.         if (paramType.isAssignableFrom(argTypes[i])) {
  322.           if (index != null) {
  323.             throw new BindingException(
  324.                 method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
  325.           }
  326.           index = i;
  327.         }
  328.       }
  329.       return index;
  330.     }

  331.     public String getMapKey() {
  332.       return mapKey;
  333.     }

  334.     private String getMapKey(Method method) {
  335.       String mapKey = null;
  336.       if (Map.class.isAssignableFrom(method.getReturnType())) {
  337.         final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
  338.         if (mapKeyAnnotation != null) {
  339.           mapKey = mapKeyAnnotation.value();
  340.         }
  341.       }
  342.       return mapKey;
  343.     }
  344.   }

  345. }