diff --git a/core-database/pom.xml b/core-database/pom.xml
index 507a6a3..cb4c9b7 100644
--- a/core-database/pom.xml
+++ b/core-database/pom.xml
@@ -64,6 +64,12 @@
mysql-connector-j
9.2.0
+
+
+ org.postgresql
+ postgresql
+ 42.7.3
+
diff --git a/core-database/src/main/java/cn/qaiu/db/ddl/Constraint.java b/core-database/src/main/java/cn/qaiu/db/ddl/Constraint.java
index cfcad14..1fcf4c6 100644
--- a/core-database/src/main/java/cn/qaiu/db/ddl/Constraint.java
+++ b/core-database/src/main/java/cn/qaiu/db/ddl/Constraint.java
@@ -21,7 +21,6 @@ public @interface Constraint {
boolean notNull() default false;
/**
- * 唯一键约束 TODO 待实现
* @return 唯一键约束
*/
String uniqueKey() default "";
diff --git a/core-database/src/main/java/cn/qaiu/db/ddl/CreateTable.java b/core-database/src/main/java/cn/qaiu/db/ddl/CreateTable.java
index 3034ea8..2228980 100644
--- a/core-database/src/main/java/cn/qaiu/db/ddl/CreateTable.java
+++ b/core-database/src/main/java/cn/qaiu/db/ddl/CreateTable.java
@@ -8,7 +8,7 @@ import io.vertx.codegen.format.LowerCamelCase;
import io.vertx.codegen.format.SnakeCase;
import io.vertx.core.Future;
import io.vertx.core.Promise;
-import io.vertx.jdbcclient.JDBCPool;
+import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.templates.annotations.Column;
import io.vertx.sqlclient.templates.annotations.RowMapped;
import org.apache.commons.lang3.StringUtils;
@@ -24,164 +24,312 @@ import java.util.*;
* @author QAIU
*/
public class CreateTable {
- public static Map, String> javaProperty2SqlColumnMap = new HashMap<>();
+ public static Map, String> javaProperty2SqlColumnMap = new HashMap<>() {{
+ // Java类型到SQL类型的映射
+ put(Integer.class, "INT");
+ put(Short.class, "SMALLINT");
+ put(Byte.class, "TINYINT");
+ put(Long.class, "BIGINT");
+ put(java.math.BigDecimal.class, "DECIMAL");
+ put(Double.class, "DOUBLE");
+ put(Float.class, "REAL");
+ put(Boolean.class, "BOOLEAN");
+ put(String.class, "VARCHAR");
+ put(Date.class, "TIMESTAMP");
+ put(java.time.LocalDateTime.class, "TIMESTAMP");
+ put(java.sql.Timestamp.class, "TIMESTAMP");
+ put(java.sql.Date.class, "DATE");
+ put(java.sql.Time.class, "TIME");
+
+ // 基本数据类型
+ put(int.class, "INT");
+ put(short.class, "SMALLINT");
+ put(byte.class, "TINYINT");
+ put(long.class, "BIGINT");
+ put(double.class, "DOUBLE");
+ put(float.class, "REAL");
+ put(boolean.class, "BOOLEAN");
+ }};
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTable.class);
-
- static {
- javaProperty2SqlColumnMap.put(Integer.class, "INT");
- javaProperty2SqlColumnMap.put(Short.class, "SMALLINT");
- javaProperty2SqlColumnMap.put(Byte.class, "TINYINT");
- javaProperty2SqlColumnMap.put(Long.class, "BIGINT");
- javaProperty2SqlColumnMap.put(java.math.BigDecimal.class, "DECIMAL");
- javaProperty2SqlColumnMap.put(Double.class, "DOUBLE");
- javaProperty2SqlColumnMap.put(Float.class, "REAL");
- javaProperty2SqlColumnMap.put(Boolean.class, "BOOLEAN");
- javaProperty2SqlColumnMap.put(String.class, "VARCHAR");
- javaProperty2SqlColumnMap.put(java.util.Date.class, "TIMESTAMP");
- javaProperty2SqlColumnMap.put(java.time.LocalDateTime.class, "TIMESTAMP");
- javaProperty2SqlColumnMap.put(java.sql.Timestamp.class, "TIMESTAMP");
- javaProperty2SqlColumnMap.put(java.sql.Date.class, "DATE");
- javaProperty2SqlColumnMap.put(java.sql.Time.class, "TIME");
-
- javaProperty2SqlColumnMap.put(int.class, "INT");
- javaProperty2SqlColumnMap.put(short.class, "SMALLINT");
- javaProperty2SqlColumnMap.put(byte.class, "TINYINT");
- javaProperty2SqlColumnMap.put(long.class, "BIGINT");
- javaProperty2SqlColumnMap.put(double.class, "DOUBLE");
- javaProperty2SqlColumnMap.put(float.class, "REAL");
- javaProperty2SqlColumnMap.put(boolean.class, "BOOLEAN");
- }
+ public static String UNIQUE_PREFIX = "idx_";
private static Case getCase(Class> clz) {
- switch (clz.getName()) {
- case "io.vertx.codegen.format.CamelCase":
- return CamelCase.INSTANCE;
- case "io.vertx.codegen.format.SnakeCase":
- return SnakeCase.INSTANCE;
- case "io.vertx.codegen.format.LowerCamelCase":
- return LowerCamelCase.INSTANCE;
- default:
- throw new UnsupportedOperationException();
+ return switch (clz.getName()) {
+ case "io.vertx.codegen.format.CamelCase" -> CamelCase.INSTANCE;
+ case "io.vertx.codegen.format.SnakeCase" -> SnakeCase.INSTANCE;
+ case "io.vertx.codegen.format.LowerCamelCase" -> LowerCamelCase.INSTANCE;
+ default -> throw new UnsupportedOperationException();
+ };
+ }
+
+ public static List getCreateTableSQL(Class> clz, JDBCType type) {
+ // 获取表名和主键
+ TableInfo tableInfo = extractTableInfo(clz, type);
+
+ // 构建表的SQL语句
+ List sqlList = new ArrayList<>();
+ StringBuilder sb = new StringBuilder(50);
+ sb.append("CREATE TABLE IF NOT EXISTS ")
+ .append(tableInfo.quotationMarks).append(tableInfo.tableName).append(tableInfo.quotationMarks)
+ .append(" ( \r\n ");
+
+ // 处理字段并生成列定义
+ List indexSQLs = new ArrayList<>();
+ processFields(clz, tableInfo, sb, indexSQLs);
+
+ // 去掉最后一个逗号并添加表尾部信息
+ String tableSQL = sb.substring(0, sb.lastIndexOf(",")) + tableInfo.endStr;
+ sqlList.add(tableSQL);
+
+ // 添加索引SQL
+ sqlList.addAll(indexSQLs);
+
+ return sqlList;
+ }
+
+
+ // 修改extractTableInfo方法,处理没有Table注解时默认使用id字段作为主键
+ private static TableInfo extractTableInfo(Class> clz, JDBCType type) {
+ String quotationMarks;
+ String endStr;
+ if (type == JDBCType.MySQL) {
+ quotationMarks = "`";
+ endStr = ")ENGINE=InnoDB DEFAULT CHARSET=utf8;";
+ } else {
+ quotationMarks = "\"";
+ endStr = ");";
+ }
+
+ String primaryKey = null;
+ String tableName = null;
+ Case caseFormat = SnakeCase.INSTANCE;
+
+ // 判断类上是否有RowMapped注解
+ if (clz.isAnnotationPresent(RowMapped.class)) {
+ RowMapped annotation = clz.getAnnotation(RowMapped.class);
+ caseFormat = getCase(annotation.formatter());
+ }
+
+ // 判断类上是否有Table注解
+ if (clz.isAnnotationPresent(Table.class)) {
+ Table annotation = clz.getAnnotation(Table.class);
+ tableName = StringUtils.isNotEmpty(annotation.value())
+ ? annotation.value()
+ : LowerCamelCase.INSTANCE.to(caseFormat, clz.getSimpleName());
+ primaryKey = annotation.keyFields();
+ }
+
+ // 如果表名仍为null,使用类名转下划线命名作为表名
+ if (StringUtils.isEmpty(tableName)) {
+ tableName = LowerCamelCase.INSTANCE.to(SnakeCase.INSTANCE, clz.getSimpleName());
+ }
+
+ // 如果主键为空,默认使用id字段作为主键
+ if (StringUtils.isEmpty(primaryKey)) {
+ try {
+ clz.getDeclaredField("id");
+ primaryKey = "id";
+ } catch (NoSuchFieldException e) {
+ // 如果没有id字段,不设置主键
+ primaryKey = null;
+ }
+ }
+
+ return new TableInfo(tableName, quotationMarks, endStr, primaryKey, caseFormat, type);
+ }
+
+ // 修改processFields方法,处理索引
+ private static void processFields(Class> clz, TableInfo tableInfo, StringBuilder sb, List indexSQLs) {
+ Field[] fields = clz.getDeclaredFields();
+ for (Field field : fields) {
+ // 跳过无效字段
+ if (isIgnoredField(field)) {
+ continue;
+ }
+
+ // 获取字段名和SQL类型
+ String column = LowerCamelCase.INSTANCE.to(tableInfo.caseFormat, field.getName());
+ String sqlType = javaProperty2SqlColumnMap.get(field.getType());
+
+ // 处理字段注解
+ column = processColumnAnnotation(field, column);
+ int[] decimalSize = {22, 2};
+ int varcharSize = 255;
+ if (field.isAnnotationPresent(Length.class)) {
+ Length length = field.getAnnotation(Length.class);
+ decimalSize = length.decimalSize();
+ varcharSize = length.varcharSize();
+ }
+
+ // 构建列定义
+ sb.append(tableInfo.quotationMarks).append(column).append(tableInfo.quotationMarks)
+ .append(" ").append(sqlType);
+ appendTypeLength(sqlType, sb, decimalSize, varcharSize);
+ appendConstraints(field, sb, tableInfo);
+ appendPrimaryKey(tableInfo.primaryKey, column, sb);
+
+ // 添加索引
+ appendIndex(tableInfo, indexSQLs, field);
+
+ sb.append(",\n ");
}
}
- public static String getCreateTableSQL(Class> clz, JDBCType type) {
- String quotationMarks = type == JDBCType.H2DB ? "\"" : "`";
- String endStr = type == JDBCType.H2DB ? ");" : ")ENGINE=InnoDB DEFAULT CHARSET=utf8;";
- // 判断类上是否有次注解
- String primaryKey = null; // 主键
- String tableName = null; // 表名
- Case caseFormat = SnakeCase.INSTANCE;
- if (clz.isAnnotationPresent(RowMapped.class)) {
- RowMapped annotation = clz.getAnnotation(RowMapped.class);
- Class extends Case> formatter = annotation.formatter();
- caseFormat = getCase(formatter);
- }
+ // 判断是否忽略字段
+ private static boolean isIgnoredField(Field field) {
+ return field.getName().equals("serialVersionUID")
+ || StringUtils.isEmpty(javaProperty2SqlColumnMap.get(field.getType()))
+ || field.isAnnotationPresent(TableGenIgnore.class);
+ }
- if (clz.isAnnotationPresent(Table.class)) {
- // 获取类上的注解
- Table annotation = clz.getAnnotation(Table.class);
- // 输出注解上的类名
- String tableNameAnnotation = annotation.value();
- if (StringUtils.isNotEmpty(tableNameAnnotation)) {
- tableName = tableNameAnnotation;
- } else {
- tableName = LowerCamelCase.INSTANCE.to(caseFormat, clz.getSimpleName());
+ // 处理Column注解
+ private static String processColumnAnnotation(Field field, String column) {
+ if (field.isAnnotationPresent(Column.class)) {
+ Column columnAnnotation = field.getAnnotation(Column.class);
+ if (StringUtils.isNotBlank(columnAnnotation.name())) {
+ column = columnAnnotation.name();
}
- primaryKey = annotation.keyFields();
}
- Field[] fields = clz.getDeclaredFields();
- String column;
- int[] decimalSize = {22, 2};
- int varcharSize = 255;
- StringBuilder sb = new StringBuilder(50);
- sb.append("CREATE TABLE IF NOT EXISTS ").append(quotationMarks).append(tableName).append(quotationMarks).append(" ( \r\n ");
- boolean firstId = true;
- for (Field f : fields) {
- Class> paramType = f.getType();
- String sqlType = javaProperty2SqlColumnMap.get(paramType);
- if (f.getName().equals("serialVersionUID") || StringUtils.isEmpty(sqlType) || f.isAnnotationPresent(TableGenIgnore.class)) {
- continue;
+ return column;
+ }
+
+ // 添加类型长度
+ private static void appendTypeLength(String sqlType, StringBuilder sb, int[] decimalSize, int varcharSize) {
+ if ("DECIMAL".equals(sqlType)) {
+ sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
+ } else if ("VARCHAR".equals(sqlType)) {
+ sb.append("(").append(varcharSize).append(")");
+ }
+ }
+
+ // 添加约束
+ private static void appendConstraints(Field field, StringBuilder sb, TableInfo tableInfo) {
+ JDBCType type = tableInfo.dbType;
+
+ if (field.isAnnotationPresent(Constraint.class)) {
+ Constraint constraint = field.getAnnotation(Constraint.class);
+ if (constraint.notNull()) {
+ sb.append(" NOT NULL");
}
- column = LowerCamelCase.INSTANCE.to(caseFormat, f.getName());
- if (f.isAnnotationPresent(Column.class)) {
- Column columnAnnotation = f.getAnnotation(Column.class);
- //输出注解属性
- if (StringUtils.isNotBlank(columnAnnotation.name())) {
- column = columnAnnotation.name();
- }
+ String apostrophe = constraint.defaultValueIsFunction() ? "" : "'";
+ if (StringUtils.isNotEmpty(constraint.defaultValue())) {
+ sb.append(" DEFAULT ").append(apostrophe).append(constraint.defaultValue()).append(apostrophe);
}
- if (f.isAnnotationPresent(Length.class)) {
- Length fieldAnnotation = f.getAnnotation(Length.class);
- decimalSize = fieldAnnotation.decimalSize();
- varcharSize = fieldAnnotation.varcharSize();
- }
- sb.append(quotationMarks).append(column).append(quotationMarks);
- sb.append(" ").append(sqlType);
- // 添加类型长度
- if (sqlType.equals("DECIMAL")) {
- sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
- }
- if (sqlType.equals("VARCHAR")) {
- sb.append("(").append(varcharSize).append(")");
- }
- if (f.isAnnotationPresent(Constraint.class)) {
- Constraint constraintAnnotation = f.getAnnotation(Constraint.class);
- if (constraintAnnotation.notNull()) {
- //非空约束
- sb.append(" NOT NULL");
- }
- String apostrophe = constraintAnnotation.defaultValueIsFunction() ? "" : "'";
- if (StringUtils.isNotEmpty(constraintAnnotation.defaultValue())) {
- //默认值约束
- sb.append(" DEFAULT ").append(apostrophe).append(constraintAnnotation.defaultValue()).append(apostrophe);
- }
- if (constraintAnnotation.autoIncrement() && paramType.equals(Integer.class) || paramType.equals(Long.class)) {
- ////自增
+ if (constraint.autoIncrement()) {
+ if (type == JDBCType.PostgreSQL) {
+ // 需要移除字段类型(最后一个单词)
+ if (field.getType().equals(Integer.class)) {
+ sb.delete(sb.lastIndexOf(" "), sb.length());
+ sb.append(" SERIAL");
+ } else if (field.getType().equals(Long.class)) {
+ sb.delete(sb.lastIndexOf(" "), sb.length());
+ sb.append(" BIGSERIAL");
+ }
+ } else if (field.getType().equals(Integer.class) || field.getType().equals(Long.class)) {
sb.append(" AUTO_INCREMENT");
}
}
- if (StringUtils.isEmpty(primaryKey)) {
- if (firstId) {//类型转换
- sb.append(" PRIMARY KEY");
- firstId = false;
- }
- } else {
- if (primaryKey.equals(column.toLowerCase())) {
- sb.append(" PRIMARY KEY");
+ }
+ }
+
+ // 添加主键
+ private static void appendPrimaryKey(String primaryKey, String column, StringBuilder sb) {
+ if (StringUtils.isEmpty(primaryKey)) {
+ return;
+ }
+ if (primaryKey.equalsIgnoreCase(column)) {
+ sb.append(" PRIMARY KEY");
+ }
+ }
+
+ private static void appendIndex(TableInfo tableInfo, List indexSQLs, Field field) {
+ if (!field.isAnnotationPresent(Constraint.class)) {
+ return;
+ }
+
+ Constraint constraint = field.getAnnotation(Constraint.class);
+ if (StringUtils.isEmpty(constraint.uniqueKey())) {
+ return;
+ }
+
+ String indexName = UNIQUE_PREFIX + tableInfo.tableName + "_" + constraint.uniqueKey();
+ String columnName = field.getName();
+
+ // 检查是否已有相同索引名称的索引
+ Optional existingIndex = indexSQLs.stream()
+ .filter(sql -> sql.contains(tableInfo.quotationMarks + indexName + tableInfo.quotationMarks))
+ .findFirst();
+
+ if (existingIndex.isPresent()) {
+ // 如果存在相同索引名称,追加字段到索引定义中
+ String updatedIndex = existingIndex.get().replaceFirst(
+ "\\(([^)]+)\\)", // 匹配索引字段列表
+ "($1, " + tableInfo.quotationMarks + columnName + tableInfo.quotationMarks + ")"
+ );
+ indexSQLs.remove(existingIndex.get());
+ indexSQLs.add(updatedIndex);
+ } else {
+ // 如果不存在相同索引名称,创建新的索引
+ String indexSQL = String.format(
+ "CREATE UNIQUE INDEX %s %s%s%s ON %s%s%s (%s%s%s);",
+ tableInfo.dbType == JDBCType.MySQL ? "" : "IF NOT EXISTS",
+ tableInfo.quotationMarks, indexName, tableInfo.quotationMarks,
+ tableInfo.quotationMarks, tableInfo.tableName, tableInfo.quotationMarks,
+ tableInfo.quotationMarks, columnName, tableInfo.quotationMarks
+ );
+ indexSQLs.add(indexSQL);
+ }
+ }
+
+ // 表信息类
+ private record TableInfo(
+ String tableName, // 表名
+ String quotationMarks, // 引号或反引号
+ String endStr, // 表尾部信息
+ String primaryKey, // 主键字段
+ Case caseFormat, // 命名格式
+ JDBCType dbType // 数据库类型
+ ) {
+ }
+
+ public static Future createTable(Pool pool, JDBCType type) {
+ Promise promise = Promise.promise();
+ Set> tableClasses = ReflectionUtil.getReflections().getTypesAnnotatedWith(Table.class);
+
+ if (tableClasses.isEmpty()) {
+ LOGGER.warn("Table model class not found");
+ promise.complete();
+ return promise.future();
+ }
+
+ List> futures = new ArrayList<>();
+
+ for (Class> clazz : tableClasses) {
+ List sqlList = getCreateTableSQL(clazz, type);
+ LOGGER.info("Class `{}` auto-generate table", clazz.getName());
+
+ for (String sql : sqlList) {
+ try {
+ pool.query(sql).execute().toCompletionStage().toCompletableFuture().join();
+ futures.add(Future.succeededFuture());
+ LOGGER.debug("Executed SQL:\n{}", sql);
+ } catch (Exception e) {
+ String message = e.getMessage();
+ if (message != null && message.contains("Duplicate key name")) {
+ LOGGER.warn("Ignoring duplicate key error: {}", message);
+ futures.add(Future.succeededFuture());
+ } else {
+ LOGGER.error("SQL Error: {}\nSQL: {}", message, sql);
+ futures.add(Future.failedFuture(e));
+ throw new RuntimeException(e); // Stop execution for other exceptions
+ }
}
}
- sb.append(",\n ");
}
- String sql = sb.toString();
- //去掉最后一个逗号
- int lastIndex = sql.lastIndexOf(",");
- sql = sql.substring(0, lastIndex) + sql.substring(lastIndex + 1);
- return sql.substring(0, sql.length() - 1) + endStr;
- }
-
- public static Future createTable(JDBCPool pool, JDBCType type) {
- Set> tableClassList = ReflectionUtil.getReflections().getTypesAnnotatedWith(Table.class);
- if (tableClassList.isEmpty()) LOGGER.info("Table model class not fount");
- List> futures = new ArrayList<>();
- tableClassList.forEach(clazz -> {
- String createTableSQL = getCreateTableSQL(clazz, type);
- Future