浅谈MyBatis Plus主键设置策略

根据一次插入失败报错来了解下MyBatis Plus主键设置策略
今天学习使用MyBatis Plus,发现使用代码生成器生成对应的实体类、Service和Mapper后,在保存数据时报错

com.baomidou.mybatisplus.exceptions.MybatisPlusException: java.lang.reflect.InvocationTargetException
    at com.baomidou.mybatisplus.MybatisSqlSessionTemplate$SqlSessionInterceptor.invoke(MybatisSqlSessionTemplate.java:405)
    at com.sun.proxy.$Proxy70.insert(Unknown Source)
    at com.baomidou.mybatisplus.MybatisSqlSessionTemplate.insert(MybatisSqlSessionTemplate.java:243)
    at com.baomidou.mybatisplus.activerecord.Model.insert(Model.java:56)
    at com.lemon.rabbit.common.base.city.CityInitial.printInfo(CityInitial.java:112)
    at com.lemon.rabbit.common.base.city.CityInitial.parseNextLevel(CityInitial.java:87)
    at com.lemon.rabbit.common.base.city.CityInitial.test(CityInitial.java:59)
    at com.lemon.rabbit.RabbitApplicationTests.contextLoads(RabbitApplicationTests.java:19)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.baomidou.mybatisplus.MybatisSqlSessionTemplate$SqlSessionInterceptor.invoke(MybatisSqlSessionTemplate.java:401)
    ... 37 more
Caused by: org.apache.ibatis.exceptions.PersistenceException:
### Error updating database.  Cause: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.lemon.rabbit.model.City' with value '1017367047558582273' Cause: java.lang.IllegalArgumentException: argument type mismatch
### Cause: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.lemon.rabbit.model.City' with value '1017367047558582273' Cause: java.lang.IllegalArgumentException: argument type mismatch
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:200)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:185)
    ... 42 more
Caused by: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.lemon.rabbit.model.City' with value '1017367047558582273' Cause: java.lang.IllegalArgumentException: argument type mismatch
    at org.apache.ibatis.reflection.wrapper.BeanWrapper.setBeanProperty(BeanWrapper.java:185)
    at org.apache.ibatis.reflection.wrapper.BeanWrapper.set(BeanWrapper.java:59)
    at org.apache.ibatis.reflection.MetaObject.setValue(MetaObject.java:140)
    at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.populateKeys(MybatisDefaultParameterHandler.java:217)
    at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.processBatch(MybatisDefaultParameterHandler.java:156)
    at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.<init>(MybatisDefaultParameterHandler.java:71)
    at com.baomidou.mybatisplus.MybatisXMLLanguageDriver.createParameterHandler(MybatisXMLLanguageDriver.java:37)
    at org.apache.ibatis.session.Configuration.newParameterHandler(Configuration.java:545)
    at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:69)
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.<init>(PreparedStatementHandler.java:40)
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.<init>(RoutingStatementHandler.java:46)
    at org.apache.ibatis.session.Configuration.newStatementHandler(Configuration.java:558)
    at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:48)
    at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)
    at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)
    ... 43 more
Caused by: java.lang.IllegalArgumentException: argument type mismatch
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.ibatis.reflection.invoker.MethodInvoker.invoke(MethodInvoker.java:41)
    at org.apache.ibatis.reflection.wrapper.BeanWrapper.setBeanProperty(BeanWrapper.java:180)
    ... 58 more

实体类City的主键是Integer类型的,在进行insert操作时,MyBatis Plus自动生成了一个Long类型的主键id,导致参数类型不匹配,出现上述错误

经过查看日志和调试发现,MyBatis最终调用BeanWrapper的setBeanProperty方法,通过反射执行最终的插入操作(增删改查应该都是通过此处的反射,不过暂时只调试了insert方法)

private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
    try {
        Invoker method = metaClass.getSetInvoker(prop.getName());
        Object[] params = {value};
        try {
            method.invoke(object, params);
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    } catch (Throwable t) {
        throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
    }
}

