需求分析

后管页面的功能落实到数据上,其实也无非是对数据的增删改查,对应到SQL就是CRUD的操作。再落实到业务提出的具体功能上,比如新增菜单就是执行insert操作,修改布局数据就是执行update操作。想到这里,思路就很明显了——在后管页面上增加“导出为insert语句”和“导出为update语句”的功能。
以菜单功能为例,在后管中菜单的列表页上增加导出SQL按钮,鼠标放到哪一行数据上,哪一行就弹出导出为insert语句和导出为update语句的气泡按钮,点击按钮之后调用接口,后台从数据库查询这条数据,然后据此生成insert或update语句。

代码示例

下面以user表为例,演示生成insert语句的代码

数据准备

创建user表,并插入3条数据

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(100) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `email_address` varchar(100) DEFAULT NULL COMMENT '电子邮箱地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

INSERT INTO `user`(`id`, `name`, `age`, `email_address`) VALUES (1, 'tom', 10, 'tom.cat@qq.com');
INSERT INTO `user`(`id`, `name`, `age`, `email_address`) VALUES (2, 'jerry', 20, 'jerry.mouse@163.com');
INSERT INTO `user`(`id`, `name`, `age`, `email_address`) VALUES (3, 'Jessica', 15, 'jessica1999@gmail.com');

JAVA代码

由下层往上,依次创建SqlExportMapper SqlExportService SqlExportController 项目由SpringBoot搭建,持久层框架为MyBatis

@Mapper
public interface SqlExportMapper {
    @Select("SELECT * FROM ${table} WHERE ${where}")
    List<LinkedHashMap<String, Object>> select(@Param("table") String table, @Param("where") String where);
}
@Service
public class SqlExportService {
    @Autowired
    private SqlExportMapper sqlExportMapper;

    public List<LinkedHashMap<String, Object>> select(String table, String where) {
        return sqlExportMapper.select(table, where);
    }

    public String assembleUpdateSQL(Map<String, Object> record, String table, String where, List<String> columnsToUpdate) {
        StringBuilder sb = new StringBuilder("update ").append(table).append(" set ");
        int i = 1;
        for (String column : columnsToUpdate) {
            Object value = record.get(column);
            if (value == null) {
                sb.append(column).append("=").append("null");
            } else if (value instanceof Number) {
                sb.append(column).append("=").append(value);
            } else {
                sb.append(column).append("='").append(value.toString()).append("'");
            }
            if (i < columnsToUpdate.size()) {
                sb.append(", ");
                i++;
            }
        }
        sb.append(" where ").append(where).append(";");
        return sb.toString();
    }

    public String assembleInsertSQL(Map<String, Object> record, String table) {
        StringBuilder sb = new StringBuilder("insert into ").append(table).append(" ");
        sb.append(assembleColumnNames(record)).append("values ").append(assembleColumnValues(record)).append(";");
        return sb.toString();
    }

    private String assembleColumnNames(Map<String, Object> record) {
        StringBuilder sb = new StringBuilder("(");
        int i = 1;
        for (String key : record.keySet()) {
            sb.append(key);
            if (i < record.size()) {
                sb.append(", ");
                i++;
            }
        }
        sb.append(")");
        return sb.toString();
    }

    private String assembleColumnValues(Map<String, Object> record) {
        StringBuilder sb = new StringBuilder("(");
        int i = 1;
        for (Map.Entry<String, Object> entry : record.entrySet()) {
            Object value = entry.getValue();
            if (value == null) {
                sb.append("null");
            } else if (value instanceof Number) {
                sb.append(value);
            } else {
                sb.append("'").append(value.toString()).append("'");
            }
            if (i < record.size()) {
                sb.append(", ");
                i++;
            }
        }
        sb.append(")");
        return sb.toString();
    }
}
@RestController
public class SqlExportController {
    @Autowired
    private SqlExportService sqlExportService;

    @RequestMapping("/generateInsert")
    public String generateInsert(String table, String where) {
        List<LinkedHashMap<String, Object>> records = sqlExportService.select(table, where);
        StringBuilder returnSql = new StringBuilder();
        for (Map<String, Object> record : records) {
            String sql = sqlExportService.assembleInsertSQL(record, table);
            returnSql.append(sql).append("\r\n");
        }
        return returnSql.toString();
    }

    @RequestMapping("/generateUpdate")
    public String generateUpdate(String table, String where,
                                 @RequestParam("columnsToUpdate") List<String> columnsToUpdate) {
        List<LinkedHashMap<String, Object>> records = sqlExportService.select(table, where);
        StringBuilder returnSql = new StringBuilder();
        for (Map<String, Object> record : records) {
            String sql = sqlExportService.assembleUpdateSQL(record, table, where, columnsToUpdate);
            returnSql.append(sql).append("\r\n");
        }
        return returnSql.toString();
    }
}

执行效果

生成insert语句

1、http://localhost:8080/generateInsert?table=user&where=id = 1

java 生成sql工具 java中写sql语句_mybatis


2、http://localhost:8080/generateInsert?table=user&where=id in (1,2)

java 生成sql工具 java中写sql语句_java 生成sql工具_02

生成update语句

1、http://localhost:8080/generateUpdate?table=user&where=id = 1&columnsToUpdate=name,email_address

java 生成sql工具 java中写sql语句_sql_03


2、http://localhost:8080/generateUpdate?table=user&where=id =3&columnsToUpdate=name

java 生成sql工具 java中写sql语句_java_04

代码注意点

Mapper中的select方法需要可以从任意表以任意条件查询数据,所以table和where都是使用${}包围起来的,而不是常用的#{}
在MyBatis中,#{}表示预编译参数,框架先会生成预编译语句(PreparedStatement),把参数的位置放上问号(?),在执行SQL时,再将问号替换为实际传入的参数。预编译语句一次编译、多次执行,省去了解析优化等繁琐过程,此外预编译语句能防止sql 注入(就是在问号的左右加上了单引号)。${}则是表示字符串拼接,参数传入的是什么,就参数拼接到对应的位置,这就导致SQL每次都需要编译,同时有sql注入的风险。因此在绝大多数情况下,我们都是使用#{}的。

但是在我们的需求中,表名和where条件都是需要动态拼接的,如果使用#{},SQL写成下面这样子

@Select("SELECT * FROM #{table} WHERE #{where}")
List<LinkedHashMap<String, Object>> select(@Param("table") String table, @Param("where") String where);

预编译的SQL语句就成了下面这样

SELECT * FROM ? WHERE ?

假设传入的table=“user”,where=“id = 2”,那么实际执行的SQL就是

SELECT * FROM 'user' WHERE 'id=2'

上面最终执行的SQL语句中,由于"user"和"id=2"都被单引号包起来了,导致SQL的语法都错了,最终执行报错。因此在这个需求里,table和where都只能使用${}包裹起来。
除此以外,我们可以使用${}的原因还有这是面向后管页面的接口,只对内网开放,不用担心SQL注入的问题。注意如果是对外网开放的接口,MyBatis的SQL当中尽量不要使用${}拼接参数