用 ChatGPT 辅助开发 SQL 注释生成工具的实践
在日常开发中,我们经常会收到产品或业务方整理好的 Excel 字段文档,比如字段名和字段说明,期望我们将这些信息转化成数据库字段的注释(COMMENT ON COLUMN)语句。
手动转写不仅枯燥,还容易出错。这次我用 ChatGPT 辅助完成了一个自动读取 Excel 并生成 SQL 注释脚本的 Java 工具,整个过程不到半小时,效果非常好。
我的需求
初始需求非常明确:
读取目录下所有 Excel 文件的所有 Sheet,如果前两列是 "FIELD NAME"和 "FIELD DESCRIPTION",就生成如下 SQL:
comment on column <目录名>.<sheet名>.<FIELD NAME> is '<FIELD DESCRIPTION>';
并将所有结果合并输出到一个 .sql 文件中。
用 ChatGPT 辅助开发的过程
我首先将需求发给了 ChatGPT,大致描述如下:
"我的jdk版本是java8,用 Java 写一个小工具,遍历目录中所有 Excel 文件,读取每个 sheet 的前两列,字段名是 FIELD NAME 和 FIELD DESCRIPTION,就生成 SQL 注释语句。"
ChatGPT 很快给出了完整的实现建议,包括:
- 使用 Apache POI 读取 Excel 文件(兼容 .xls和.xlsx);
- 判断 Sheet 的前两列标题;
- 自动拼接 SQL 并输出;
- 遍历目录、写文件逻辑。
甚至还考虑到了文件命名安全、空值过滤等细节。
ChatGPT 的具体回复
下面是一个完整的 Java 小工具,它会:
- 
遍历指定目录中的所有 Excel 文件(支持 .xlsx格式)。
- 
对每个 Excel 文件的所有 Sheet,读取前两列。 
- 
如果前两列的标题是 FIELD NAME和FIELD DESCRIPTION,则处理该 Sheet:- 为每一行生成 SQL:comment on column <目录名>.<sheet名>.<FIELD NAME> is '<FIELD DESCRIPTION>';
 
- 为每一行生成 SQL:
- 
将所有 SQL 语句合并写入同一个 .sql文件。
🔧 依赖
请确保你使用的是支持 Maven 的项目,并在 pom.xml 中添加如下依赖:
            
            
              xml
              
              
            
          
          <dependencies>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.3</version>
    </dependency>