此处传入的object即为我们想要保存到数据库的实体信息(不带ID信息),value为主键信息,此时主键值已经是一个Long类型的值,我们接着向上看value是哪里传过来的

setBeanProperty是一个私有方法,在本类调用,查询看到set(PropertyTokenizer prop, Object value)方法调用了它

@Override
public void set(PropertyTokenizer prop, Object value) {
    if (prop.getIndex() != null) {
        Object collection = resolveCollection(prop, object);
        setCollectionValue(prop, collection, value);
    } else {
      setBeanProperty(prop, object, value);
    }
}

set方法中的value也是其他地方传入的

Caused by: org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.lemon.rabbit.model.City' with value '1017367047558582273' Cause: java.lang.IllegalArgumentException: argument type mismatch
    at org.apache.ibatis.reflection.wrapper.BeanWrapper.setBeanProperty(BeanWrapper.java:185)
    at org.apache.ibatis.reflection.wrapper.BeanWrapper.set(BeanWrapper.java:59)
    at org.apache.ibatis.reflection.MetaObject.setValue(MetaObject.java:140)
    at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.populateKeys(MybatisDefaultParameterHandler.java:217)
    at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.processBatch(MybatisDefaultParameterHandler.java:156)
    at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.<init>(MybatisDefaultParameterHandler.java:71)
    at com.baomidou.mybatisplus.MybatisXMLLanguageDriver.createParameterHandler(MybatisXMLLanguageDriver.java:37)
    at org.apache.ibatis.session.Configuration.newParameterHandler(Configuration.java:545)
    at org.apache.ibatis.executor.statement.BaseStatementHandler.<init>(BaseStatementHandler.java:69)
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.<init>(PreparedStatementHandler.java:40)
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.<init>(RoutingStatementHandler.java:46)
    at org.apache.ibatis.session.Configuration.newStatementHandler(Configuration.java:558)
    at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:48)
    at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)
    at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)
    ... 43 more

查看报错日志可以看到,BeanWrapper的上一层是MetaObject,我们找到MetaObject,看到在getValue方法中调用了BeanWrapper的set方法(BeanWrapper实现了ObjectWrapper)

public void setValue(String name, Object value) {
    PropertyTokenizer prop = new PropertyTokenizer(name);
    if (prop.hasNext()) {
        MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
        if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
            if (value == null && prop.getChildren() != null) {
                // don't instantiate child path if value is null
                return;
            } else {
                metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
            }
        }
        metaValue.setValue(prop.getChildren(), value);
    } else {
        objectWrapper.set(prop, value);
    }
}

MetaObject中的主键值也是上层调用传入的,继续根据错误日志向上看:MybatisDefaultParameterHandler

    /**
     * <p>
     * 自定义元对象填充控制器
     * </p>
     *
     * @param metaObjectHandler 元数据填充处理器
     * @param tableInfo         数据库表反射信息
     * @param ms                MappedStatement
     * @param parameterObject   插入数据库对象
     * @return Object
     */
    protected static Object populateKeys(MetaObjectHandler metaObjectHandler, TableInfo tableInfo,
                                         MappedStatement ms, Object parameterObject) {
        if (null == tableInfo || StringUtils.isEmpty(tableInfo.getKeyProperty()) || null == tableInfo.getIdType()) {
            /* 不处理 */
            return parameterObject;
        }
        /* 自定义元对象填充控制器 */
        MetaObject metaObject = ms.getConfiguration().newMetaObject(parameterObject);
        if (ms.getSqlCommandType() == SqlCommandType.INSERT) {
            if (tableInfo.getIdType().getKey() >= 2) {
                Object idValue = metaObject.getValue(tableInfo.getKeyProperty());
                /* 自定义 ID */
                if (StringUtils.checkValNull(idValue)) {
                    if (tableInfo.getIdType() == IdType.ID_WORKER) {
                        metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getId());
                    } else if (tableInfo.getIdType() == IdType.ID_WORKER_STR) {
                        metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getIdStr());
                    } else if (tableInfo.getIdType() == IdType.UUID) {
                        metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.get32UUID());
                    }
                }
            }
            // 插入填充
            if (metaObjectHandler.openInsertFill()) {
                metaObjectHandler.insertFill(metaObject);
            }
        } else if (ms.getSqlCommandType() == SqlCommandType.UPDATE && metaObjectHandler.openUpdateFill()) {
            // 更新填充
            metaObjectHandler.updateFill(metaObject);
        }
        return metaObject.getOriginalObject();
    }

