MyBatis-Plus 实现多租户管理的实践

mybatis,plus · 浏览次数 : 0

小编点评

**Infrastructure Service** * 实现了 `TenantService` 接口,负责与数据库进行交互。 * 使用 `DynamicRoutingDataSource` 管理多个数据源,根据租户ID切换数据源。 * 提供 `changeDsByTenantId` 方法,根据租户ID切换数据源。 * 提供 `queryTenantIdToDataSource` 方法,根据租户ID获取数据库连接信息并生成数据源。 **TenantServiceImpl** * 实现 `TenantService` 接口的方法,负责处理租户数据源切换和数据源清理。 * 使用 `TenantMapper` 转换 `DbInfo` 对象到 `DataSourceProperty`。 * 使用 `DynamicDataSourceContextHolder` 来管理数据源切换。 * 提供 `changeDsByTenantId` 和 `removeDs` 方法,根据租户ID切换和移除数据源。 * 提供 `queryTenantIdToDataSource` 方法,根据租户ID获取数据源。 **TenantMapper** * 负责将 `DbInfo` 对象转换为 `DataSourceProperty`。 **Handler Interceptor** * 实现 `HandlerInterceptor`接口,用于处理请求和响应之间的切换。 * 在 `preHandle` 方法中获取租户ID并设置 `TenantContext`。 * 在 `afterCompletion` 方法中清理当前线程数据源。 **WebConfigurer** * 为 MVC 配置设置数据源,并注册 `TenantDsInterceptor`。 **测试** * 在 `UserInfoController` 中使用 `TenantService` 获取数据源,并测试其切换能力。

正文

本文主要讲解使用Mybatis-Plus结合dynamic-datasource来实现多租户管理

在现代企业应用中,多租户(Multi-Tenant)架构已经成为一个非常重要的设计模式。多租户架构允许多个租户共享同一应用程序实例,但每个租户的数据彼此隔离。实现这一点可以大大提高资源利用率并降低运营成本。在本文中,我们将探讨如何使用 MyBatis-Plus 结合 Dynamic-Datasource 来实现多租户管理。

MyBatis-Plus 是 MyBatis 的增强工具,提供了很多开箱即用的功能,如 CRUD 操作、分页插件、逻辑删除等,使开发人员能够更加专注于业务逻辑,而无需过多关注底层的数据库操作细节。Dynamic-Datasource 是一个功能强大的动态数据源切换框架,能够方便地在多个数据源之间进行切换,非常适合实现多租户数据库管理。

本文将通过一个具体的例子,详细讲解如何配置和使用 MyBatis-Plus 以及 Dynamic-Datasource 来实现多租户管理。我们将首先创建租户信息表,并为每个租户分别创建用户信息表。然后,我们将配置 MyBatis-Plus 和 Dynamic-Datasource 实现动态数据源切换和多租户数据隔离。最后,我们会展示如何通过代码动态地切换数据源,以确保每个租户的数据操作都在各自的数据库中进行。

通过本文的学习,您将掌握:

  • 如何配置 MyBatis-Plus 和 Dynamic-Datasource 实现动态数据源切换
  • 如何在代码中实现多租户数据隔离

让我们开始吧!

环境

本文演示开发工具环境如下

IntelliJ IDEA 2023.3.6
Maven 3.8.6
JDK 17

依赖包如下


 <properties>
	    <druid.version>1.1.22</druid.version>
        <fastjson.version>2.0.39</fastjson.version>
        <dynamic.ds.version>3.5.1</dynamic.ds.version>
        <mybatis-plus.generator.version>3.5.1</mybatis-plus.generator.version>
 </properties>


 <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
 </dependency>
<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
</dependency>

<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>${dynamic.ds.version}</version>
</dependency>

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
</dependency>

<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus.generator.version}</version>
</dependency>

初始sql语句如下

