前言
背景
在 SpringBoot 项目开发前,关于初始代码的生成,是值得考虑的一件事。当我们根据业务需求完成表设计后,接下来就需要根据表生成相关代码,在 SpringBoot 项目中需要以下几部分内容:
- entity, 实体层,用于存放我们的实体类,与数据库中的属性值基本保持一致,实现set和get的方法 ;
- mapper,对数据库进行数据持久化操作,它的方法语句是直接针对数据库操作的,主要实现一些增删改查操作,在 mybatis 中方法主要与与 xxx.xml 内相互一一映射;
- service,业务 service 层,给 controller 层的类提供接口进行调用。一般就是自己写的方法封装起来,就是声明一下,具体实现在 serviceImpl 中;
- controller,控制层,负责具体模块的业务流程控制,需要调用 service 逻辑设计层的接口来控制业务流程。因为 service 中的方法是我们使用到的,controller 通过接收前端 H5 或者 App 传过来的参数进行业务操作,再将处理结果返回到前端。
除了上述项目架构中最基本的文件,为了更好的管理项目,我们还增加以下几个层级:
- dto文件,用来分担实体类的功效,可以将查询条件单独封装一个类,以及前后端交互的实体类(有时候我们可能会传入 entity 实体类中不存在的字段);
- vo文件,后台返回给前台的数据结构,同样可以自定义字段;
- struct文件,用来处理 dto 、entity、vo 文件之间的转换。
项目中使用的 ORM 框架多为 Mybatis 和 Mybatis Plus,虽然各自的官方文档都有代码生成器配置,但是过于简单,无法满足实际需求,因此整理出一套通用的代码生成器,势在必行。
在开始本文之前,首先介绍一下要用到的知识点。
知识点
FreeMarker
FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像 PHP 那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
Mybatis
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
Mybatis Plus
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
本文主要讲述选择使用 Mybatis 和 Mybatis Plus 时,相关代码文件的生成过程。
JCommander
JCommander 是一个用于解析命令行参数的Java框架,支持解析所有基本的数据类型,也支持将命令行解析成用户自定义的类型,只需要写一个转变函数。
接下来就进行代码实战环节。
实战
首先新建一个 maven 项目,命名为 mybatis-generator。
基本环境配置
导入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.msdn.generator</groupId> <artifactId>mybatis-generator</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> <logback.version>1.2.3</logback.version> <slf4j.version>1.7.30</slf4j.version> <fastjson.version>1.2.73</fastjson.version> <hutool.version>5.5.8</hutool.version> <mysql.version>8.0.19</mysql.version> <swagger.version>1.9.1.RELEASE</swagger.version> <mybatis.version>2.1.4</mybatis.version> <mapper.version>4.1.5</mapper.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> <scope>compile</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.spring4all</groupId> <artifactId>swagger-spring-boot-starter</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>${mapper.version}</version> </dependency> <!--JCommander解析命令行参数--> <dependency> <groupId>com.beust</groupId> <artifactId>jcommander</artifactId> <version>1.78</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> <version>2.4.6</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 复制代码
配置文件
application.yml 文件内容如下:
server: port: 8525 #并无实际意义,实际项目中具体配置 spring: datasource: username: root password: xxxx url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&useSSL=true&serverTimezone=UTC&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver 复制代码
代码生成
实体类
为了接收相关配置参数,我们通过 JCommander 解析命令行参数,此处创建对应的实体类 GenerateParameter来接收这些参数。
@Getter @Setter @ApiModel("使用帮助") @Parameters(commandDescription = "使用帮助") public class GenerateParameter { @ApiModelProperty("mysql主机名") @Parameter(names = {"--host", "-h"}, description = "mysql主机名") private String host; @ApiModelProperty("mysql端口") @Parameter(names = {"--port", "-P"}, description = "mysql端口") private Integer port; @ApiModelProperty("mysql用户名") @Parameter(names = {"--username", "-u"}, description = "mysql用户名") private String username; @ApiModelProperty("mysql密码") @Parameter(names = {"--password", "-p"}, description = "mysql密码") private String password; @ApiModelProperty("mysql数据库名") @Parameter(names = {"--database", "-d"}, description = "mysql数据库名") private String database; @ApiModelProperty("mysql数据库表") @Parameter(names = {"--table", "-t"}, description = "mysql数据库表") private List<String> table; @ApiModelProperty("业务模块名") @Parameter(names = {"--module", "-m"}, description = "业务模块名") private String module; @ApiModelProperty("业务分组,目前是base和business") @Parameter(names = {"--group", "-g"}, description = "业务分组,目前是base和business") private String group; @ApiModelProperty("是否按表名分隔目录") @Parameter(names = {"--flat"}, description = "是否按表名分隔目录") private boolean flat; @ApiModelProperty("orm框架选择") @Parameter(names = {"--type"}, description = "orm框架选择") private String type; @ApiModelProperty("查看帮助") @Parameter(names = "--help", help = true, description = "查看帮助") private boolean help; @ApiModelProperty("表名截取起始索引,比如表名叫做t_sale_contract_detail,生成的实体类为ContractDetail,则该字段为7") @Parameter(names = {"--tableStartIndex", "-tsi"}, description = "表名截取起始索引") private String tableStartIndex; } 复制代码
当连接上数据库后,我们需要解析读取的表结构,包括获取表字段,字段备注,字段类型等内容,对应此处创建的 Column 类。
/** * 数据表的解析内容 */ @Data public class Column { /** * 是否是主键 */ private Boolean isPrimaryKey; /** * 主键类型,Mybatis Plus实体类需要使用,默认为ASSIGN_ID(3) */ private String primaryKeyType = "ASSIGN_ID"; /** * 数据库表名称 */ private String tableName; /** * 表描述 */ private String tableDesc; /** * 数据库字段名称 **/ private String fieldName; /** * 数据库字段类型 **/ private String fieldType; /** * Java类型 */ private String javaType; /** * 是否是数字类型 */ private Boolean isNumber; /** * 数据库字段驼峰命名,saleBooke **/ private String camelName; /** * 数据库字段Pascal命名,SaleBook **/ private String pascalName; /** * 数据库字段注释 **/ private String comment; private String field; private String key; private Boolean isConfig; } 复制代码
最后创建一个常量类 Config,来存储常量信息。
public class Config { public static final String OutputPath = "." + File.separator + "output"; public static final String Author = "hresh"; } 复制代码
服务类
首先定义 FreeMarker 的使用代码:
/** * 使用FreeMarker 根据设定好的文件模板来生成相关文件 */ @Service public class FreemarkerService { private static final Logger logger = LoggerFactory.getLogger(FreemarkerService.class); @Autowired private Configuration configuration; /** * 输出文件模板 * * @param templateName resources 文件夹下的模板名,比如说model.ftl,是生成实体类的模块 * @param dataModel 表名,字段名等内容集合 * @param filePath 输出文件名,包括路径 * @param generateParameter * @throws Exception */ public void write(String templateName, Map<String, Object> dataModel, String filePath, GenerateParameter generateParameter) throws Exception { // FTL(freemarker templete language)模板的文件名称 Template template = configuration.getTemplate(dataModel.get("type") + File.separator + templateName + ".ftl"); File file; // 判断是不是多表,如果是,则按照表名生成各自的文件夹目录 if (generateParameter.isFlat()) { file = new File(Config.OutputPath + File.separator + dataModel.get("tempId") + File.separator + filePath); } else { file = new File(Config.OutputPath + File.separator + dataModel.get("tempId") + File.separator + dataModel.get("tableName") + File.separator + filePath); } if (!file.exists()) { file.getParentFile().mkdirs(); file.createNewFile(); } FileOutputStream fileOutputStream = new FileOutputStream(file); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, StandardCharsets.UTF_8); template.process(dataModel, outputStreamWriter); fileOutputStream.flush(); fileOutputStream.close(); } } 复制代码
接下来是本项目最核心的代码,通过读取数据表,获取表的定义信息,然后利用 FreeMarker 读取 Ftl 模板文件来生成关于该表的基础代码。
基础服务类 BaseService
public class BaseService { public String getUrl(GenerateParameter generateParameter) { return "jdbc:mysql://" + generateParameter.getHost() + ":" + generateParameter.getPort() + "/" + generateParameter.getDatabase() + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"; } // 数据库连接,类似于:DriverManager.getConnection("jdbc:mysql://localhost:3306/test_demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC","root","password"); public Connection getConnection(GenerateParameter generateParameter) throws Exception { return DriverManager.getConnection(getUrl(generateParameter), generateParameter.getUsername(), generateParameter.getPassword()); } /** * 根据表具体位置,获取表中字段的具体信息,包括字段名,字段类型,备注等 * * @param tableName * @param parameter * @return * @throws Exception */ public List<Column> getColumns(String tableName, GenerateParameter parameter, String[] commonColumns) throws Exception { // 数据库连接 Connection connection = getConnection(parameter); // 获取表定义的字段信息 ResultSet resultSet = connection.createStatement().executeQuery("SHOW FULL COLUMNS FROM " + tableName); List<Column> columnList = new ArrayList<>(); while (resultSet.next()) { String fieldName = resultSet.getString("Field"); Column column = new Column(); // 判断是否是主键 column.setIsPrimaryKey("PRI".equals(resultSet.getString("Key"))); // 获取字段名称 column.setFieldName(fieldName); // Mybatis Plus特定字段从核心类里获取 if (Objects.nonNull(commonColumns) && Arrays.asList(commonColumns).contains(fieldName)) { column.setIsCommonField(true); } else { column.setIsCommonField(false); } // 获取字段类型 column.setFieldType(resultSet.getString("Type").replaceAll("\\(.*\\)", "")); switch (column.getFieldType()) { case "json": case "longtext": case "char": case "varchar": case "text": column.setJavaType("String"); column.setIsNumber(false); break; case "date": case "datetime": column.setJavaType("Date"); column.setIsNumber(false); break; case "bit": column.setJavaType("Boolean"); column.setIsNumber(false); break; case "int": case "tinyint": column.setJavaType("Integer"); column.setIsNumber(true); break; case "bigint": column.setJavaType("Long"); column.setIsNumber(true); break; case "decimal": column.setJavaType("BigDecimal"); column.setIsNumber(true); break; case "varbinary": column.setJavaType("byte[]"); column.setIsNumber(false); break; default: throw new Exception(tableName + " " + column.getFieldName() + " " + column.getFieldType() + "类型没有解析"); } // 转换字段名称,receipt_sign_name字段改为 receiptSignName column.setCamelName(StringUtils.underscoreToCamel(column.getFieldName())); // 首字母大写 column.setPascalName(StringUtils.capitalize(column.getCamelName())); // 字段在数据库的注释 column.setComment(resultSet.getString("Comment")); columnList.add(column); } return columnList; } /** * 获取表的描述 * * @param tableName * @param parameter * @return * @throws Exception */ public String getTableComment(String tableName, GenerateParameter parameter) throws Exception { Connection connection = getConnection(parameter); ResultSet resultSet = connection.createStatement().executeQuery("SELECT table_comment FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = '" + parameter.getDatabase() + "' AND table_name = '" + tableName + "'"); String tableComment = ""; while (resultSet.next()) { tableComment = resultSet.getString("table_comment"); } return tableComment; } } 复制代码
GenerateService 获取表信息生成相关代码
@Service public class GenerateService extends BaseService{ private static final Logger logger = LoggerFactory.getLogger(GenerateService.class); @Autowired private FreemarkerService freemarkerService; /** * @param tableName 数据库表名 * @param parameter 模块名 * @param uuid * @throws Exception */ public void generate(String tableName, GenerateParameter parameter, String uuid) throws Exception { // 各模块包名,比如 com.msdn.sale 或 com.msdn.finance String packagePrefix = "com.msdn." + parameter.getModule(); // 分组 if (!StringUtils.isEmpty(parameter.getGroup())) { packagePrefix = packagePrefix + "." + parameter.getGroup(); } // 根据项目设计的表名获取到表名,比如表名叫做:t_sale_contract_detail // 现在表名截取起始索引该由参数配置 // int index = tableName.indexOf("_", 2); Integer index = new Integer(parameter.getTableStartIndex()); // 驼峰命名,首字母小写,比如:contractDetail String camelName = StringUtils.underscoreToCamel(tableName.substring(index)); Map<String, Object> dataModel = new HashMap<>(); //获取表中字段的具体信息,包括字段名,字段类型,备注等,排除指定字段 List<Column> columns = getColumns(tableName, parameter,Config.COMMON_COLUMNS); Column primaryColumn = columns.stream().filter(Column::getIsPrimaryKey).findFirst().orElse(null); dataModel.put("package", packagePrefix); dataModel.put("camelName", camelName); // 首字母转大写,作为实体类名称等 dataModel.put("pascalName", StringUtils.capitalize(camelName)); dataModel.put("moduleName", parameter.getModule()); dataModel.put("tableName", tableName); // 表描述 dataModel.put("tableComment", getTableComment(tableName, parameter)); dataModel.put("columns", columns); dataModel.put("primaryColumn", primaryColumn); dataModel.put("tempId", uuid); dataModel.put("author", Config.Author); dataModel.put("date", DateUtil.now()); dataModel.put("type", parameter.getType()); logger.info("准备生成模板代码的表名为:" + tableName + ",表描述为:" + dataModel.get("tableComment")); // 生成模板代码 logger.info("**********开始生成Model模板文件**********"); generateModel(dataModel, parameter); logger.info("**********开始生成VO视图模板文件**********"); generateVO(dataModel, parameter); logger.info("**********开始生成DTO模板文件**********"); generateDTO(dataModel, parameter); // logger.info("**********开始生成Struct模板文件**********"); // generateStruct(dataModel, parameter); logger.info("**********开始生成Mapper模板文件**********"); generateMapper(dataModel, parameter); logger.info("**********开始生成Service模板文件**********"); generateService(dataModel, parameter); logger.info("**********开始生成Controller模板文件**********"); generateController(dataModel, parameter); } /** * 生成 controller 模板代码 * * @param dataModel * @param generateParameter * @throws Exception */ private void generateController(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception { String path = "java" + File.separator + "controller" + File.separator + dataModel.get("pascalName") + "Controller.java"; freemarkerService.write("controller", dataModel, path, generateParameter); } private void generateDTO(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception { String path = "java" + File.separator + "dto" + File.separator + dataModel.get("pascalName"); freemarkerService.write("dto", dataModel, path + "DTO.java", generateParameter); freemarkerService.write("dto-page", dataModel, path + "QueryPageDTO.java", generateParameter); } // private void generateModel(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception { String path = "java" + File.separator + "model" + File.separator + dataModel.get("pascalName") + ".java"; freemarkerService.write("model", dataModel, path, generateParameter); } private void generateStruct(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception { String path = "java" + File.separator + "struct" + File.separator + dataModel.get("pascalName") + "Struct.java"; freemarkerService.write("struct", dataModel, path, generateParameter); } private void generateMapper(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception { String path = "java" + File.separator + "mapper" + File.separator + dataModel.get("pascalName") + "Mapper.java"; freemarkerService.write("mapper", dataModel, path, generateParameter); path = "resources" + File.separator + dataModel.get("pascalName") + "Mapper.xml"; freemarkerService.write("mapper-xml", dataModel, path, generateParameter); } private void generateService(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception { String path = "java" + File.separator + "service" + File.separator + dataModel.get("pascalName") + "Service.java"; freemarkerService.write("service", dataModel, path, generateParameter); path = "java" + File.separator + "service" + File.separator + "impl" + File.separator + dataModel.get("pascalName") + "ServiceImpl.java"; freemarkerService.write("service-impl", dataModel, path, generateParameter); } private void generateVO(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception { String path = "java" + File.separator + "vo" + File.separator + dataModel.get("pascalName") + "VO.java"; freemarkerService.write("vo", dataModel, path, generateParameter); } } 复制代码
控制器
为了更加方便地使用代码生成器,我们通过 swagger 来调用 Rest 服务接口。
@RestController public class GeneratorController { private static final Logger logger = LoggerFactory.getLogger(GeneratorController.class); @Autowired private GenerateService generateService; /* // 请求参数 { "database": "db_tl_sale", "flat": true, "type": "mybatis", "group": "base", "host": "127.0.0.1", "module": "sale", "password": "123456", "port": 3306, "table": [ "t_xs_sale_contract" ], "username": "root", "tableStartIndex":"5" } */ @PostMapping("/generator/build") public void build(@RequestBody GenerateParameter parameter, HttpServletResponse response) throws Exception { logger.info("**********欢迎使用基于FreeMarker的模板文件生成器**********"); logger.info("************************************************************"); String uuid = UUID.randomUUID().toString(); for (String table : parameter.getTable()) { generateService.generate(table, parameter, uuid); } logger.info("**********模板文件生成完毕,准备下载**********"); String path = Config.OutputPath + File.separator + uuid; //设置响应头控制浏览器的行为,这里我们下载zip response.setHeader("Content-disposition", "attachment; filename=code.zip"); response.setHeader("Access-Control-Expose-Headers", "Content-disposition"); // 将response中的输出流中的文件压缩成zip形式 ZipDirectory(path, response.getOutputStream()); // 递归删除目录 FileSystemUtils.deleteRecursively(new File(path)); logger.info("************************************************************"); logger.info("**********模板文件下载完毕,谢谢使用**********"); } /** * 一次性压缩多个文件,文件存放至一个文件夹中 */ public static void ZipDirectory(String directoryPath, ServletOutputStream outputStream) { InputStream input = null; ZipOutputStream output = null; try { output = new ZipOutputStream(outputStream); List<File> files = getFiles(new File(directoryPath)); for (File file : files) { input = new FileInputStream(file); output.putNextEntry(new ZipEntry(file.getPath().substring(directoryPath.length() + 1))); int temp = 0; while ((temp = input.read()) != -1) { output.write(temp); } input.close(); } output.close(); } catch (Exception e) { e.printStackTrace(); } finally { if (input != null) { try { input.close(); } catch (IOException e) { e.printStackTrace(); } } if (output != null) { try { output.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static List<File> getFiles(File file) { List<File> files = new ArrayList<>(); for (File subFile : file.listFiles()) { if (subFile.isDirectory()) { List<File> subFiles = getFiles(subFile); files.addAll(subFiles); } else { files.add(subFile); } } return files; } } 复制代码
启动类
@EnableSwagger2Doc @SpringBootApplication public class GeneratorApplication { /** * 测试的时候添加参数 -h 127.0.0.1 -P 3306 -d db_tl_sale -u root -p 123456 -m sale -g base -t t_xs_sale_contract,t_xs_sale_contract_detail * * @param args * @throws Exception */ public static void main(String[] args) { SpringApplication.run(GeneratorApplication.class, args); } } 复制代码
模板文件
定义的模板文件如下图所示:
其它代码
除了上述代码,还有一些工具类,以及公共组件,这里就不一一介绍了,感兴趣的同学可以去 github 上参看相关代码。
效果
启动项目后,直接访问 http://localhost:8525/swagger-ui.html#/。
传入参数根据个人需要按照如下格式整理信息:
{ "database": "db_tl_sale", "flat": true, "type": "mybatis", "group": "base", "host": "127.0.0.1", "module": "sale", "password": "123456", "port": 3306, "table": [ "t_xs_sale_contract" ], "username": "root", "tableStartIndex":"5" } 复制代码
然后点击执行,执行成功后点击下载,将生成好的代码下载到本地。文件结构如下图所示:
这里截取一部分代码图片,首先是实体类:
然后是查询实体类:
接着是 Service 接口:
以及对应的实现类:
最后是 controller:
扩展
一对多关联查询
resultMap
元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets
数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap
能够代替实现同等功能的数千行代码。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
需求:
目前订单类详情查询返回的结果中,除了包含订单类的全部信息,还需要返回多个订单子项的数据,也就是我们常说的一对多关系,那么在实际开发中如何操作呢?
首先我们看一下代码案例:
1、订单类
public class OmsOrder implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "订单id") private Long id; private Long memberId; private Long couponId; @ApiModelProperty(value = "订单编号") private String orderSn; @ApiModelProperty(value = "提交时间") private Date createTime; @ApiModelProperty(value = "用户帐号") private String memberUsername; @ApiModelProperty(value = "订单总金额") private BigDecimal totalAmount; @ApiModelProperty(value = "应付金额(实际支付金额)") private BigDecimal payAmount; @ApiModelProperty(value = "运费金额") private BigDecimal freightAmount; @ApiModelProperty(value = "促销优化金额(促销价、满减、阶梯价)") private BigDecimal promotionAmount; @ApiModelProperty(value = "积分抵扣金额") private BigDecimal integrationAmount; @ApiModelProperty(value = "优惠券抵扣金额") private BigDecimal couponAmount; @ApiModelProperty(value = "管理员后台调整订单使用的折扣金额") private BigDecimal discountAmount; @ApiModelProperty(value = "支付方式:0->未支付;1->支付宝;2->微信") private Integer payType; @ApiModelProperty(value = "订单来源:0->PC订单;1->app订单") private Integer sourceType; @ApiModelProperty(value = "订单状态:0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单") private Integer status; @ApiModelProperty(value = "订单类型:0->正常订单;1->秒杀订单") private Integer orderType; @ApiModelProperty(value = "物流公司(配送方式)") private String deliveryCompany; @ApiModelProperty(value = "物流单号") private String deliverySn; @ApiModelProperty(value = "自动确认时间(天)") private Integer autoConfirmDay; @ApiModelProperty(value = "可以获得的积分") private Integer integration; @ApiModelProperty(value = "可以活动的成长值") private Integer growth; @ApiModelProperty(value = "活动信息") private String promotionInfo; @ApiModelProperty(value = "发票类型:0->不开发票;1->电子发票;2->纸质发票") private Integer billType; @ApiModelProperty(value = "发票抬头") private String billHeader; @ApiModelProperty(value = "发票内容") private String billContent; @ApiModelProperty(value = "收票人电话") private String billReceiverPhone; @ApiModelProperty(value = "收票人邮箱") private String billReceiverEmail; @ApiModelProperty(value = "收货人姓名") private String receiverName; @ApiModelProperty(value = "收货人电话") private String receiverPhone; @ApiModelProperty(value = "收货人邮编") private String receiverPostCode; @ApiModelProperty(value = "省份/直辖市") private String receiverProvince; @ApiModelProperty(value = "城市") private String receiverCity; @ApiModelProperty(value = "区") private String receiverRegion; @ApiModelProperty(value = "详细地址") private String receiverDetailAddress; @ApiModelProperty(value = "订单备注") private String note; @ApiModelProperty(value = "确认收货状态:0->未确认;1->已确认") private Integer confirmStatus; @ApiModelProperty(value = "删除状态:0->未删除;1->已删除") private Integer deleteStatus; @ApiModelProperty(value = "下单时使用的积分") private Integer useIntegration; @ApiModelProperty(value = "支付时间") private Date paymentTime; @ApiModelProperty(value = "发货时间") private Date deliveryTime; @ApiModelProperty(value = "确认收货时间") private Date receiveTime; @ApiModelProperty(value = "评价时间") private Date commentTime; @ApiModelProperty(value = "修改时间") private Date modifyTime; } 复制代码
2、订单子项类
public class OmsOrderItem implements Serializable { private static final long serialVersionUID = 1L; private Long id; @ApiModelProperty(value = "订单id") private Long orderId; @ApiModelProperty(value = "订单编号") private String orderSn; private Long productId; private String productPic; private String productName; private String productBrand; private String productSn; @ApiModelProperty(value = "销售价格") private BigDecimal productPrice; @ApiModelProperty(value = "购买数量") private Integer productQuantity; @ApiModelProperty(value = "商品sku编号") private Long productSkuId; @ApiModelProperty(value = "商品sku条码") private String productSkuCode; @ApiModelProperty(value = "商品分类id") private Long productCategoryId; @ApiModelProperty(value = "商品促销名称") private String promotionName; @ApiModelProperty(value = "商品促销分解金额") private BigDecimal promotionAmount; @ApiModelProperty(value = "优惠券优惠分解金额") private BigDecimal couponAmount; @ApiModelProperty(value = "积分优惠分解金额") private BigDecimal integrationAmount; @ApiModelProperty(value = "该商品经过优惠后的分解金额") private BigDecimal realAmount; private Integer giftIntegration; private Integer giftGrowth; @ApiModelProperty(value = "商品销售属性:[{'key':'颜色','value':'颜色'},{'key':'容量','value':'4G'}]") private String productAttr; } 复制代码
3、前端返回类
public class OmsOrderDetail extends OmsOrder { @Getter @Setter @ApiModelProperty("订单商品列表") private List<OmsOrderItem> orderItemList; } 复制代码
4、OmsOrderMapper.xml 文件中自定义 SQL 语句
<resultMap id="orderDetailResultMap" type="com.macro.mall.dto.OmsOrderDetail" extends="com.macro.mall.mapper.OmsOrderMapper.BaseResultMap"> <collection property="orderItemList" resultMap="com.macro.mall.mapper.OmsOrderItemMapper.BaseResultMap" columnPrefix="item_"/> </resultMap> <select id="getDetail" resultMap="orderDetailResultMap"> SELECT o.*, oi.id item_id, oi.product_id item_product_id, oi.product_sn item_product_sn, oi.product_pic item_product_pic, oi.product_name item_product_name, oi.product_brand item_product_brand, oi.product_price item_product_price, oi.product_quantity item_product_quantity, oi.product_attr item_product_attr FROM oms_order o LEFT JOIN oms_order_item oi ON o.id = oi.order_id WHERE o.id = #{id} ORDER BY oi.id ASC DESC </select> 复制代码
其中 com.macro.mall.mapper.OmsOrderItemMapper.BaseResultMap
是引用自 OmsOrderItemMapper.xml 文件中的定义,
<resultMap id="BaseResultMap" type="com.macro.mall.model.OmsOrderItem"> <id column="id" property="id" /> <result column="order_id" property="orderId" /> <result column="order_sn" property="orderSn" /> <result column="product_id" jdbcType="BIGINT" property="productId" /> <result column="product_pic" jdbcType="VARCHAR" property="productPic" /> <result column="product_name" jdbcType="VARCHAR" property="productName" /> <result column="product_brand" jdbcType="VARCHAR" property="productBrand" /> <result column="product_sn" jdbcType="VARCHAR" property="productSn" /> <result column="product_price" jdbcType="DECIMAL" property="productPrice" /> <result column="product_quantity" jdbcType="INTEGER" property="productQuantity" /> <result column="product_sku_id" jdbcType="BIGINT" property="productSkuId" /> <result column="product_sku_code" jdbcType="VARCHAR" property="productSkuCode" /> <result column="product_category_id" jdbcType="BIGINT" property="productCategoryId" /> <result column="promotion_name" jdbcType="VARCHAR" property="promotionName" /> <result column="promotion_amount" jdbcType="DECIMAL" property="promotionAmount" /> <result column="coupon_amount" jdbcType="DECIMAL" property="couponAmount" /> <result column="integration_amount" jdbcType="DECIMAL" property="integrationAmount" /> <result column="real_amount" jdbcType="DECIMAL" property="realAmount" /> <result column="gift_integration" jdbcType="INTEGER" property="giftIntegration" /> <result column="gift_growth" jdbcType="INTEGER" property="giftGrowth" /> <result column="product_attr" jdbcType="VARCHAR" property="productAttr" /> </resultMap> 复制代码
5、执行效果
这种查询方式相较于先查主表,再根据主表字段关联查询子表信息,减少了 IO 连接查询次数,效率更高一些。
resultMap模板生成
通过上述代码我们可知,实现一对多关联查询的关键在于定义子项数据(多)的 resultMap 定义,既然我们通过代码生成器生成了基本的项目代码,那么是否可以生成 resultMap 呢?说干就干,代码如下:
1、定义模板 ftl 文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="${package}.mapper.${pascalName}Mapper"> <resultMap id="BaseResultMap" type="${package}.model.${pascalName}"> <#list columns as column> <#if column.isPrimaryKey> <id column="${column.fieldName}" property="${column.camelName}" /> <#else> <result column="${column.fieldName}" property="${column.camelName}" /> </#if> </#list> </resultMap> </mapper> 复制代码
2、编写服务类 XmlGenerateService
@Service public class XmlGenerateService extends BaseService { private static final Logger logger = LoggerFactory.getLogger(XmlGenerateService.class); @Autowired private FreemarkerService freemarkerService; /** * @param tableName 数据库表名 * @param parameter 模块名 * @param uuid * @throws Exception */ public void generate(String tableName, GenerateParameter parameter, String uuid) throws Exception { // 各模块包名,比如 com.msdn.sale 或 com.msdn.finance String packagePrefix = "com.msdn." + parameter.getModule(); // 分组 if (!StringUtils.isEmpty(parameter.getGroup())) { packagePrefix = packagePrefix + "." + parameter.getGroup(); } // 根据项目设计的表名获取到表名,比如表名叫做:t_sale_contract_detail // 现在表名截取起始索引该由参数配置 // int index = tableName.indexOf("_", 2); Integer index = new Integer(parameter.getTableStartIndex()); // 驼峰命名,首字母小写,比如:contractDetail String camelName = StringUtils.underscoreToCamel(tableName.substring(index)); Map<String, Object> dataModel = new HashMap<>(); //获取表中字段的具体信息,包括字段名,字段类型,备注等,排除指定字段 List<Column> columns = getColumns(tableName, parameter, null); Column primaryColumn = columns.stream().filter(Column::getIsPrimaryKey).findFirst().orElse(null); dataModel.put("package", packagePrefix); dataModel.put("camelName", camelName); // 首字母转大写,作为实体类名称等 dataModel.put("pascalName", StringUtils.capitalize(camelName)); dataModel.put("moduleName", parameter.getModule()); dataModel.put("tableName", tableName); // 表描述 dataModel.put("tableComment", getTableComment(tableName, parameter)); dataModel.put("columns", columns); dataModel.put("primaryColumn", primaryColumn); dataModel.put("tempId", uuid); dataModel.put("author", Config.Author); dataModel.put("date", DateUtil.now()); dataModel.put("type", parameter.getType()); logger.info("准备生成模板代码的表名为:" + tableName + ",表描述为:" + dataModel.get("tableComment")); // 生成模板代码 logger.info("**********开始生成Model模板文件**********"); generateXML(dataModel, parameter); } /** * 生成 controller 模板代码 * * @param dataModel * @param generateParameter * @throws Exception */ private void generateXML(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception { String path = "resources" + File.separator + "xml" + File.separator + dataModel.get("pascalName") + "Mapper.xml"; freemarkerService.write("mybatis-xml", dataModel, path, generateParameter); } } 复制代码
3、服务接口
@PostMapping("/generator/buildXml") public void buildXml(@RequestBody GenerateParameter parameter, HttpServletResponse response) throws Exception { logger.info("**********欢迎使用基于FreeMarker的模板文件生成器**********"); logger.info("************************************************************"); String uuid = UUID.randomUUID().toString(); for (String table : parameter.getTable()) { xmlGenerateService.generate(table, parameter, uuid); } logger.info("**********模板文件生成完毕,准备下载**********"); String path = Config.OutputPath + File.separator + uuid; //设置响应头控制浏览器的行为,这里我们下载zip response.setHeader("Content-disposition", "attachment; filename=code.zip"); response.setHeader("Access-Control-Expose-Headers", "Content-disposition"); // 将response中的输出流中的文件压缩成zip形式 ZipDirectory(path, response.getOutputStream()); // 递归删除目录 FileSystemUtils.deleteRecursively(new File(path)); logger.info("************************************************************"); logger.info("**********模板文件下载完毕,谢谢使用**********"); } 复制代码
4、通过 swagger 调用 api
5、执行结果
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.msdn.mall.mapper.OmsOrderItemMapper"> <resultMap id="BaseResultMap" type="com.msdn.mall.model.OmsOrderItem"> <id column="order_item_id" property="orderItemId" /> <result column="order_id" property="orderId" /> <result column="order_sn" property="orderSn" /> <result column="product_id" property="productId" /> <result column="product_pic" property="productPic" /> <result column="product_name" property="productName" /> <result column="product_brand" property="productBrand" /> <result column="product_sn" property="productSn" /> <result column="product_price" property="productPrice" /> <result column="purchase_amount" property="purchaseAmount" /> <result column="product_sku_id" property="productSkuId" /> <result column="product_sku_code" property="productSkuCode" /> <result column="product_category_id" property="productCategoryId" /> <result column="sp1" property="sp1" /> <result column="sp2" property="sp2" /> <result column="sp3" property="sp3" /> <result column="promotion_name" property="promotionName" /> <result column="promotion_money" property="promotionMoney" /> <result column="coupon_money" property="couponMoney" /> <result column="integration_money" property="integrationMoney" /> <result column="real_money" property="realMoney" /> <result column="gift_integration" property="giftIntegration" /> <result column="gift_growth" property="giftGrowth" /> <result column="product_attr" property="productAttr" /> <result column="is_deleted" property="isDeleted" /> <result column="create_user_code" property="createUserCode" /> <result column="create_user_name" property="createUserName" /> <result column="create_date" property="createDate" /> <result column="update_user_code" property="updateUserCode" /> <result column="update_user_name" property="updateUserName" /> <result column="update_date" property="updateDate" /> <result column="version" property="version" /> </resultMap> </mapper> 复制代码
后续
在生产开发中如果还遇到好玩的东西,会不定期追加更新,希望工具越来越强大。
总结
虽然 Mybatis 和 Mybatis Plus 都有相关的代码生成器配置,但是构建器代码不容易整合,外部调用也不方便,最主要的是无法满足实际需求。为了能够一次性生成所有代码,最终选择 SpringBoot 和 FreeMarker 来构建我们专属的代码生成器。
除了可以生成 Java 相关代码,FreeMarker 还可以根据模板文件来生成前端代码,又或者是 Word 文档等,后续更多功能会根据情况逐步补充的。
感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!