在populateKeys方法中调用了metaObject.setValue()

                if (StringUtils.checkValNull(idValue)) {
                    if (tableInfo.getIdType() == IdType.ID_WORKER) {
                        metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getId());
                    } else if (tableInfo.getIdType() == IdType.ID_WORKER_STR) {
                        metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getIdStr());
                    } else if (tableInfo.getIdType() == IdType.UUID) {
                        metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.get32UUID());
                    }
                }

可以看到,此处根据IdType生成不同类型的主键id,IdType是一个枚举类,定义了生成ID的类型

  • AUTO 数据库ID自增
  • INPUT 用户输入ID
  • ID_WORKER 全局唯一ID,Long类型的主键
  • ID_WORKER_STR 字符串全局唯一ID
  • UUID 全局唯一ID,UUID类型的主键
  • NONE 该类型为未设置主键类型

当IdType的类型为ID_WORKER、ID_WORKER_STR或者UUID时,主键由MyBatis Plus的IdWorker类生成

ID_WORKER

调用IdWorker的getId()方法,生成一个与时间相关的主键id

    public static long getId() {
        return worker.nextId();
    }

IdWorker的getId()方法引用了Sequence的nextId()方法

    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {//闰秒
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    wait(offset << 1);
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } else {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
            }
        }

        if (lastTimestamp == timestamp) {
            // 相同毫秒内,序列号自增
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 同一毫秒的序列数已经达到最大
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒内,序列号置为 1 - 3 随机数
            sequence = ThreadLocalRandom.current().nextLong(1, 3);
        }

        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift)    // 时间戳部分
            | (datacenterId << datacenterIdShift)           // 数据中心部分
            | (workerId << workerIdShift)                   // 机器标识部分
            | sequence;                                     // 序列号部分
    }

ID_WORKER_STR

将worker.nextId()的返回值转化为字符串,和getId()方法相似

    public static String getIdStr() {
        return String.valueOf(worker.nextId());
    }

UUID

去除中划线的UUID字符串

    public static synchronized String get32UUID() {
        return UUID.randomUUID().toString().replace("-", "");
    }

此时,已经基本可以确认,问题出在IdType的配置上,那么这个IdType从哪里获取的呢?TableInfo中获取的!

TableInfo是数据库表反射信息实体类,此处由其他方法传入的,查看日志:

at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.populateKeys(MybatisDefaultParameterHandler.java:217)
at com.baomidou.mybatisplus.MybatisDefaultParameterHandler.processBatch(MybatisDefaultParameterHandler.java:156)