CREATE TABLE `tenant` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
     `tenant_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '租户名称',
     `tenant_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '租户详情',
     `db_info` varchar(2047) COLLATE utf8mb4_general_ci DEFAULT NULL,
     `redis_info` varchar(2047) COLLATE utf8mb4_general_ci DEFAULT NULL,
     `version` int NOT NULL DEFAULT '0' COMMENT '版本号',
     `created_time` datetime NOT NULL COMMENT '创建时间',
     `created_by` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
     `modified_time` datetime NOT NULL COMMENT '修改时间',
     `modified_by` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
     `is_deleted`TINYINT(4) not null DEFAULT 0 COMMENT '是否删除',
     PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='租户信息';



INSERT INTO `tenant` ( `tenant_name`, `tenant_desc`, `db_info`, `redis_info`, `version`, `created_time`, `created_by`, `modified_time`, `modified_by` )
VALUES
    ( '测试租户1', '租户说明信息', '{\"dbUrl\": \"jdbc:mysql://127.0.0.1:3306/tenant-one?rewriteBatchedStatements=true\",\"dbUsername\": \"root\",\"dbPassword\": \"0c0bb39488e6dbfb\"}', '{\"host\": \"localhost\",\"port\": 6379,\"pwd\": \"123456\",\"db\": 1}', 0, NOW(), '1', NOW(), '1' );


INSERT INTO `tenant` (  `tenant_name`, `tenant_desc`, `db_info`, `redis_info`, `version`, `created_time`, `created_by`, `modified_time`, `modified_by` )
VALUES
    (  '测试租户2', '租户说明信息', '{\"dbUrl\": \"jdbc:mysql://127.0.0.1:3306/tenant-two?rewriteBatchedStatements=true\",\"dbUsername\": \"root\",\"dbPassword\": \"0c0bb39488e6dbfb\"}', '{\"host\": \"localhost\",\"port\": 6379,\"pwd\": \"123456\",\"db\": 1}', 0, NOW(), '1', NOW(), '1' );



