add DDLBuilder

This commit is contained in:
Looly 2025-10-28 22:11:42 +08:00
parent 0384adcb57
commit 182103120c
11 changed files with 254 additions and 65 deletions

View File

@ -0,0 +1,187 @@
package cn.hutool.v7.db.dialect;
import cn.hutool.v7.core.collection.CollUtil;
import cn.hutool.v7.core.lang.Assert;
import cn.hutool.v7.core.text.StrUtil;
import cn.hutool.v7.db.meta.*;
import cn.hutool.v7.db.sql.QuoteWrapper;
import java.util.Collection;
import java.util.List;
import java.util.Set;
public class DDLBuilder {
private QuoteWrapper quoteWrapper;
public String buildCreateTableSql(TableMeta tableMeta) {
StringBuilder sqlBuilder = new StringBuilder();
// 表名
final String tableName = tableMeta.getTableName();
Assert.notBlank(tableName, "Table name cannot be blank!");
buildCreateTableStart(sqlBuilder, tableName);
sqlBuilder.append(StrUtil.LF);
// 列定义
Collection<Column> columns = tableMeta.getColumns();
Assert.notEmpty(columns, "Table must have at least one column");
boolean firstColumn = true;
for (Column column : columns) {
if (!firstColumn) {
sqlBuilder.append(",\n");
}
sqlBuilder.append(" ").append(buildColumnDefinition(column));
firstColumn = false;
}
// 主键约束定义
buildPrimaryKey(sqlBuilder, tableMeta.getPkNames());
// 索引定义 (如果有)
List<IndexInfo> indexInfoList = tableMeta.getIndexInfoList();
if (CollUtil.isNotEmpty(indexInfoList)) {
for (IndexInfo indexInfo : indexInfoList) {
sqlBuilder.append(",\n ");
if (!indexInfo.isNonUnique()) {
sqlBuilder.append("UNIQUE ");
}
sqlBuilder.append("INDEX `").append(indexInfo.getIndexName()).append("` (");
// 遍历索引列信息列表
List<ColumnIndex> columnIndexInfoList = indexInfo.getColumnIndexInfoList();
if (columnIndexInfoList != null && !columnIndexInfoList.isEmpty()) {
boolean firstIndexColumn = true;
for (ColumnIndex columnIndex : columnIndexInfoList) {
if (!firstIndexColumn) {
sqlBuilder.append(", ");
}
sqlBuilder.append("`").append(columnIndex.getColumnName()).append("`");
// 可以添加排序信息如果需要的话
String ascOrDesc = columnIndex.getAscOrDesc();
if ("D".equalsIgnoreCase(ascOrDesc)) {
sqlBuilder.append(" DESC");
} else if ("A".equalsIgnoreCase(ascOrDesc)) {
sqlBuilder.append(" ASC");
}
firstIndexColumn = false;
}
}
sqlBuilder.append(")");
}
}
buildCreateTableEnd(sqlBuilder);
// 表注释
String remarks = tableMeta.getRemarks();
if (remarks != null && !remarks.isEmpty()) {
sqlBuilder.append(" COMMENT='").append(remarks.replace("'", "''")).append("'");
}
sqlBuilder.append(";");
return sqlBuilder.toString();
}
/**
* 通用方法建表开头可被子类重写如Oracle可能有特殊语法生成如<br>
* <pre>{@code
* CREATE TABLE <tableName> (
* }</pre>
*
* @param tableName 表名
*/
protected void buildCreateTableStart(StringBuilder sqlBuilder,String tableName) {
if (StrUtil.isBlank(tableName)) {
throw new IllegalArgumentException("Table name cannot be blank!");
}
sqlBuilder.append("CREATE TABLE ").append(wrap(tableName)).append(" (");
}
protected void buildPrimaryKey(StringBuilder sqlBuilder, Set<String> pkNames) {
if (pkNames.isEmpty()) {
return;
}
sqlBuilder.append(",\n PRIMARY KEY (");
boolean firstPk = true;
for (String pkName : pkNames) {
if (!firstPk) {
sqlBuilder.append(", ");
}
sqlBuilder.append(wrap(pkName));
firstPk = false;
}
sqlBuilder.append(")");
}
/**
* 根据Column对象生成列定义
* <p>
* 注意: 这里是一个简化的实现实际项目中需要根据Column类的具体属性
* 和数据库类型映射来完善
*
* @param column Column对象
* @return 列定义字符串
*/
private static String buildColumnDefinition(Column column) {
StringBuilder columnBuilder = new StringBuilder();
// 列名
columnBuilder.append("`").append(column.getName()).append("`");
// 数据类型 (这里假设Column有getTypeName方法)
// 实际应用中需要更复杂的类型映射
ColumnType type = column.getType();
if (type != null) {
columnBuilder.append(" ").append(type.getTypeName());
} else {
// 默认使用VARCHAR
columnBuilder.append(" VARCHAR(255)");
}
// 是否允许为空 (假设Column有isNullable方法)
if (!column.isNullable()) {
columnBuilder.append(" NOT NULL");
}
// 默认值 (假设Column有getDefaultValue方法)
String defaultValue = column.getColumnDef();
if (defaultValue != null) {
columnBuilder.append(" DEFAULT '").append(defaultValue.replace("'", "''")).append("'");
}
// 注释 (假设Column有getRemarks方法)
String remarks = column.getRemarks();
if (remarks != null && !remarks.isEmpty()) {
columnBuilder.append(" COMMENT '").append(remarks.replace("'", "''")).append("'");
}
return columnBuilder.toString();
}
/**
* 通用方法建表结尾可被子类重写如MySQL可能有特殊语法生成如<br>
* <pre>{@code
* )
* }</pre>
*/
protected void buildCreateTableEnd(StringBuilder sqlBuilder) {
sqlBuilder.append("\n)");
}
/**
* 字段名包装
*
* @param field 字段名
* @return 包装后的字段名
*/
private String wrap(String field) {
if (null != quoteWrapper) {
return quoteWrapper.wrap(field);
}
return field;
}
}