通过日志可以看出,populateKeys()方法是在processBatch()方法中调用的,找到该方法

    /**
     * <p>
     * 批量(填充主键 ID)
     * </p>
     *
     * @param ms
     * @param parameterObject 插入数据库对象
     * @return
     */
    protected static Object processBatch(MappedStatement ms, Object parameterObject) {
        //检查parameterObject
        if (null == parameterObject) {
            return null;
        }
        boolean isFill = false;
        // 全局配置是否配置填充器
        MetaObjectHandler metaObjectHandler = GlobalConfigUtils.getMetaObjectHandler(ms.getConfiguration());
        /* 只处理插入或更新操作 */
        if (ms.getSqlCommandType() == SqlCommandType.INSERT) {
            isFill = true;
        } else if (ms.getSqlCommandType() == SqlCommandType.UPDATE
            && metaObjectHandler.openUpdateFill()) {
            isFill = true;
        }
        if (isFill) {
            Collection<Object> parameters = getParameters(parameterObject);
            if (null != parameters) {
                List<Object> objList = new ArrayList<>();
                for (Object parameter : parameters) {
                    TableInfo tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
                    if (null != tableInfo) {
                        objList.add(populateKeys(metaObjectHandler, tableInfo, ms, parameter));
                    } else {
                        /*
                         * 非表映射类不处理
                         */
                        objList.add(parameter);
                    }
                }
                return objList;
            } else {
                TableInfo tableInfo = null;
                if (parameterObject instanceof Map) {
                    Map map = (Map) parameterObject;
                    if (map.containsKey("et")) {
                        Object et = map.get("et");
                        if (et != null) {
                            if (et instanceof Map) {
                                Map realEtMap = (Map) et;
                                if (realEtMap.containsKey("MP_OPTLOCK_ET_ORIGINAL")) {//refer to OptimisticLockerInterceptor.MP_OPTLOCK_ET_ORIGINAL
                                    tableInfo = TableInfoHelper.getTableInfo(realEtMap.get("MP_OPTLOCK_ET_ORIGINAL").getClass());
                                }
                            } else {
                                tableInfo = TableInfoHelper.getTableInfo(et.getClass());
                            }
                        }
                    }
                } else {
                    tableInfo = TableInfoHelper.getTableInfo(parameterObject.getClass());
                }
                return populateKeys(metaObjectHandler, tableInfo, ms, parameterObject);
            }
        }
        return parameterObject;
    }

下面对TableInfo生成的这段代码做个说明

                TableInfo tableInfo = null;
                if (parameterObject instanceof Map) {
                    Map map = (Map) parameterObject;
                    if (map.containsKey("et")) {
                        Object et = map.get("et");
                        if (et != null) {
                            if (et instanceof Map) {
                                Map realEtMap = (Map) et;
                                if (realEtMap.containsKey("MP_OPTLOCK_ET_ORIGINAL")) {//refer to OptimisticLockerInterceptor.MP_OPTLOCK_ET_ORIGINAL
                                    tableInfo = TableInfoHelper.getTableInfo(realEtMap.get("MP_OPTLOCK_ET_ORIGINAL").getClass());
                                }
                            } else {
                                tableInfo = TableInfoHelper.getTableInfo(et.getClass());
                            }
                        }
                    }
                } else {
                    tableInfo = TableInfoHelper.getTableInfo(parameterObject.getClass());
                }

parameterObject:要保存到数据库中的实体类信息

我在保存数据时,参数形式并不是Map类型的,所以直接跳转到else中

tableInfo = TableInfoHelper.getTableInfo(parameterObject.getClass());

根据保存的实体类的类型去获取数据库表反射信息

我们看下getTableInfo()方法的实现

    public static TableInfo getTableInfo(Class<?> clazz) {
        return tableInfoCache.get(ClassUtils.getUserClass(clazz).getName());
    }

从tableInfoCache中获取指定类型的数据库反射信息。tableInfoCache是一个线程安全的私有静态Map,主要用于存放类型和数据库表的映射关系。断点调试到此处,看下tableInfoCache的内容

可以看到,idType的值为ID_WORKER,即生成一个与时间相关的Long类型的id。在放入到tableInfoCache的时候就已经指定了idType的值。