use `tenant-one`;
CREATE TABLE IF NOT EXISTS user_info (
    id BIGINT NOT NULL PRIMARY KEY COMMENT '主键Id',
    user_no VARCHAR(255) NOT NULL DEFAULT '' COMMENT '编号',
    user_name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '姓名',
    description VARCHAR(512) DEFAULT '' COMMENT '备注',
    created_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
    created_by BIGINT NOT NULL DEFAULT 0 COMMENT '记录创建者Id,默认为0',
    modified_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录修改时间',
    modified_by BIGINT DEFAULT NULL COMMENT '记录修改者Id,可以为空',
    is_deleted TINYINT(4) NOT NULL DEFAULT 0 COMMENT '是否删除,默认为0,1表示删除'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息';



use `tenant-two`;
CREATE TABLE IF NOT EXISTS user_info (
    id BIGINT NOT NULL PRIMARY KEY COMMENT '主键Id',
    user_no VARCHAR(255) NOT NULL DEFAULT '' COMMENT '编号',
    user_name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '姓名',
    description VARCHAR(512) DEFAULT '' COMMENT '备注',
    created_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
    created_by BIGINT NOT NULL DEFAULT 0 COMMENT '记录创建者Id,默认为0',
    modified_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录修改时间',
    modified_by BIGINT DEFAULT NULL COMMENT '记录修改者Id,可以为空',
    is_deleted TINYINT(4) NOT NULL DEFAULT 0 COMMENT '是否删除,默认为0,1表示删除'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息';


use `tenant-one`;
INSERT INTO `user_info` (`id`, `user_no`, `user_name`, `description`, `created_time`, `created_by`, `modified_time`, `modified_by`, `is_deleted`) VALUES (1, 'test_no', '租户1测试用户', '租户1测试用户', '2024-05-15 03:45:06', 0, '2024-05-15 03:45:06', NULL, 0);

use `tenant-two`;
INSERT INTO `user_info` (`id`, `user_no`, `user_name`, `description`, `created_time`, `created_by`, `modified_time`, `modified_by`, `is_deleted`) VALUES (1, 'test_no', '租户2测试用户', '租户2测试用户', '2024-05-15 03:45:06', 0, '2024-05-15 03:45:06', NULL, 0);

配置文件如下

server:
  port: 8080
  servlet:
    context-path: /
  # undertow 配置
  undertow:
    # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
    max-http-post-size: -1
    # 每块buffer的空间大小,越小的空间被利用越充分
    buffer-size: 512
    # 是否分配的直接内存
    direct-buffers: true
    threads:
      # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
      io: 8
      # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
      worker: 256

base:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    db: 0
  db:
    url: jdbc:mysql://127.0.0.1:3306/tenant?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    pwd: 0c0bb39488e6dbfb


spring:
  datasource:
    dynamic:
      primary: 0
      strict: true
      hikari:
        connection-timeout: 30000
        max-pool-size: 10
        min-idle: 5
        idle-timeout: 180000
        max-lifetime: 1800000
        connection-test-query: SELECT 1
      datasource:
        0:
          url: ${base.db.url}
          username: ${base.db.username}
          password: ${base.db.pwd}
          driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true
    use-generated-keys: true
    default-executor-type: simple
    log-impl: org.apache.ibatis.logging.log4j2.Log4j2Impl
  mapperLocations: classpath*:mapper/*Mapper.xml
  typeAliasesPackage: com.simple.mybaitsdynamicdatasource.infrastructure.db.entity
  type-aliases-package: ${application.base-package}.entity
  global-config:
    db-config:
      logic-delete-field: is_deleted
      logic-not-delete-value: 0
      logic-delete-value: 1

logging:
  level:
    org.springframework: warn

代码如下

首先我的代码框架具体如下

其中实现动态切换数据源的操作主要在我们的TenantServiceImpl中,具体代码如下,其中主要是我们会通过当前获取到的TenantId来调用changeDsByTenantId方法进行修改动态数据源

package com.simple.mybaitsdynamicdatasource.infrastructure.service.impl;


import com.alibaba.fastjson2.JSON;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.druid.DruidConfig;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.simple.mybaitsdynamicdatasource.infrastructure.config.TenantContext;
import com.simple.mybaitsdynamicdatasource.infrastructure.db.entity.TenantEntity;
import com.simple.mybaitsdynamicdatasource.infrastructure.db.mapper.TenantMapper;
import com.simple.mybaitsdynamicdatasource.infrastructure.db.model.DbInfo;
import com.simple.mybaitsdynamicdatasource.infrastructure.service.TenantService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;

@Slf4j
@Service
@AllArgsConstructor
public class TenantServiceImpl extends ServiceImpl<TenantMapper, TenantEntity> implements TenantService {

    private TenantMapper tenantMapper;

    private DynamicRoutingDataSource dataSource;

    private DefaultDataSourceCreator dataSourceCreator;


    /**
     * 根据租户ID切换数据源
     *
     * @param tenantId 租户ID
     */
    @Override
    public void changeDsByTenantId(String tenantId) {
        //当前租户ID对应的数据源已存在,则直接切换
        if (existInMemory(tenantId)) {
            //切换数据源
            changeTenantDs(tenantId);
            return;
        }
        DataSource dataSource = queryTenantIdToDataSource(tenantId);
        if (!ObjectUtils.isEmpty(dataSource)) {
            //动态添加数据源
            this.dataSource.addDataSource(tenantId, dataSource);
            //切换数据源
            this.changeTenantDs(tenantId);
            return;
        }
        // todo 抛出异常信息
        throw new RuntimeException("数据源不存在");
    }

    /**
     * 判断是否存在内存中
     * @param dsName
     * @return
     */
    @Override
    public Boolean existInMemory(String dsName) {
        return StringUtils.hasText(dsName) && dataSource.getDataSources().containsKey(dsName);
    }

    /**
     * 清理当前调用上下文中的数据源缓存
     */
    @Override
    public void clearDsContext() {
        //清空当前线程数据源
        DynamicDataSourceContextHolder.clear();
        TenantContext.remove();
    }

    /**
     * 移除对应的数据源信息
     *
     * @param dsName 数据源名称
     */
    @Override
    public void removeDs(String dsName) {
        dataSource.removeDataSource(dsName);
    }


    /**
     * 切换租户对应的数据源
     *
     * @param tenantId 租户ID即对应数据源名称
     */
    private void changeTenantDs(String tenantId) {
        log.debug("切换数据源:{}", tenantId);
        //设置租户上下文
        TenantContext.setTenant(tenantId);
        //根据tenantId切换数据源
        DynamicDataSourceContextHolder.push(tenantId);
    }

    /**
     * 根据租户ID查询数据源连接信息,并生成数据源
     *
     * @param tenantId
     * @return
     */
    private DataSource queryTenantIdToDataSource(String tenantId) {
        TenantEntity tenant = tenantMapper.selectById(tenantId);
        log.debug("find db tenant info by tenantId:{}", tenantId);
        //租户为空则直接返回空
        if (!StringUtils.hasText(tenantId) || ObjectUtils.isEmpty(tenant)) {
            // todo 返回业务异常信息
            return null;
        }
        DbInfo dbInfo = JSON.parseObject(tenant.getDbInfo(), DbInfo.class);
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        dataSourceProperty.setUrl(dbInfo.getDbUrl());
        dataSourceProperty.setUsername(dbInfo.getDbUsername());
        dataSourceProperty.setPassword(dbInfo.getDbPassword());
        dataSourceProperty.setDriverClassName("com.mysql.cj.jdbc.Driver");

        dataSourceProperty.setDruid(new DruidConfig());
        return this.dataSourceCreator.createDataSource(dataSourceProperty);
    }
}

然后我们会通过实现HandlerInterceptor创建我们自己的TenantDsInterceptor来处理每个请求来的时候TenantId信息

package com.simple.mybaitsdynamicdatasource.infrastructure.config.handler;

import com.simple.mybaitsdynamicdatasource.infrastructure.config.TenantContext;
import com.simple.mybaitsdynamicdatasource.infrastructure.service.TenantService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@Component
@AllArgsConstructor
public class TenantDsInterceptor implements HandlerInterceptor {

    private TenantService tenantDsService;

    /**
     * 在请求处理之前进行调用(Controller方法调用之前)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //todo 从请求中获取租户ID
        String tenantId = "1";
        TenantContext.setTenant(tenantId);
        //根据tenantId切换数据源
        tenantDsService.changeDsByTenantId(tenantId);
        return true;
    }

    /**
     * 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        //清空当前线程数据源
        tenantDsService.clearDsContext();
    }
}

然后将我们的TenantDsInterceptor进行注册,

package com.simple.mybaitsdynamicdatasource.infrastructure.config;

import com.simple.mybaitsdynamicdatasource.infrastructure.config.handler.TenantDsInterceptor;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@AllArgsConstructor
public class WebConfigurer implements WebMvcConfigurer {

    private TenantDsInterceptor tenantDsInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tenantDsInterceptor).addPathPatterns("/**");
    }
}

最后我们通过如下方法来进行测试


package com.simple.mybaitsdynamicdatasource.web.controller;

import com.simple.mybaitsdynamicdatasource.infrastructure.db.entity.UserInfoEntity;
import com.simple.mybaitsdynamicdatasource.infrastructure.service.TenantService;
import com.simple.mybaitsdynamicdatasource.infrastructure.service.UserInfoService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;


@RestController
@RequestMapping("/user-info")
@AllArgsConstructor
public class UserInfoController {
    private UserInfoService userInfoService;

    private TenantService tenantService;

    @GetMapping("/query/{tenantId}")
    public List<UserInfoEntity> query(@PathVariable String tenantId) {
        tenantService.changeDsByTenantId(tenantId);
        return userInfoService.list();
    }

    @GetMapping("/query")
    public List<UserInfoEntity> queryAll() {
        return userInfoService.list();
    }
}


最后

我们需要约定好获取TenantId的方式,通过再TenantDsInterceptor中来给上下文进行注入让其能够依据不同的TenantId进行切换数据库

如有哪里讲得不是很明白或是有错误,欢迎指正
本文所有的演示代码皆在github 地址如下:https://github.com/benxionghu/mybaits-dynamic-datasource
如您喜欢的话不妨点个赞收藏一下吧🙂

与MyBatis-Plus 实现多租户管理的实践相似的内容:

MyBatis-Plus 实现多租户管理的实践

本文主要讲解使用Mybatis-Plus结合dynamic-datasource来实现多租户管理 在现代企业应用中,多租户(Multi-Tenant)架构已经成为一个非常重要的设计模式。多租户架构允许多个租户共享同一应用程序实例,但每个租户的数据彼此隔离。实现这一点可以大大提高资源利用率并降低运营成

多租户基于Springboot+MybatisPlus实现使用一个数据库一个表 使用字段进行数据隔离

# 多租户实现方式 ```properties 多租户在数据存储上主要存在三种方案,分别是: 1. 独立数据库 即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。 优点:为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复

多数据源管理:掌握@DS注解的威力

大家在日常后端开发过程,不可避免的会接触到需要用到配置多个数据源的场景,在这里,小编介绍一种简单方便的,只需要简单的配置和一个@DS注解就能实现动态数据源的方式,这种动态数据源底层原理是基于Mybatis-plus来实现的。

SpringBoot2.7升级到3.0的实践分享

背景 最近把项目中的技术框架做一次升级,最重要的就是SpringBoot从2.7.x升级到3.0.x,当然还会有一些周边的框架也会连带着升级,比如Mybatis Plus,SpringCloud等,话不多说直接看看有哪些事情要做。 具体事项 主要分两类,第一类是单纯的提升版本,主要如下: 1.jdk

mybaits-plus实现自定义字典转换

需求:字典实现类似mybatis-plus中@EnumValue的功能,假设枚举类中应用使用code,数据库存储对应的value 思路:Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对

基于SpringBoot实现操作GaussDB(DWS)的项目实战

摘要:本文就使用springboot结合mybatis plus在项目中实现对GaussDB(DWS)的增删改查操作。 本文分享自华为云社区《基于SpringBoot实现操作GaussDB(DWS)的项目实战【玩转PB级数仓GaussDB(DWS)】》,作者:清雨小竹。 GaussDB(DWS) 数

我的第一个项目(十四) :完成数据保存功能(前端,增查改接口)

好家伙,天天拖,终于写完了 代码已开源(Gitee) PH-planewar: 个人开发的全栈小游戏 前端:vue2 + element-ui 后端: Springboot + mybatis-plus 数据库: mysql 目前实现功能: 1.注册登陆 2.游戏数据保存 3.游戏运行 (gitee

我的第一个项目(十五) :完成数据保存功能(后端,改update)

好家伙, 代码已开源(Gitee) PH-planewar: 个人开发的全栈小游戏 前端:vue2 + element-ui 后端: Springboot + mybatis-plus 数据库: mysql 目前实现功能: 1.注册登陆 2.游戏数据保存 3.游戏运行 (gitee.com) 后端这

基于EasyCode定制Mybatisplus全自动单表实现:新增/批量新增/修改/批量删除/分页查询/ID查询

基于EasyCode定制Mybatisplus全自动单表实现CRUD接口 分页查询 ID查询 新增 批量新增 修改 批量删除 注意使用了MybatisPlus的自动填充功能,和insertBatchSomeColumn扩展批量插入功能,分页插件 需要几个增加插件实现类 自动填充 package co

Mybatis Plus 3.X版本的insert填充自增id的IdType.ID_WORKER策略源码分析

总结/朱季谦 某天同事突然问我,你知道Mybatis Plus的insert方法,插入数据后自增id是如何自增的吗? 我愣了一下,脑海里只想到,当在POJO类的id设置一个自增策略后,例如@TableId(value = "id",type = IdType.ID_WORKER)的注解策略时,就能实