View File

@ -130,12 +130,13 @@ public interface DriverNames {
String DRIVER_GBASE = "com.gbase.jdbc.Driver";
/**
* JDBC 驱动 南大通用 GBase 8s<br>
* https://www.gbase.cn/community/post/4029
* <a href="https://www.gbase.cn/community/post/4029">南大通用GBase 8s JDBC驱动URL使用指南</a>
*/
String DRIVER_GBASE8S = "com.gbasedbt.jdbc.Driver";
/**
* JDBC 驱动 南大通用 GBase 8c<br>
* https://www.gbase.cn/download/gbase-8c?category=DRIVER_PACKAGE 页面 GBase8c_JDBC.zip 中的JDBC 使用手册_V1.0_20230818.pdfp14
* <a href="https://www.gbase.cn/download/gbase-8c?category=DRIVER_PACKAGE">gbase-8c?category=DRIVER_PACKAGE</a>
* 页面 GBase8c_JDBC.zip 中的JDBC 使用手册_V1.0_20230818.pdfp14
*/
String DRIVER_GBASE8C = "cn.gbase8c.Driver";
/**
@ -272,22 +273,23 @@ public interface DriverNames {
String DRIVER_HANA = "com.sap.db.jdbc.Driver";
/**
* JDBC 驱动 腾讯 TDSQL PostgreSQL 版本<br>
* https://cloud.tencent.com/document/product/1129/116487
* <a href="https://cloud.tencent.com/document/product/1129/116487">使用 JDBC 连接 TDSQL PG</a>
*/
String DRIVER_TDSQL_POSTGRESQL = "com.tencentcloud.tdsql.pg.jdbc.Driver";
/**
* JDBC 驱动 腾讯 TDSQL-H LibraDB<br>
* https://cloud.tencent.com/document/product/1488/79810
* <a href="https://cloud.tencent.com/document/product/1488/79810">使用 JDBC 连接 TDSQL-H LibraDB</a>
*/
String DRIVER_TDSQL_H_LIBRADB = "ru.yandex.clickhouse.ClickHouseDriver";
/**
* JDBC 驱动 Snowflake<br>
* https://docs.snowflake.cn/zh/developer-guide/jdbc/jdbc-configure#label-jdbc-connection-string
* <a href="https://docs.snowflake.cn/zh/developer-guide/jdbc/jdbc-configure#label-jdbc-connection-string">Snowflake JDBC 驱动程序连接字符串</a>
*/
String DRIVER_SNOWFLAKE = "net.snowflake.client.jdbc.SnowflakeDriver";
/**
* JDBC 驱动 Teradata<br>
* https://teradata-docs.s3.amazonaws.com/doc/connectivity/jdbc/reference/current/frameset.html 页面 JDBC Interfaces A-L 部分
* <a href="https://teradata-docs.s3.amazonaws.com/doc/connectivity/jdbc/reference/current/frameset.html">Teradata JDBC Driver Reference</a>
* 页面 JDBC Interfaces A-L 部分
*/
String DRIVER_TERADATA = "com.teradata.jdbc.TeraDriver";
}