查看TableInfoHelper的源码可以得知,initTableInfo()方法负责initTableInfo的初始化

    public synchronized static TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) {
        //检测是否已存在
        TableInfo tableInfo = tableInfoCache.get(clazz.getName());
        if (StringUtils.checkValNotNull(tableInfo)) {
            if (StringUtils.checkValNotNull(builderAssistant)) {
                tableInfo.setConfigMark(builderAssistant.getConfiguration());
            }
            return tableInfo;
        }
        tableInfo = new TableInfo();
        GlobalConfiguration globalConfig;
        if (null != builderAssistant) {
            tableInfo.setCurrentNamespace(builderAssistant.getCurrentNamespace());
            tableInfo.setConfigMark(builderAssistant.getConfiguration());
            //获取全局配置,其中包括了idType
            globalConfig = GlobalConfigUtils.getGlobalConfig(builderAssistant.getConfiguration());
        } else {
            // 兼容测试场景
            globalConfig = GlobalConfigUtils.DEFAULT;
        }
        /* 表名 */
        TableName table = clazz.getAnnotation(TableName.class);
        String tableName = clazz.getSimpleName();
        if (table != null && StringUtils.isNotEmpty(table.value())) {
            tableName = table.value();
        } else {
            // 开启字段下划线申明
            if (globalConfig.isDbColumnUnderline()) {
                tableName = StringUtils.camelToUnderline(tableName);
            }
            // 大写命名判断
            if (globalConfig.isCapitalMode()) {
                tableName = tableName.toUpperCase();
            } else {
                // 首字母小写
                tableName = StringUtils.firstToLowerCase(tableName);
            }
            // 存在表前缀
            if (null != globalConfig.getTablePrefix()) {
                tableName = globalConfig.getTablePrefix() + tableName;
            }
        }
        tableInfo.setTableName(tableName);

        // 开启了自定义 KEY 生成器
        if (null != globalConfig.getKeyGenerator()) {
            tableInfo.setKeySequence(clazz.getAnnotation(KeySequence.class));
        }

        /* 表结果集映射 */
        if (table != null && StringUtils.isNotEmpty(table.resultMap())) {
            tableInfo.setResultMap(table.resultMap());
        }
        List<TableFieldInfo> fieldList = new ArrayList<>();
        List<Field> list = getAllFields(clazz);
        // 标记是否读取到主键
        boolean isReadPK = false;
        boolean existTableId = existTableId(list);
        for (Field field : list) {
            /*
             * 主键ID 初始化
             */
            if (!isReadPK) {
                if (existTableId) {
                    isReadPK = initTableId(globalConfig, tableInfo, field, clazz);
                } else {
                    isReadPK = initFieldId(globalConfig, tableInfo, field, clazz);
                }
                if (isReadPK) {
                    continue;
                }

            }
            /*
             * 字段初始化
             */
            if (initTableField(globalConfig, tableInfo, fieldList, field, clazz)) {
                continue;
            }

            /*
             * 字段, 使用 camelToUnderline 转换驼峰写法为下划线分割法, 如果已指定 TableField , 便不会执行这里
             */
            fieldList.add(new TableFieldInfo(globalConfig, tableInfo, field));
        }

        /* 字段列表 */
        tableInfo.setFieldList(globalConfig, fieldList);
        /*
         * 未发现主键注解,提示警告信息
         */
        if (StringUtils.isEmpty(tableInfo.getKeyColumn())) {
            logger.warn(String.format("Warn: Could not find @TableId in Class: %s.", clazz.getName()));
        }
        /*
         * 注入
         */
        tableInfoCache.put(clazz.getName(), tableInfo);
        return tableInfo;
    }

在existTableId方法中判断主键注解@TableId是否存在

    /**
     * <p>
     * 判断主键注解是否存在
     * </p>
     *
     * @param list 字段列表
     * @return
     */
    public static boolean existTableId(List<Field> list) {
        boolean exist = false;
        for (Field field : list) {
            TableId tableId = field.getAnnotation(TableId.class);
            if (tableId != null) {
                exist = true;
                break;
            }
        }
        return exist;
    }

当不存在主键注解时,会调用initFieldId()方法对主键属性进行初始化

    private static boolean initFieldId(GlobalConfiguration globalConfig, TableInfo tableInfo, Field field, Class<?> clazz) {
        String column = field.getName();
        if (globalConfig.isCapitalMode()) {
            column = column.toUpperCase();
        }
        if (DEFAULT_ID_NAME.equalsIgnoreCase(column)) {
            if (StringUtils.isEmpty(tableInfo.getKeyColumn())) {
                tableInfo.setIdType(globalConfig.getIdType());
                tableInfo.setKeyColumn(column);
                tableInfo.setKeyProperty(field.getName());
                return true;
            } else {
                throwExceptionId(clazz);
            }
        }
        return false;
    }