</dependencies>📂 工具类 Java 源码
            
            
              java
              
              
            
          
          import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class ExcelCommentSqlGenerator {
    public static void main(String[] args) throws IOException {
        if (args.length < 2) {
            System.out.println("用法: java ExcelCommentSqlGenerator <excel目录路径> <输出sql文件路径>");
            return;
        }
        File excelDir = new File(args[0]);
        File outputSqlFile = new File(args[1]);
        if (!excelDir.isDirectory()) {
            System.err.println("错误:路径 " + excelDir.getAbsolutePath() + " 不是一个有效的目录。");
            return;
        }
        List<String> allSqlStatements = new ArrayList<>();
        File[] excelFiles = excelDir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".xlsx") && !name.startsWith("~$"); // 排除临时文件
            }
        });
        if (excelFiles == null || excelFiles.length == 0) {
            System.err.println("未找到任何 Excel 文件。");
            return;
        }
        for (File excelFile : excelFiles) {
            String directoryName = excelDir.getName(); // 用文件夹名作为"目录名"
            FileInputStream fis = null;
            Workbook workbook = null;
            try {
                fis = new FileInputStream(excelFile);
                workbook = new XSSFWorkbook(fis);
                for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
                    Sheet sheet = workbook.getSheetAt(i);
                    String sheetName = sheet.getSheetName();
                    Row headerRow = sheet.getRow(0);
                    if (headerRow == null || headerRow.getPhysicalNumberOfCells() < 2) {
                        continue;
                    }
                    String col1 = getCellStringValue(headerRow.getCell(0));
                    String col2 = getCellStringValue(headerRow.getCell(1));
                    if (!"FIELD NAME".equalsIgnoreCase(col1) || !"FIELD DESCRIPTION".equalsIgnoreCase(col2)) {
                        continue;
                    }
                    for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
                        Row row = sheet.getRow(rowIndex);
                        if (row == null) continue;
                        String fieldName = getCellStringValue(row.getCell(0));
                        String fieldDesc = getCellStringValue(row.getCell(1)).replace("'", "''"); // 转义单引号
                        if (!fieldName.isEmpty() && !fieldDesc.isEmpty()) {
                            String sql = "comment on column " + directoryName + "." + sheetName + "." + fieldName
                                    + " is '" + fieldDesc + "';";
                            allSqlStatements.add(sql);
                        }
                    }
                }
            } catch (Exception e) {
                System.err.println("处理文件失败: " + excelFile.getName() + ",错误:" + e.getMessage());
            } finally {
                if (workbook != null) workbook.close();
                if (fis != null) fis.close();
            }
        }
        // 写入 .sql 文件
        BufferedWriter writer = new BufferedWriter(new FileWriter(outputSqlFile));
        for (String sql : allSqlStatements) {
            writer.write(sql);
            writer.newLine();
        }
        writer.close();
        System.out.println("共生成 " + allSqlStatements.size() + " 条 SQL,已写入: " + outputSqlFile.getAbsolutePath());
    }
    private static String getCellStringValue(Cell cell) {
        if (cell == null) return "";
        switch (cell.getCellType()) {
            case STRING:
                return cell.getStringCellValue().trim();
            case NUMERIC:
                return String.valueOf(cell.getNumericCellValue());
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            case FORMULA:
                try {
                    return cell.getStringCellValue().trim(); // 若为字符串公式
                } catch (IllegalStateException e) {
                    try {
                        return String.valueOf(cell.getNumericCellValue()); // 若为数字公式
                    } catch (Exception ex) {
                        return "";
                    }
                }
            default:
                return "";
        }
    }
}📌 使用说明
假设你已经将该工具编译并打包为 ExcelCommentSqlGenerator.jar,可以这样使用命令行运行它:
            
            
              bash
              
              
            
          
          java -jar ExcelCommentSqlGenerator.jar ./excel目录 ./output/comment_output.sql需要我帮你封装为命令行工具、打包成可运行 jar 或处理 .xls 格式也可以继续告诉我。
最终效果
生成的 SQL 类似如下:
            
            
              sql
              
              
            
          
          comment on column finance.customer.ID is '客户编号';
comment on column finance.customer.NAME is '客户名称';
comment on column hr.employee.ENTRY_DATE is '入职日期';随后,为了适配sqlserver数据库,又向ChatGPT提了新的需求,让其帮我编写一个可以将comment on结构的sql语句转为适用于sqlserver的小工具。
当然可以!下面是将 Oracle 注释语句转换为 SQL Server sp_addextendedproperty 格式的 Java 脚本,支持从指定文件读取 Oracle 注释语句并生成 SQL Server 格式的输出文件。
✅ Java 脚本功能说明:
- 
输入文件格式:Oracle 的注释语句,形如: sqlCOMMENT ON COLUMN EXIMSYS.TRX_VOUCHER_HEADER.AB_TIMES IS 'System Reserved Field';
- 
输出文件格式(SQL Server): sqlEXEC sp_addextendedproperty @name = N'MS_Description', @value = N'System Reserved Field', @level0type = N'SCHEMA', @level0name = N'EXIMSYS', @level1type = N'TABLE', @level1name = N'TRX_VOUCHER_HEADER', @level2type = N'COLUMN', @level2name = N'AB_TIMES';
✅ Java 脚本:OracleCommentConverter.java
        
            
            
              java
              
              
            
          
          import java.io.*;
