用 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 就能帮你把它实现为脚本或服务。
一人开发,不再孤独。