至此, 我们基本可以判断是因为在实体类City中id属性没有加@TableId注解,我们看下TableId的源码

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TableId {

    /**
     * <p>
     * 字段值(驼峰命名方式,该值可无)
     * </p>
     */
    String value() default "";

    /**
     * <p>
     * 主键ID
     * </p>
     * {@link IdType}
     */
    IdType type() default IdType.NONE;

}

TableId的类型通过type来指定,默认是IdType.NONE(该类型为未设置主键类型),City表是通过数据库的自增序列实现的,所以设置为AUTO

@TableId(type = IdType.AUTO) private Integer id;

然后测试,程序正常运行,保存数据成功。

扩展

对于tableInfo默认的idType值配置,可以看出用的是全局配置的idType,全局配置的值是在initTableInfo()方法中获取的,有兴趣的话可以去看看全局配置的实现,此处暂不深入了

        GlobalConfiguration globalConfig;
        if (null != builderAssistant) {
            tableInfo.setCurrentNamespace(builderAssistant.getCurrentNamespace());
            tableInfo.setConfigMark(builderAssistant.getConfiguration());
            globalConfig = GlobalConfigUtils.getGlobalConfig(builderAssistant.getConfiguration());
        } else {
            // 兼容测试场景
            globalConfig = GlobalConfigUtils.DEFAULT;
        }