import java.nio.file.*;
import java.util.regex.*;
public class OracleCommentConverter {
    public static void main(String[] args) throws IOException {
        if (args.length < 2) {
            System.out.println("用法: java OracleCommentConverter <输入文件路径> <输出文件路径>");
            return;
        }
        Path inputPath = Paths.get(args[0]);
        Path outputPath = Paths.get(args[1]);
        if (!Files.exists(inputPath)) {
            System.err.println("错误:输入文件不存在:" + inputPath);
            return;
        }
        Pattern pattern = Pattern.compile(
            "COMMENT ON COLUMN ([^.]+)\\.([^.]+)\\.([ ]*[^ ]+) IS '(.+?)';?",
            Pattern.CASE_INSENSITIVE
        );
        try (
            BufferedReader reader = Files.newBufferedReader(inputPath);
            BufferedWriter writer = Files.newBufferedWriter(outputPath)
        ) {
            String line;
            while ((line = reader.readLine()) != null) {
                Matcher matcher = pattern.matcher(line.trim());
                if (matcher.find()) {
                    String schema = matcher.group(1).trim();
                    String table = matcher.group(2).trim();
                    String column = matcher.group(3).trim();
                    String comment = matcher.group(4).trim().replace("'", "''");
                    writer.write("EXEC sp_addextendedproperty \n");
                    writer.write("  @name = N'MS_Description',\n");
                    writer.write("  @value = N'" + comment + "',\n");
                    writer.write("  @level0type = N'SCHEMA', @level0name = N'" + schema + "',\n");
                    writer.write("  @level1type = N'TABLE',  @level1name = N'" + table + "',\n");
                    writer.write("  @level2type = N'COLUMN', @level2name = N'" + column + "';\n\n");
                }
            }
            System.out.println("✅ 转换完成,输出文件:" + outputPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}🛠 小贴士
- 输入文件建议是 UTF-8 编码(含中文注释也支持);
- Java 会自动处理换行、空格;
- 你可以打成 .jar方便后续自动批量执行。
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P password -i /GitRepo/Comments.sql > /GitRepo/log.txt
由于sqlserver脚本中sp_addextendedproperty执行时若表或表中对应字段不存在,会直接退出脚本,因此加了需求,遇错不报错 你这段 SQL 脚本会在
sqlcmd中执行失败直接退出 ,很可能是因为目标扩展属性已存在 ,而sp_addextendedproperty不能重复添加同名属性,会报错:
Msg 15233: Property cannot be added because it already exists.
这类错误会被 sqlcmd 认为是严重错误,导致后续语句不会执行。
✅ 解决方案(推荐):
🔁 使用 IF EXISTS 判断后再添加,或先删除再添加
你可以改用如下结构:
            
            
              sql
              
              
            
          
          -- 删除已存在的扩展属性(忽略失败)
BEGIN TRY
  EXEC sp_dropextendedproperty 
    @name = N'MS_Description',
    @level0type = N'SCHEMA', @level0name = N'EXIMTRX',
    @level1type = N'TABLE',  @level1name = N'ADV_BANK_CUST',
    @level2type = N'COLUMN', @level2name = N'BANK_N90_REF_20';
END TRY
BEGIN CATCH
END CATCH;
GO
-- 添加扩展属性
BEGIN TRY
  EXEC sp_addextendedproperty 
    @name = N'MS_Description',
    @value = N'Bank N90 Reference for Tag 20',
    @level0type = N'SCHEMA', @level0name = N'EXIMTRX',
    @level1type = N'TABLE',  @level1name = N'ADV_BANK_CUST',
    @level2type = N'COLUMN', @level2name = N'BANK_N90_REF_20';
END TRY
BEGIN CATCH
  PRINT 'add failed: ' + ERROR_MESSAGE();
END CATCH;
GO🧪 另一种方式:用 sp_updateextendedproperty 更新
如果你知道属性存在,可以直接更新:
            
            
              sql
              
              
            
          
          EXEC sp_updateextendedproperty 
  @name = N'MS_Description',
  @value = N'New description here',
  @level0type = N'SCHEMA', @level0name = N'EXIMTRX',
  @level1type = N'TABLE',  @level1name = N'ADV_BANK_CUST',
  @level2type = N'COLUMN', @level2name = N'BANK_N90_REF_20';不过如果它不存在也会报错,所以更安全还是先删除再加 ,或者使用 TRY...CATCH 包裹。
🔧 配套建议
如果你还是希望继续执行,即使有报错:
- 
在 .sql文件开头加上:sql:ON ERROR CONTINUE
- 
或者你在命令行执行时,不加 -b参数,并加 stderr 输出:bash/opt/mssql-tools/bin/sqlcmd \ -S localhost -U sa -P QWEasd2023 \ -i /GitRepo/EXIMTRX_Comments.sql \ > /GitRepo/log.txt 2>&1
根据实际需求,最终采用的第一种先删后增的方式来修改脚本,同时向ChatGPT提出了新的需求
改写下结构
- writer.write("EXEC sp_addextendedproperty \n");
- writer.write(" @name = N'MS_Description',\n");
- writer.write(" @value = N'" + comment + "',\n");
- writer.write(" @level0type = N'SCHEMA', @level0name = N'" + schema + "',\n");
- writer.write(" @level1type = N'TABLE', @level1name = N'" + table + "',\n");
- writer.write(" @level2type = N'COLUMN', @level2name = N'" + column + "';\n\n");
你可以将原本的 Java 写文件代码改为自动输出"先删除再添加扩展属性"的形式,每个字段的注释生成代码可以这么写:
            
            
              java
              
              
            
          
          writer.write("-- 删除已存在的扩展属性(忽略失败)\n");
writer.write("BEGIN TRY\n");
writer.write("  EXEC sp_dropextendedproperty \n");
writer.write("    @name = N'MS_Description',\n");
writer.write("    @level0type = N'SCHEMA', @level0name = N'" + schema + "',\n");
writer.write("    @level1type = N'TABLE',  @level1name = N'" + table + "',\n");
writer.write("    @level2type = N'COLUMN', @level2name = N'" + column + "';\n");
writer.write("END TRY\n");
writer.write("BEGIN CATCH\n");
writer.write("END CATCH;\n");
writer.write("GO\n\n");
writer.write("-- 添加扩展属性\n");
writer.write("BEGIN TRY\n");
writer.write("  EXEC sp_addextendedproperty \n");
writer.write("    @name = N'MS_Description',\n");
writer.write("    @value = N'" + comment + "',\n");
writer.write("    @level0type = N'SCHEMA', @level0name = N'" + schema + "',\n");
writer.write("    @level1type = N'TABLE',  @level1name = N'" + table + "',\n");
writer.write("    @level2type = N'COLUMN', @level2name = N'" + column + "';\n");
writer.write("END TRY\n");
writer.write("BEGIN CATCH\n");
writer.write("  PRINT 'add failed: ' + ERROR_MESSAGE();\n");
writer.write("END CATCH;\n");
writer.write("GO\n\n");✅ 效果举例(生成后)
            
            
              sql
              
              
            
          
          -- 删除已存在的扩展属性(忽略失败)
BEGIN TRY
  EXEC sp_dropextendedproperty 
    @name = N'MS_Description',
    @level0type = N'SCHEMA', @level0name = N'EXIMTRX',
    @level1type = N'TABLE',  @level1name = N'ADV_BANK_CUST',
    @level2type = N'COLUMN', @level2name = N'BANK_N90_REF_20';
END TRY
BEGIN CATCH
END CATCH;
GO
-- 添加扩展属性
BEGIN TRY
  EXEC sp_addextendedproperty 
    @name = N'MS_Description',
    @value = N'Bank N90 Reference for Tag 20',
    @level0type = N'SCHEMA', @level0name = N'EXIMTRX',
    @level1type = N'TABLE',  @level1name = N'ADV_BANK_CUST',
    @level2type = N'COLUMN', @level2name = N'BANK_N90_REF_20';
END TRY
BEGIN CATCH
  PRINT 'add failed: ' + ERROR_MESSAGE();
END CATCH;
GO总结:AI 编程的价值
这个工具虽然简单,但体现了 AI 编程的几个核心优势:
- 节省查文档时间:我不再去查 Apache POI 的各种方法;
- 提升代码组织能力:ChatGPT 给出的逻辑非常清晰,易于维护;
- 快速从需求到交付:10 分钟内拿到原型代码,稍加修改就可投入使用。
很多人觉得 ChatGPT 是"代码自动生成器",但在我看来,它更像是一个随时待命、可以理解上下文的"资深搭档"。
推荐给你
如果你也经常处理重复性文档转化工作,推荐你试试用 ChatGPT 写个小工具------ 只要你能描述出规则,ChatGPT 就能帮你把它实现为脚本或服务。
一人开发,不再孤独。