View File

@ -83,12 +83,12 @@ public class Column implements Serializable, Cloneable {
* 创建列对象
*
* @param columnMetaRs 列元信息的ResultSet
* @param table 表信息
* @param tableMeta 表信息
* @return 列对象
* @since 5.4.3
*/
public static Column of(final Table table, final ResultSet columnMetaRs) {
return new Column(table, columnMetaRs);
public static Column of(final TableMeta tableMeta, final ResultSet columnMetaRs) {
return new Column(tableMeta, columnMetaRs);
}
// ----------------------------------------------------- Constructor start
@ -102,13 +102,13 @@ public class Column implements Serializable, Cloneable {
/**
* 构造
*
* @param table 表信息
* @param tableMeta 表信息
* @param columnMetaRs Meta信息的ResultSet
* @since 5.4.3
*/
public Column(final Table table, final ResultSet columnMetaRs) {
public Column(final TableMeta tableMeta, final ResultSet columnMetaRs) {
try {
init(table, columnMetaRs);
init(tableMeta, columnMetaRs);
} catch (final SQLException e) {
throw new DbException(e, "Get table [{}] meta info error!", tableName);
}
@ -118,15 +118,15 @@ public class Column implements Serializable, Cloneable {
/**
* 初始化
*
* @param table 表信息
* @param tableMeta 表信息
* @param columnMetaRs 列的meta ResultSet
* @throws SQLException SQL执行异常
*/
public void init(final Table table, final ResultSet columnMetaRs) throws SQLException {
this.tableName = table.getTableName();
public void init(final TableMeta tableMeta, final ResultSet columnMetaRs) throws SQLException {
this.tableName = tableMeta.getTableName();
this.name = columnMetaRs.getString("COLUMN_NAME");
this.isPk = table.isPk(this.name);
this.isPk = tableMeta.isPk(this.name);
final int type = columnMetaRs.getInt("DATA_TYPE");
String typeName = columnMetaRs.getString("TYPE_NAME");

View File

@ -26,7 +26,7 @@ import cn.hutool.v7.core.text.StrUtil;
*/
public class ColumnType {
/**
* 类型对应java.sql.Types中的类型
* 类型对应{@link java.sql.Types}中的类型
*/
private int type;
/**
@ -37,7 +37,7 @@ public class ColumnType {
/**
* 构造
*
* @param type 类型对应java.sql.Types中的类型
* @param type 类型对应{@link java.sql.Types}中的类型
* @param typeName 类型名称
* @param size 大小或数据长度
*/

View File

@ -272,20 +272,20 @@ public class DatabaseMetaDataWrapper extends SimpleWrapper<DatabaseMetaData> {
/**
* 从数据库元数据中获取指定表的列信息
*
* @param table 表对象用于存储获取到的列信息
* @param tableMeta 表对象用于存储获取到的列信息
*/
public void fetchColumns(final Table table) {
public void fetchColumns(final TableMeta tableMeta) {
final String catalog = this.catalog;
final String schema = this.schema;
// issue#I9BANE Oracle中特殊表名需要解包
final String tableName = getPureTableName(ObjUtil.defaultIfNull(table.getPureTableName(), table::getTableName));
final String tableName = getPureTableName(ObjUtil.defaultIfNull(tableMeta.getPureTableName(), tableMeta::getTableName));
// 获得列
try (final ResultSet rs = this.raw.getColumns(catalog, schema, tableName, null)) {
if (null != rs) {
while (rs.next()) {
table.addColumn(Column.of(table, rs));
tableMeta.addColumn(Column.of(tableMeta, rs));
}
}
} catch (final SQLException e) {

View File

@ -210,7 +210,7 @@ public class MetaUtil {
* @param tableName 表名
* @return Table对象
*/
public static Table getTableMeta(final DataSource ds, final String tableName) {
public static TableMeta getTableMeta(final DataSource ds, final String tableName) {
return getTableMeta(ds, null, null, tableName);
}
@ -229,7 +229,7 @@ public class MetaUtil {
* @return Table对象
* @since 5.7.22
*/
public static Table getTableMeta(final DataSource ds, final String catalog, final String schema, final String tableName) {
public static TableMeta getTableMeta(final DataSource ds, final String catalog, final String schema, final String tableName) {
Connection conn = null;
try {
conn = ds.getConnection();
@ -256,37 +256,37 @@ public class MetaUtil {
* @return Table对象
* @since 5.8.28
*/
public static Table getTableMeta(final Connection conn, String catalog, String schema, final String tableName) {
final Table table = Table.of(tableName);
public static TableMeta getTableMeta(final Connection conn, String catalog, String schema, final String tableName) {
final TableMeta tableMeta = TableMeta.of(tableName);
// catalog和schema获取失败默认使用null代替
if (null == catalog) {
catalog = getCatalog(conn);
}
table.setCatalog(catalog);
tableMeta.setCatalog(catalog);
if (null == schema) {
schema = getSchema(conn);
}
table.setSchema(schema);
tableMeta.setSchema(schema);
final DatabaseMetaData metaData = getMetaData(conn);
final DatabaseMetaDataWrapper metaDataWrapper = DatabaseMetaDataWrapper.of(metaData, catalog, schema);
// 获取原始表名
final String pureTableName = metaDataWrapper.getPureTableName(tableName);
table.setPureTableName(pureTableName);
tableMeta.setPureTableName(pureTableName);
// 获得表元数据表注释
table.setRemarks(metaDataWrapper.getRemarks(pureTableName));
tableMeta.setRemarks(metaDataWrapper.getRemarks(pureTableName));
// 获得主键
table.setPkNames(metaDataWrapper.getPrimaryKeys(pureTableName));
tableMeta.setPkNames(metaDataWrapper.getPrimaryKeys(pureTableName));
// 获得列
metaDataWrapper.fetchColumns(table);
metaDataWrapper.fetchColumns(tableMeta);
// 获得索引信息(since 5.7.23)
final Map<String, IndexInfo> indexInfoMap = metaDataWrapper.getIndexInfo(tableName);
table.setIndexInfoList(ListUtil.of(indexInfoMap.values()));
tableMeta.setIndexInfoList(ListUtil.of(indexInfoMap.values()));
return table;
return tableMeta;
}
/**

View File

@ -30,7 +30,7 @@ import java.util.Set;
*
* @author loolly
*/
public class Table implements Serializable, Cloneable {
public class TableMeta implements Serializable, Cloneable {
@Serial
private static final long serialVersionUID = -810699625961392983L;
@ -73,8 +73,8 @@ public class Table implements Serializable, Cloneable {
* @param tableName 表的名称用于标识数据库中的特定表
* @return 返回一个新的Table实例其名称为传入的表名
*/
public static Table of(final String tableName) {
return new Table(tableName);
public static TableMeta of(final String tableName) {
return new TableMeta(tableName);
}
// ----------------------------------------------------- Constructor start
@ -84,7 +84,7 @@ public class Table implements Serializable, Cloneable {
*
* @param tableName 表名
*/
public Table(final String tableName) {
public TableMeta(final String tableName) {
this.setTableName(tableName);
}
// ----------------------------------------------------- Constructor end
@ -108,7 +108,7 @@ public class Table implements Serializable, Cloneable {
* @return this
* @since 5.4.3
*/
public Table setSchema(final String schema) {
public TableMeta setSchema(final String schema) {
this.schema = schema;
return this;
}
@ -130,7 +130,7 @@ public class Table implements Serializable, Cloneable {
* @return this
* @since 5.4.3
*/
public Table setCatalog(final String catalog) {
public TableMeta setCatalog(final String catalog) {
this.catalog = catalog;
return this;
}
@ -186,7 +186,7 @@ public class Table implements Serializable, Cloneable {
* @param remarks 注释
* @return this
*/
public Table setRemarks(final String remarks) {
public TableMeta setRemarks(final String remarks) {
this.remarks = remarks;
return this;
}
@ -227,7 +227,7 @@ public class Table implements Serializable, Cloneable {
* @param column 列对象
* @return 自己
*/
public Table addColumn(final Column column) {
public TableMeta addColumn(final Column column) {
this.columns.put(column.getName(), column);
return this;
}
@ -259,7 +259,7 @@ public class Table implements Serializable, Cloneable {
* @param pkColumnName 主键的列名
* @return 自己
*/
public Table addPk(final String pkColumnName) {
public TableMeta addPk(final String pkColumnName) {
this.pkNames.add(pkColumnName);
return this;
}
@ -285,7 +285,7 @@ public class Table implements Serializable, Cloneable {
}
@Override
public Table clone() throws CloneNotSupportedException {
return (Table) super.clone();
public TableMeta clone() throws CloneNotSupportedException {
return (TableMeta) super.clone();
}
}

View File

@ -17,14 +17,14 @@ public class MysqlTableGenerator {
/**
* 根据Table对象生成MySQL的CREATE TABLE语句
*
* @param table Table对象
* @param tableMeta Table对象
* @return MySQL的CREATE TABLE语句
*/
public static String generateCreateTableSql(Table table) {
public static String generateCreateTableSql(TableMeta tableMeta) {
StringBuilder sqlBuilder = new StringBuilder();
// 表名
final String tableName = table.getTableName();
final String tableName = tableMeta.getTableName();
if (StrUtil.isBlank(tableName)) {
throw new IllegalArgumentException("Table name cannot be blank!");
}
@ -32,7 +32,7 @@ public class MysqlTableGenerator {
sqlBuilder.append("CREATE TABLE `").append(tableName).append("` (\n");
// 列定义
Collection<Column> columns = table.getColumns();
Collection<Column> columns = tableMeta.getColumns();
if (columns.isEmpty()) {
throw new IllegalArgumentException("Table must have at least one column");
}
@ -47,7 +47,7 @@ public class MysqlTableGenerator {
}
// 主键定义
Set<String> pkNames = table.getPkNames();
Set<String> pkNames = tableMeta.getPkNames();
if (!pkNames.isEmpty()) {
sqlBuilder.append(",\n PRIMARY KEY (");
boolean firstPk = true;
@ -62,7 +62,7 @@ public class MysqlTableGenerator {
}
// 索引定义 (如果有)
List<IndexInfo> indexInfoList = table.getIndexInfoList();
List<IndexInfo> indexInfoList = tableMeta.getIndexInfoList();
if (indexInfoList != null && !indexInfoList.isEmpty()) {
for (IndexInfo indexInfo : indexInfoList) {
sqlBuilder.append(",\n ");
@ -97,7 +97,7 @@ public class MysqlTableGenerator {
sqlBuilder.append("\n)");
// 表注释
String remarks = table.getRemarks();
String remarks = tableMeta.getRemarks();
if (remarks != null && !remarks.isEmpty()) {
sqlBuilder.append(" COMMENT='").append(remarks.replace("'", "''")).append("'");
}

View File

@ -20,7 +20,7 @@ import cn.hutool.v7.core.lang.Console;
import cn.hutool.v7.db.ds.DSUtil;
import cn.hutool.v7.db.ds.DSWrapper;
import cn.hutool.v7.db.meta.MetaUtil;
import cn.hutool.v7.db.meta.Table;
import cn.hutool.v7.db.meta.TableMeta;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -29,7 +29,7 @@ public class IssueI9BANETest {
@Disabled
void metaTest() {
final DSWrapper ds = DSUtil.getDS("orcl");
final Table tableMeta = MetaUtil.getTableMeta(ds, null, null, "\"1234\"");
final TableMeta tableMeta = MetaUtil.getTableMeta(ds, null, null, "\"1234\"");
Console.log("remarks: " + tableMeta.getRemarks());
Console.log("pks: " + tableMeta.getPkNames());
Console.log("columns: " + tableMeta.getColumns());

View File

@ -43,8 +43,8 @@ public class MetaUtilTest {
@Test
public void getTableMetaTest() {
final Table table = MetaUtil.getTableMeta(ds, "user");
Assertions.assertEquals(SetUtil.of("id"), table.getPkNames());
final TableMeta tableMeta = MetaUtil.getTableMeta(ds, "user");
Assertions.assertEquals(SetUtil.of("id"), tableMeta.getPkNames());
}
@Test
@ -55,8 +55,8 @@ public class MetaUtilTest {
@Test
public void getTableIndexInfoTest() {
final Table table = MetaUtil.getTableMeta(ds, "user_1");
Assertions.assertEquals(2, table.getIndexInfoList().size());
final TableMeta tableMeta = MetaUtil.getTableMeta(ds, "user_1");
Assertions.assertEquals(2, tableMeta.getIndexInfoList().size());
}
/**
@ -64,8 +64,8 @@ public class MetaUtilTest {
*/
@Test
public void getTableColumnTest() {
final Table table = MetaUtil.getTableMeta(ds, "user");
System.out.println(table.getColumns());
Assertions.assertEquals(SetUtil.of("id"), table.getPkNames());
final TableMeta tableMeta = MetaUtil.getTableMeta(ds, "user");
System.out.println(tableMeta.getColumns());
Assertions.assertEquals(SetUtil.of("id"), tableMeta.getPkNames());
}
}

View File

@ -11,7 +11,7 @@ public class MysqlTableGeneratorTest {
@Test
public void testGenerateCreateTableSql_WithIndexes() {
Table table = new Table("products");
TableMeta tableMeta = new TableMeta("products");
// 列定义
Column idCol = new Column().setName("id").setType(new ColumnType(-5, "BIGINT", 20));
@ -27,10 +27,10 @@ public class MysqlTableGeneratorTest {
ColumnIndex priceIdxCol = new ColumnIndex("price", "D");
normalIndex.setColumnIndexInfoList(Collections.singletonList(priceIdxCol));
table.addColumn(idCol).addColumn(nameCol).addColumn(priceCol);
table.setIndexInfoList(Arrays.asList(uniqueIndex, normalIndex));
tableMeta.addColumn(idCol).addColumn(nameCol).addColumn(priceCol);
tableMeta.setIndexInfoList(Arrays.asList(uniqueIndex, normalIndex));
String sql = MysqlTableGenerator.generateCreateTableSql(table);
String sql = MysqlTableGenerator.generateCreateTableSql(tableMeta);
Assertions.assertEquals("""
CREATE TABLE `products` (
`id` BIGINT NOT NULL,