001/*
002 *  Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.mybatisflex.core.keygen;
017
018import com.mybatisflex.core.FlexConsts;
019import com.mybatisflex.core.util.MapUtil;
020import org.apache.ibatis.binding.MapperMethod.ParamMap;
021import org.apache.ibatis.executor.Executor;
022import org.apache.ibatis.executor.ExecutorException;
023import org.apache.ibatis.executor.keygen.KeyGenerator;
024import org.apache.ibatis.mapping.MappedStatement;
025import org.apache.ibatis.reflection.ArrayUtil;
026import org.apache.ibatis.reflection.MetaObject;
027import org.apache.ibatis.reflection.ParamNameResolver;
028import org.apache.ibatis.session.Configuration;
029import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;
030import org.apache.ibatis.type.JdbcType;
031import org.apache.ibatis.type.TypeHandler;
032import org.apache.ibatis.type.TypeHandlerRegistry;
033
034import java.sql.ResultSet;
035import java.sql.ResultSetMetaData;
036import java.sql.SQLException;
037import java.sql.Statement;
038import java.util.*;
039import java.util.Map.Entry;
040
041/**
042 * 主要作用是为 Row 生成自增主键
043 */
044public class RowJdbc3KeyGenerator implements KeyGenerator {
045
046    private final String keyProperty;
047
048
049    private static final String SECOND_GENERIC_PARAM_NAME = ParamNameResolver.GENERIC_NAME_PREFIX + "2";
050    private static final String MSG_TOO_MANY_KEYS = "Too many keys are generated. There are only %d target objects. "
051        + "You either specified a wrong 'keyProperty' or encountered a driver bug like #1523.";
052
053
054    public RowJdbc3KeyGenerator(String keyProperty) {
055        this.keyProperty = keyProperty;
056    }
057
058    @Override
059    public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
060        // do nothing
061    }
062
063    @Override
064    public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
065        processBatch(ms, stmt, parameter);
066    }
067
068    public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
069        String[] keyProperties = new String[]{FlexConsts.ROW + "." + keyProperty};
070        try (ResultSet rs = stmt.getGeneratedKeys()) {
071            final ResultSetMetaData rsmd = rs.getMetaData();
072            final Configuration configuration = ms.getConfiguration();
073            if (rsmd.getColumnCount() < keyProperties.length) {
074                // Error?
075            } else {
076                assignKeys(configuration, rs, rsmd, keyProperties, parameter);
077            }
078        } catch (Exception e) {
079            throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
080        }
081    }
082
083    @SuppressWarnings("unchecked")
084    private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
085                            Object parameter) throws SQLException {
086        if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
087            // Multi-param or single param with @Param
088            assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
089        } else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
090            && ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
091            // Multi-param or single param with @Param in batch operation
092            assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
093        } else {
094            // Single param without @Param
095            assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
096        }
097    }
098
099    private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
100                                   String[] keyProperties, Object parameter) throws SQLException {
101        Collection<?> params = collectionize(parameter);
102        if (params.isEmpty()) {
103            return;
104        }
105        List<KeyAssigner> assignerList = new ArrayList<>();
106        for (int i = 0; i < keyProperties.length; i++) {
107            assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
108        }
109        Iterator<?> iterator = params.iterator();
110        while (rs.next()) {
111            if (!iterator.hasNext()) {
112                throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
113            }
114            Object param = iterator.next();
115            assignerList.forEach(x -> x.assign(rs, param));
116        }
117    }
118
119    private void assignKeysToParamMapList(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
120                                          String[] keyProperties, ArrayList<ParamMap<?>> paramMapList) throws SQLException {
121        Iterator<ParamMap<?>> iterator = paramMapList.iterator();
122        List<KeyAssigner> assignerList = new ArrayList<>();
123        long counter = 0;
124        while (rs.next()) {
125            if (!iterator.hasNext()) {
126                throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
127            }
128            ParamMap<?> paramMap = iterator.next();
129            if (assignerList.isEmpty()) {
130                for (int i = 0; i < keyProperties.length; i++) {
131                    assignerList
132                        .add(getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, false)
133                            .getValue());
134                }
135            }
136            assignerList.forEach(x -> x.assign(rs, paramMap));
137            counter++;
138        }
139    }
140
141    private void assignKeysToParamMap(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
142                                      String[] keyProperties, Map<String, ?> paramMap) throws SQLException {
143        if (paramMap.isEmpty()) {
144            return;
145        }
146        Map<String, Entry<Iterator<?>, List<KeyAssigner>>> assignerMap = new HashMap<>();
147        for (int i = 0; i < keyProperties.length; i++) {
148            Entry<String, KeyAssigner> entry = getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i],
149                keyProperties, true);
150            Entry<Iterator<?>, List<KeyAssigner>> iteratorPair = MapUtil.computeIfAbsent(assignerMap, entry.getKey(),
151                k -> MapUtil.entry(collectionize(paramMap.get(k)).iterator(), new ArrayList<>()));
152            iteratorPair.getValue().add(entry.getValue());
153        }
154        long counter = 0;
155        while (rs.next()) {
156            for (Entry<Iterator<?>, List<KeyAssigner>> pair : assignerMap.values()) {
157                if (!pair.getKey().hasNext()) {
158                    throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
159                }
160                Object param = pair.getKey().next();
161                pair.getValue().forEach(x -> x.assign(rs, param));
162            }
163            counter++;
164        }
165    }
166
167    private Entry<String, KeyAssigner> getAssignerForParamMap(Configuration config, ResultSetMetaData rsmd,
168                                                              int columnPosition, Map<String, ?> paramMap, String keyProperty, String[] keyProperties, boolean omitParamName) {
169        Set<String> keySet = paramMap.keySet();
170        // A caveat : if the only parameter has {@code @Param("param2")} on it,
171        // it must be referenced with param name e.g. 'param2.x'.
172        boolean singleParam = !keySet.contains(SECOND_GENERIC_PARAM_NAME);
173        int firstDot = keyProperty.indexOf('.');
174        if (firstDot == -1) {
175            if (singleParam) {
176                return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
177            }
178            throw new ExecutorException("Could not determine which parameter to assign generated keys to. "
179                + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
180                + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
181                + keySet);
182        }
183        String paramName = keyProperty.substring(0, firstDot);
184        if (keySet.contains(paramName)) {
185            String argParamName = omitParamName ? null : paramName;
186            String argKeyProperty = keyProperty.substring(firstDot + 1);
187            return MapUtil.entry(paramName, new KeyAssigner(config, rsmd, columnPosition, argParamName, argKeyProperty));
188        } else if (singleParam) {
189            return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
190        } else {
191            throw new ExecutorException("Could not find parameter '" + paramName + "'. "
192                + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
193                + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
194                + keySet);
195        }
196    }
197
198    private Entry<String, KeyAssigner> getAssignerForSingleParam(Configuration config, ResultSetMetaData rsmd,
199                                                                 int columnPosition, Map<String, ?> paramMap, String keyProperty, boolean omitParamName) {
200        // Assume 'keyProperty' to be a property of the single param.
201        String singleParamName = nameOfSingleParam(paramMap);
202        String argParamName = omitParamName ? null : singleParamName;
203        return MapUtil.entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty));
204    }
205
206    private static String nameOfSingleParam(Map<String, ?> paramMap) {
207        // There is virtually one parameter, so any key works.
208        return paramMap.keySet().iterator().next();
209    }
210
211    private static Collection<?> collectionize(Object param) {
212        if (param instanceof Collection) {
213            return (Collection<?>) param;
214        } else if (param instanceof Object[]) {
215            return Arrays.asList((Object[]) param);
216        } else {
217            return Arrays.asList(param);
218        }
219    }
220
221    private class KeyAssigner {
222
223        private final Configuration configuration;
224        private final ResultSetMetaData rsmd;
225        private final TypeHandlerRegistry typeHandlerRegistry;
226        private final int columnPosition;
227        private final String paramName;
228        private final String propertyName;
229        private TypeHandler<?> typeHandler;
230
231        protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName,
232                              String propertyName) {
233            super();
234            this.configuration = configuration;
235            this.rsmd = rsmd;
236            this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
237            this.columnPosition = columnPosition;
238            this.paramName = paramName;
239            this.propertyName = propertyName;
240        }
241
242        protected void assign(ResultSet rs, Object param) {
243            if (paramName != null) {
244                // If paramName is set, param is ParamMap
245                param = ((ParamMap<?>) param).get(paramName);
246            }
247            MetaObject metaParam = configuration.newMetaObject(param);
248            try {
249                if (typeHandler == null) {
250                    if (metaParam.hasSetter(propertyName)) {
251                        Class<?> propertyType = metaParam.getSetterType(propertyName);
252                        typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
253                            JdbcType.forCode(rsmd.getColumnType(columnPosition)));
254                    } else {
255                        throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
256                            + metaParam.getOriginalObject().getClass().getName() + "'.");
257                    }
258                }
259                if (typeHandler == null) {
260                    // Error?
261                } else {
262                    Object value = typeHandler.getResult(rs, columnPosition);
263                    metaParam.setValue(propertyName, value);
264                }
265            } catch (SQLException e) {
266                throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
267                    e);
268            }
269        }
270
271    }
272
273}