到此这篇关于浅谈MyBatis Plus主键设置策略的文章就介绍到这了,更多相关MyBatis Plus主键设置内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解mybatis plus使用insert没有返回主键的处理

    项目使用springboot搭建.最初的时候是使用mybatis,后来升级到mybatis plus.按照mp的官网介绍,使用mp的insert方法,对于自增的数据库表,mp会把主键写入回实例的对应属性.但实际操作起来,却没有主键. entity 类设置如下: @TableName(value = "USERINFO") public class UserInfo { /** * 指定自增策略 */ @TableId(value = "user_id",type =

  • MybatisPlus中插入数据后获取该对象主键值的实现

    实体对象 主键IdType要设置为AUTO 表示数据库ID自增 @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class Employee implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) priv

  • mybatis-plus的selectById(或者selectOne)在根据主键ID查询实体对象的时候偶尔会出现null的问题记录

    mybatis-plus的selectById/selectOne查询结果偶尔出错(为null)的问题记录 错误截图: 亲测重复执行此段代码10次中大概会有连续的2次出现结果为null的情况. 由于后续还需引用到这个查询结果的某些字段信息,会导致程序出现空指针异常,故投机取巧做了如下处理(加了一个while循环让其一直执行selectById(或者selectOne)直到查询结果不为空): 但这终归不是从根本上解决了问题.我也不清白他出现这个问题的根本原因是什么. 到此这篇关于mybatis-p

  • mybatis-plus实体类主键策略有3种(小结)

    mybatis plus 实体类主键策略有3种( 注解 > 全局 > 默认 ) 当IdType的类型为ID_WORKER.ID_WORKER_STR或者UUID时,主键由MyBatis Plus的IdWorker类生成,idWorker中调用了分布式唯一 ID 生成器 - Sequence 1.注解方式 @TableId(type = IdType.AUTO)在实体类增加注解即可 @TableName("t_article") public class TArticle e

  • mybatisplus 复合主键(多主键) CRUD示例详解

    目录 mybatisplus复合主键CRUD 需求描述 mybatisplus-plus mybatisplus 复合主键CRUD 需求描述 最近接到个挺有意思的需求,做用户观看学习视频时长的一个数据埋点 储存用户观看视频时长.记录的接口的调用肯定会特别频繁,因为每间隔指定时间每个用户都会调用,如果在这个接口里直接操作数据库将会给我们的数据库带来一定的压力,在我的代码中是不允许的,而我是这样完成这个需求的: 首先将用户观看视频的时长.记录存储到阿里云的日志库里,随后以定时器从阿里云的日志库中拉取

  • 浅谈MyBatis-Plus学习之Oracle的主键Sequence设置的方法

    一.Oracle的主键Sequence设置简介 在Oracle数据库中不支持主键自增策略,它是通过Sequence序列来进行完成的,因此需要在MP中进行相关配置 二.相关配置如下 2.1.pom.xml 添加相关依赖 注意:由于oracle的授权问题,没办法从maven仓库中下载,因此可以手动从oracle官网中下载,并本地打包到仓库中 <!-- Oracle驱动: 因为Oracle授权的问题,不能从Maven的仓库中下载到Oracle驱动 --> <dependency> <

  • mybatis-plus主键id生成、字段自动填充的实现代码

    一.主键id的生成 数据库表里通常都会有一个主键id,来作为这条数据的唯一标识. 常见的方式 1.数据库自动增长 这种很常见了,可以做到全库唯一.因为id是天然排序的,对于涉及到排序的操作会很方便. 2.UUID 上面的自动增长,虽然简单,但是对于分表这样的操作来说就比较麻烦.因为你在第二张插入数据的时候,需要拿到上一张表最后一个数据的id. UUID则不同,每次都一个随机唯一的值,不过因为是随机,所以也就没有排序了. 3.redis redis也可以用来生成id,利用redis的原子操作.好处

  • mybatis-plus id主键生成的坑

    简要说明 由于mybatis-plus会自动插入一个id到实体对象, 不管你封装与否, 所以有时候导致一些意外的情况发生 默认是生成一个长数字字符串(编码不同可能结尾带有字母) 错误 ested exception is org.apache.ibatis.reflection.ReflectionException: Could not set property 'id' of 'class com.xxx' with value '1110423703487479810' Cause: ja

  • mybatis-plus主键生成策略

    MP 支持多种主键策略 默认是推特的"" 雪花算法"" ,也可以设置其他策略下面我演示主键策略使用 MP的主键定义在一个一个枚举类中 源码如下 public enum IdType { AUTO(0),//数据库自增 依赖数据库 NONE(1),// 表示该类型未甚至主键类型 (如果没有主键策略)默认根据雪花算法生成 INPUT(2),//用户输入ID(该类型可以通过自己注册填充插件进行填充) //下面这三种类型,只有当插入对象id为空时 才会自动填充. ID_WO

  • 浅谈MyBatis Plus主键设置策略

    根据一次插入失败报错来了解下MyBatis Plus主键设置策略今天学习使用MyBatis Plus,发现使用代码生成器生成对应的实体类.Service和Mapper后,在保存数据时报错 com.baomidou.mybatisplus.exceptions.MybatisPlusException: java.lang.reflect.InvocationTargetException at com.baomidou.mybatisplus.MybatisSqlSessionTemplate$

  • 浅谈Redis对于过期键的三种清除策略

    目录 Pre Redis Key的超时设置处理 被动删除 主动删除 当前已用内存超过maxmemory限定时,触发主动清理策略 对于过期键一般有三种删除策略 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作: 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键:如果没有过期,那就返回该键: 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键.至于删除多少过期

  • 浅谈Redis中的内存淘汰策略和过期键删除策略

    目录 8种淘汰策略 过期键的删除策略 总结 redis是我们现在最常用的一个工具,帮助我们建设系统的高可用,高性能. 而且我们都知道redis是一个完全基于内存的工具,这也是redis速度快的一个原因,当我们往redis中不断缓存数据的时候,其内存总有满的时候(而且内存是很贵的东西,尽量省着点用),所以尽可能把有用的数据,或者使用频繁的数据缓存在redis中,物尽其用. 那么如果正在使用的redis内存用完了,我们应该怎么取舍redis中已存在的数据和即将要存入的数据呢,我们要怎么处理呢? re

  • 浅谈Mybatis版本升级踩坑及背后原理分析

    1.背景 某一天的晚上,系统服务正在进行常规需求的上线,因为发布时,提示统一的pom版本需要升级,于是从 1.3.9.6 升级至 1.4.2.1. 当服务开始上线后,开始陆续出现了一些更新系统交互日志方面的报警,属于系统辅助流程,报警下图所示, 具体系统数据已脱敏,内容是Mybatis相关的报警,在进行类型转换的时候,产生了强转错误. 更新开票请求返回日志, id:{#######}, response:{{"code":XXX,"data":{"call

  • 浅谈mybatis 乐观锁实现,解决并发问题

    情景展示: 银行两操作员同时操作同一账户就是典型的例子. 比如A.B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户扣除50元,A先提交,B后提交.最后实际账户余额为1000-50=950元,但本该为1000+100-50=1050.这就是典型的并发问题. 乐观锁机制在一定程度上解决了这个问题.乐观锁,大多是基于数据版本(Version)记录机制实现.何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 "

  • 浅谈Mybatis #和$区别以及原理

    总结: 1.#可以防止Sql 注入,它会将所有传入的参数作为一个字符串来处理. 2.$ 则将传入的参数拼接到Sql上去执行,一般用于表名和字段名参数,$ 所对应的参数应该由服务器端提供,前端可以用参数进行选择,避免 Sql 注入的风险 为什么? 为什么# 和 $ 的作用不同,Mybatis 对他们做了哪些惨无人道的处理,我们看一下下面的例子,并追踪一下源码总结. 示例代码: 创建一个 tb_class 表(具体字段不做解释). 创建一个 ClassDao.java 并使用注解的方式 ,table

  • 浅谈MyBatis 如何执行一条 SQL语句

    前言 Mybatis 是 Java 开发中比较常用的 ORM 框架.在日常工作中,我们都是直接通过 Spring Boot 自动配置,并直接使用,但是却不知道 Mybatis 是如何执行一条 SQL 语句的,而这篇文章就是来揭开 Mybatis 的神秘面纱. 基础组件 我们要理解 Mybatis 的执行过程,就必须先了解 Mybatis 中都有哪一些重要的类,这些类的职责都是什么? SqlSession 我们都很熟悉,它对外提供用户和数据库之间交互需要使用的方法,隐藏了底层的细节.它默认是实现类

  • 浅谈redis的过期时间设置和过期删除机制

    目录 一:设置过期时间 二:保存过期时间 三:移除过期时间 四:计算并返回剩余生存时间 五:过期键的删除策略 六:redis使用的策略 一:设置过期时间 redis有四种命令可以用于设置键的生存时间和过期时间: EXPIRE <KEY> <TTL> : 将键的生存时间设为 ttl 秒 PEXPIRE <KEY> <TTL> :将键的生存时间设为 ttl 毫秒 EXPIREAT <KEY> <timestamp> :将键的过期时间设为

  • 浅谈Redis 中的过期删除策略和内存淘汰机制

    目录 前言 Redis 中 key 的过期删除策略 1.定时删除 2.惰性删除 3.定期删除 Redis 中过期删除策略 从库是否会脏读主库创建的过期键 内存淘汰机制 内存淘汰触发的最大内存 有哪些内存淘汰策略 内存淘汰算法 LRU LFU 为什么数据删除后内存占用还是很高 内存碎片如何产生 碎片率的意义 如何清理内存碎片 总结 参考 前言 Redis 中的 key 设置一个过期时间,在过期时间到的时候,Redis 是如何清除这个 key 的呢? 这来分析下 Redis 中的过期删除策略和内存淘

  • 浅谈Redis 中的过期删除策略和内存淘汰机制

    目录 前言 Redis 中 key 的过期删除策略 1.定时删除 2.惰性删除 3.定期删除 Redis 中过期删除策略 从库是否会脏读主库创建的过期键 内存淘汰机制 内存淘汰触发的最大内存 有哪些内存淘汰策略 内存淘汰算法 LRU LFU 为什么数据删除后内存占用还是很高 内存碎片如何产生 碎片率的意义 如何清理内存碎片 总结 参考 前言 Redis 中的 key 设置一个过期时间,在过期时间到的时候,Redis 是如何清除这个 key 的呢? 这来分析下 Redis 中的过期删除策略和内存淘

随机推荐