diff --git a/vertx-demo/src/test/kotlin/app/GenerateMigration.kt b/vertx-demo/src/test/kotlin/app/GenerateMigration.kt
index 71a7ba6..9734c4b 100644
--- a/vertx-demo/src/test/kotlin/app/GenerateMigration.kt
+++ b/vertx-demo/src/test/kotlin/app/GenerateMigration.kt
@@ -3,6 +3,8 @@ package app
import org.aikrai.vertx.db.annotation.TableField
import org.aikrai.vertx.db.annotation.TableId
import org.aikrai.vertx.db.annotation.TableName
+import org.aikrai.vertx.db.annotation.TableIndex
+import org.aikrai.vertx.db.annotation.EnumValue
import org.aikrai.vertx.db.migration.AnnotationMapping
import org.aikrai.vertx.db.migration.ColumnMapping
import org.aikrai.vertx.db.migration.DbMigration
@@ -38,6 +40,7 @@ object GenerateMigration {
val dbMigration = DbMigration.create()
dbMigration.setEntityPackage("app.data.domain") // 指定实体类包路径
dbMigration.setSqlAnnotationMapper(mapper)
+ dbMigration.setGenerateDropStatements(false) // 不生成删除语句
// 生成迁移
val migrationVersion = dbMigration.generateMigration()
@@ -55,6 +58,12 @@ object GenerateMigration {
private fun createSqlAnnotationMapper(): SqlAnnotationMapper {
val mapper = SqlAnnotationMapper()
+ // 设置实体类注解映射
+ mapper.entityMapping = AnnotationMapping(
+ annotationClass = TableName::class,
+ propertyName = "value"
+ )
+
// 设置表名映射
mapper.tableName = AnnotationMapping(
annotationClass = TableName::class,
@@ -79,6 +88,14 @@ object GenerateMigration {
defaultValueMapping = AnnotationMapping(
annotationClass = TableField::class,
propertyName = "default"
+ ),
+ lengthMapping = AnnotationMapping(
+ annotationClass = TableField::class,
+ propertyName = "length"
+ ),
+ uniqueMapping = AnnotationMapping(
+ annotationClass = TableField::class,
+ propertyName = "unique"
)
)
)
@@ -88,6 +105,18 @@ object GenerateMigration {
annotationClass = TableId::class,
propertyName = "value"
)
+
+ // 设置索引映射
+ mapper.indexMapping = AnnotationMapping(
+ annotationClass = TableIndex::class,
+ propertyName = "name"
+ )
+
+ // 设置枚举值映射
+ mapper.enumValueMapping = AnnotationMapping(
+ annotationClass = EnumValue::class,
+ propertyName = "value"
+ )
return mapper
}
diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DbMigration.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DbMigration.kt
index dbc86b6..0228419 100644
--- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DbMigration.kt
+++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DbMigration.kt
@@ -1,8 +1,6 @@
package org.aikrai.vertx.db.migration
import java.io.File
-import java.io.IOException
-import kotlin.reflect.KClass
/**
* 生成PostgreSQL DDL迁移脚本,基于实体类及其注解的变更。
@@ -88,6 +86,15 @@ interface DbMigration {
* 设置是否输出日志到控制台(默认为true)
*/
fun setLogToSystemOut(logToSystemOut: Boolean)
+
+ /**
+ * 设置是否生成删除语句(默认为false)
+ *
+ * 如果设置为false,生成的SQL中将不包含任何DROP语句。
+ * 但.model.xml文件中仍会记录删除表和字段的信息。
+ *
+ */
+ fun setGenerateDropStatements(generateDropStatements: Boolean)
/**
* 生成下一次迁移SQL脚本和相关模型XML
diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DefaultDbMigration.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DefaultDbMigration.kt
index 589ca9a..c5fb0ea 100644
--- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DefaultDbMigration.kt
+++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DefaultDbMigration.kt
@@ -16,6 +16,7 @@ class DefaultDbMigration : DbMigration {
private var version: String? = null
private var name: String? = null
private var logToSystemOut: Boolean = true
+ private var generateDropStatements: Boolean = false
override fun setEntityPackage(packagePath: String) {
this.entityPackage = packagePath
@@ -52,6 +53,10 @@ class DefaultDbMigration : DbMigration {
override fun setLogToSystemOut(logToSystemOut: Boolean) {
this.logToSystemOut = logToSystemOut
}
+
+ override fun setGenerateDropStatements(generateDropStatements: Boolean) {
+ this.generateDropStatements = generateDropStatements
+ }
override fun generateMigration(): String? {
validateConfiguration()
@@ -66,6 +71,9 @@ class DefaultDbMigration : DbMigration {
System.setProperty("ddl.migration.name", name!!)
}
+ // 设置是否生成删除语句
+ System.setProperty("ddl.migration.generateDropStatements", generateDropStatements.toString())
+
try {
SqlMigrationGenerator.generateMigrations(entityPackage, sqlAnnotationMapper!!)
return version
@@ -83,6 +91,7 @@ class DefaultDbMigration : DbMigration {
if (name != null) {
System.clearProperty("ddl.migration.name")
}
+ System.clearProperty("ddl.migration.generateDropStatements")
}
}
@@ -99,6 +108,9 @@ class DefaultDbMigration : DbMigration {
System.setProperty("ddl.migration.name", name!!)
}
+ // 设置是否生成删除语句
+ System.setProperty("ddl.migration.generateDropStatements", generateDropStatements.toString())
+
try {
// 修改目录结构,强制生成初始迁移
val modelDir = File("${pathToResources}/${migrationPath}/${modelPath}")
@@ -128,6 +140,7 @@ class DefaultDbMigration : DbMigration {
if (name != null) {
System.clearProperty("ddl.migration.name")
}
+ System.clearProperty("ddl.migration.generateDropStatements")
}
}
diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapper.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapper.kt
index 0f71225..56a8573 100644
--- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapper.kt
+++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapper.kt
@@ -1,6 +1,5 @@
package org.aikrai.vertx.db.migration
-import java.time.LocalDateTime
import kotlin.reflect.KClass
/**
@@ -8,6 +7,12 @@ import kotlin.reflect.KClass
* 用于记录从哪些注解获取SQL生成所需的信息
*/
class SqlAnnotationMapper {
+ /**
+ * 实体类注解映射
+ * 用于标识哪个类是实体类
+ */
+ var entityMapping: AnnotationMapping? = null
+
/**
* 表名映射信息
*/
@@ -23,6 +28,16 @@ class SqlAnnotationMapper {
*/
var primaryKeyMapping: AnnotationMapping? = null
+ /**
+ * 索引映射信息
+ */
+ var indexMapping: AnnotationMapping? = null
+
+ /**
+ * 枚举值映射信息
+ */
+ var enumValueMapping: AnnotationMapping? = null
+
/**
* 其他自定义映射
*/
@@ -65,384 +80,9 @@ data class ColumnMapping(
/** 是否可为空映射,可选 */
val nullableMapping: AnnotationMapping? = null,
/** 默认值映射,可选 */
- val defaultValueMapping: AnnotationMapping? = null
-)
-
-/**
- * SQL注解映射生成器
- * 用于生成和使用SQL注解映射中间类
- */
-class SqlAnnotationMapperGenerator {
-
- companion object {
- /**
- * 从实体类获取SQL信息
- * @param entityClass 实体类
- * @param mapper 注解映射中间类
- * @return SQL信息
- */
- fun extractSqlInfo(entityClass: KClass<*>, mapper: SqlAnnotationMapper): SqlInfo {
- val sqlInfo = SqlInfo()
-
- // 获取表名
- if (mapper.tableName == null) {
- throw IllegalArgumentException("表名映射未设置,请检查SqlAnnotationMapper中的tableName配置")
- }
-
- try {
- val tableNameMapping = mapper.tableName!!
- val annotation = entityClass.annotations.find {
- it.annotationClass.qualifiedName == tableNameMapping.annotationClass.qualifiedName
- }
-
- if (annotation == null) {
- throw IllegalArgumentException("实体类 ${entityClass.simpleName} 未标记 @${tableNameMapping.annotationClass.simpleName} 注解")
- }
-
- try {
- val method = annotation.javaClass.getMethod(tableNameMapping.propertyName)
- sqlInfo.tableName = method.invoke(annotation) as String
-
- if (sqlInfo.tableName.isEmpty()) {
- throw IllegalArgumentException("实体类 ${entityClass.simpleName} 的表名为空,请检查 @${tableNameMapping.annotationClass.simpleName} 注解的 ${tableNameMapping.propertyName} 属性")
- }
- } catch (e: Exception) {
- throw IllegalArgumentException("获取 ${entityClass.simpleName} 的表名失败: ${e.message}", e)
- }
- } catch (e: Exception) {
- throw IllegalArgumentException("处理实体类 ${entityClass.simpleName} 的表名时出错: ${e.message}", e)
- }
-
- // 获取实体类的所有字段
- val fields = entityClass.java.declaredFields
- if (fields.isEmpty()) {
- throw IllegalArgumentException("实体类 ${entityClass.simpleName} 没有声明任何字段")
- }
-
- // 创建已处理字段名集合,用于记录已经处理过的字段
- val processedFields = mutableSetOf()
-
- // 查找实体类中的@Transient注解类
- val transientAnnotationClasses = listOf(
- "kotlin.jvm.Transient",
- "javax.persistence.Transient",
- "jakarta.persistence.Transient",
- "java.beans.Transient",
- "org.aikrai.vertx.db.annotation.Transient"
- )
-
- fields.forEach { field ->
- try {
- // 检查字段是否有@Transient注解,如果有则跳过
- val isTransient = field.annotations.any { annotation ->
- transientAnnotationClasses.any { className ->
- annotation.annotationClass.qualifiedName == className
- }
- }
-
- if (isTransient) {
- return@forEach
- }
-
- // 获取列信息
- val columnInfo = ColumnInfo()
- var foundColumnMapping = false
-
- // 处理每个列映射 - 这部分处理带有特定注解的字段
- mapper.columnMappings.forEach { columnMapping ->
- val nameAnnotation = field.annotations.find {
- it.annotationClass.qualifiedName == columnMapping.nameMapping.annotationClass.qualifiedName
- }
-
- if (nameAnnotation != null) {
- foundColumnMapping = true
- try {
- val nameMethod = nameAnnotation.javaClass.getMethod(columnMapping.nameMapping.propertyName)
- val columnName = nameMethod.invoke(nameAnnotation) as? String
- columnInfo.name = if (columnName.isNullOrEmpty()) toSnakeCase(field.name) else columnName
-
- // 处理类型映射
- columnMapping.typeMapping?.let { typeMapping ->
- val typeAnnotation = field.annotations.find {
- it.annotationClass.qualifiedName == typeMapping.annotationClass.qualifiedName
- }
- if (typeAnnotation != null) {
- try {
- val typeMethod = typeAnnotation.javaClass.getMethod(typeMapping.propertyName)
- val typeName = typeMethod.invoke(typeAnnotation) as String
- columnInfo.type = if (typeName.isEmpty()) {
- inferSqlType(field.type, columnInfo)
- } else {
- typeName
- }
- } catch (e: Exception) {
- throw IllegalArgumentException("处理字段 ${field.name} 的类型映射时出错: ${e.message}", e)
- }
- } else {
- columnInfo.type = inferSqlType(field.type, columnInfo)
- }
- }
-
- // 处理可空映射
- columnMapping.nullableMapping?.let { nullableMapping ->
- val nullableAnnotation = field.annotations.find {
- it.annotationClass.qualifiedName == nullableMapping.annotationClass.qualifiedName
- }
- if (nullableAnnotation != null) {
- try {
- val nullableMethod = nullableAnnotation.javaClass.getMethod(nullableMapping.propertyName)
- columnInfo.nullable = nullableMethod.invoke(nullableAnnotation) as Boolean
- } catch (e: Exception) {
- throw IllegalArgumentException("处理字段 ${field.name} 的可空性映射时出错: ${e.message}", e)
- }
- }
- }
-
- // 处理默认值映射
- columnMapping.defaultValueMapping?.let { defaultValueMapping ->
- val defaultValueAnnotation = field.annotations.find {
- it.annotationClass.qualifiedName == defaultValueMapping.annotationClass.qualifiedName
- }
- if (defaultValueAnnotation != null) {
- try {
- val defaultValueMethod = defaultValueAnnotation.javaClass.getMethod(defaultValueMapping.propertyName)
- columnInfo.defaultValue = defaultValueMethod.invoke(defaultValueAnnotation) as String
- } catch (e: Exception) {
- throw IllegalArgumentException("处理字段 ${field.name} 的默认值映射时出错: ${e.message}", e)
- }
- }
- }
-
- sqlInfo.columns.add(columnInfo)
- processedFields.add(field.name) // 记录已处理的字段
- } catch (e: Exception) {
- throw IllegalArgumentException("处理字段 ${field.name} 时出错: ${e.message}", e)
- }
- }
- }
-
- // 处理主键
- if (mapper.primaryKeyMapping != null) {
- val pkMapping = mapper.primaryKeyMapping!!
- val pkAnnotation = field.annotations.find {
- it.annotationClass.qualifiedName == pkMapping.annotationClass.qualifiedName
- }
- if (pkAnnotation != null) {
- try {
- val pkMethod = pkAnnotation.javaClass.getMethod(pkMapping.propertyName)
- val isPk = pkMethod.invoke(pkAnnotation)
- if (isPk is Boolean && isPk) {
- // 如果字段还未处理,创建默认列信息
- if (!processedFields.contains(field.name)) {
- columnInfo.name = toSnakeCase(field.name)
- columnInfo.type = inferSqlType(field.type, columnInfo)
- columnInfo.isPrimaryKey = true
- sqlInfo.columns.add(columnInfo)
- sqlInfo.primaryKeys.add(columnInfo.name)
- processedFields.add(field.name)
- } else {
- // 如果已处理,找到对应列并标记为主键
- val column = sqlInfo.columns.find { it.name == toSnakeCase(field.name) || it.name == field.name }
- if (column != null) {
- column.isPrimaryKey = true
- if (!sqlInfo.primaryKeys.contains(column.name)) {
- sqlInfo.primaryKeys.add(column.name)
- }
- }
- }
- }
- } catch (e: Exception) {
- throw IllegalArgumentException("处理字段 ${field.name} 的主键映射时出错: ${e.message}", e)
- }
- }
- }
-
- // 如果字段未被处理,并且不是static或transient,添加默认处理
- if (!processedFields.contains(field.name) &&
- !java.lang.reflect.Modifier.isStatic(field.modifiers) &&
- !java.lang.reflect.Modifier.isTransient(field.modifiers)) {
-
- // 检查字段类型是否可空
- val isNullable = isNullableType(field)
-
- // 创建默认列信息
- val defaultColumnInfo = ColumnInfo(
- name = toSnakeCase(field.name),
- type = "", // 先不设置类型
- nullable = isNullable,
- defaultValue = "",
- isPrimaryKey = false
- )
-
- // 设置类型并处理枚举值
- defaultColumnInfo.type = inferSqlType(field.type, defaultColumnInfo)
-
- sqlInfo.columns.add(defaultColumnInfo)
- processedFields.add(field.name)
- }
- } catch (e: Exception) {
- throw IllegalArgumentException("处理实体类 ${entityClass.simpleName} 的字段 ${field.name} 时出错: ${e.message}", e)
- }
- }
-
- // 验证结果
- if (sqlInfo.tableName.isEmpty()) {
- throw IllegalArgumentException("实体类 ${entityClass.simpleName} 的表名为空,请检查表名注解")
- }
-
- if (sqlInfo.columns.isEmpty()) {
- throw IllegalArgumentException("实体类 ${entityClass.simpleName} 没有可用的列信息,请检查列注解")
- }
-
- return sqlInfo
- }
-
- /**
- * 将驼峰命名转换为蛇形命名
- */
- private fun toSnakeCase(camelCase: String): String {
- return camelCase.replace(Regex("([a-z])([A-Z])"), "$1_$2").toLowerCase()
- }
-
- /**
- * 根据Java类型推断SQL类型
- */
- private fun inferSqlType(javaType: Class<*>, columnInfo: ColumnInfo? = null): String {
- val sqlType = when {
- javaType == String::class.java -> "VARCHAR(255)"
- javaType == Int::class.java || javaType == Integer::class.java -> "INTEGER"
- javaType == Long::class.java || javaType == java.lang.Long::class.java -> "BIGINT"
- javaType == Double::class.java || javaType == java.lang.Double::class.java -> "DOUBLE PRECISION"
- javaType == Float::class.java || javaType == java.lang.Float::class.java -> "REAL"
- javaType == Boolean::class.java || javaType == java.lang.Boolean::class.java -> "BOOLEAN"
- javaType == Char::class.java || javaType == Character::class.java -> "CHAR(1)"
- javaType == java.util.Date::class.java || javaType == java.sql.Date::class.java -> "DATE"
- javaType == java.sql.Timestamp::class.java -> "TIMESTAMPTZ"
- javaType == LocalDateTime::class.java -> "TIMESTAMPTZ"
- javaType == ByteArray::class.java -> "BYTEA"
- javaType.name.contains("Map") || javaType.name.contains("HashMap") -> "JSONB"
- javaType.name.contains("List") || javaType.name.contains("ArrayList") -> "JSONB"
- javaType.name.contains("Set") || javaType.name.contains("HashSet") -> "JSONB"
- javaType.name.endsWith("DTO") || javaType.name.endsWith("Dto") -> "JSONB"
- javaType.name.contains("Json") || javaType.name.contains("JSON") -> "JSONB"
- javaType.isEnum -> {
- // 处理枚举类型,提取枚举值并保存到columnInfo中
- if (columnInfo != null) {
- try {
- // 获取枚举类中的所有枚举常量
- val enumConstants = javaType.enumConstants
- if (enumConstants != null && enumConstants.isNotEmpty()) {
- // 提取枚举常量的名称
- val enumValues = enumConstants.map { it.toString() }
- columnInfo.enumValues = enumValues
- }
- } catch (e: Exception) {
- // 忽略枚举值提取失败的情况
- }
- }
- "VARCHAR(50)"
- }
- else -> "VARCHAR(255)"
- }
-
- return sqlType
- }
-
- /**
- * 判断字段类型是否可空
- */
- private fun isNullableType(field: java.lang.reflect.Field): Boolean {
- // 检查字段类型名称中是否包含Nullable标记
- val typeName = field.genericType.typeName
-
- // 1. 先检查字段类型名是否包含"?",这是Kotlin可空类型的标志
- if (typeName.contains("?")) {
- return true
- }
-
- // 2. 检查字段是否为Java原始类型,这些类型不可为空
- if (field.type.isPrimitive) {
- return false
- }
-
- // 3. 通过Java反射获取字段的声明可空性
- try {
- // 检查是否有@Nullable相关注解
- val hasNullableAnnotation = field.annotations.any {
- val name = it.annotationClass.qualifiedName ?: ""
- name.contains("Nullable") || name.contains("nullable")
- }
-
- if (hasNullableAnnotation) {
- return true
- }
- } catch (e: Exception) {
- // 忽略注解检查错误
- }
-
- // 4. 检查字段的类型并判断其可空性
- // Kotlin的String类型不可为空,而Java的String类型可为空
- if (field.type == String::class.java) {
- // 尝试通过字段的初始值判断
- try {
- field.isAccessible = true
- // 如果是非静态字段且初始值为null,则可能为可空类型
- if (!java.lang.reflect.Modifier.isStatic(field.modifiers)) {
- // 对于具有初始值的非静态字段,如果初始值为非null字符串,则认为是非可空类型
- // 如果字段名以OrNull或Optional结尾,认为是可空类型
- if (field.name.endsWith("OrNull") || field.name.endsWith("Optional")) {
- return true
- }
-
- // 对于Kotlin中的非空String类型,如果有初始值"",则不可为空
- // 检查是否为Kotlin类型
- val isKotlinType = typeName.startsWith("kotlin.")
- if (isKotlinType) {
- return false // Kotlin中的String类型不可为空
- }
- }
- } catch (e: Exception) {
- // 忽略访问字段值的错误
- }
- }
-
- // 5. 检查字段是否为其他Kotlin基本类型且非可空
- if (field.type == Int::class.java ||
- field.type == Long::class.java ||
- field.type == Boolean::class.java ||
- field.type == Float::class.java ||
- field.type == Double::class.java ||
- field.type == Char::class.java ||
- field.type == Byte::class.java ||
- field.type == Short::class.java) {
- // Kotlin基本类型不带?就不可为空
- return false
- }
-
- // 6. 默认情况: 引用类型默认认为是可空的
- return true
- }
- }
-}
-
-/**
- * SQL信息类
- * 存储从实体类中提取的SQL相关信息
- */
-data class SqlInfo(
- var tableName: String = "",
- val columns: MutableList = mutableListOf(),
- val primaryKeys: MutableList = mutableListOf()
-)
-
-/**
- * 列信息类
- */
-data class ColumnInfo(
- var name: String = "",
- var type: String = "",
- var nullable: Boolean = true,
- var defaultValue: String = "",
- var isPrimaryKey: Boolean = false,
- var enumValues: List? = null
-)
\ No newline at end of file
+ val defaultValueMapping: AnnotationMapping? = null,
+ /** 字段长度映射,可选 */
+ val lengthMapping: AnnotationMapping? = null,
+ /** 是否唯一映射,可选 */
+ val uniqueMapping: AnnotationMapping? = null
+)
\ No newline at end of file
diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapperGenerator.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapperGenerator.kt
new file mode 100644
index 0000000..e4cd47f
--- /dev/null
+++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapperGenerator.kt
@@ -0,0 +1,516 @@
+package org.aikrai.vertx.db.migration
+
+import java.time.LocalDateTime
+import kotlin.reflect.KClass
+
+/**
+ * SQL注解映射生成器
+ * 用于生成和使用SQL注解映射中间类
+ */
+class SqlAnnotationMapperGenerator {
+
+ companion object {
+ /**
+ * 从实体类获取SQL信息
+ * @param entityClass 实体类
+ * @param mapper 注解映射中间类
+ * @return SQL信息
+ */
+ fun extractSqlInfo(entityClass: KClass<*>, mapper: SqlAnnotationMapper): SqlInfo {
+ val sqlInfo = SqlInfo()
+
+ // 验证实体类注解
+ val isEntityClass = validateEntityClass(entityClass, mapper)
+ if (!isEntityClass) {
+ throw IllegalArgumentException("类 ${entityClass.simpleName} 不是有效的实体类,未标记所需的实体类注解")
+ }
+
+ // 获取表名 - 优先从表名注解中获取,如果没有则使用类名转换
+ try {
+ if (mapper.tableName != null) {
+ val tableNameMapping = mapper.tableName!!
+ val annotation = entityClass.annotations.find {
+ it.annotationClass.qualifiedName == tableNameMapping.annotationClass.qualifiedName
+ }
+
+ if (annotation != null) {
+ try {
+ val method = annotation.javaClass.getMethod(tableNameMapping.propertyName)
+ val tableName = method.invoke(annotation) as String
+
+ if (tableName.isNotEmpty()) {
+ sqlInfo.tableName = tableName
+ } else {
+ // 使用类名转蛇形命名作为表名
+ sqlInfo.tableName = toSnakeCase(entityClass.simpleName ?: "")
+ }
+ } catch (e: Exception) {
+ // 使用类名转蛇形命名作为表名
+ sqlInfo.tableName = toSnakeCase(entityClass.simpleName ?: "")
+ }
+ } else {
+ // 使用类名转蛇形命名作为表名
+ sqlInfo.tableName = toSnakeCase(entityClass.simpleName ?: "")
+ }
+ } else {
+ // 使用类名转蛇形命名作为表名
+ sqlInfo.tableName = toSnakeCase(entityClass.simpleName ?: "")
+ }
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理实体类 ${entityClass.simpleName} 的表名时出错: ${e.message}", e)
+ }
+
+ // 获取实体类的所有字段
+ val fields = entityClass.java.declaredFields
+ if (fields.isEmpty()) {
+ throw IllegalArgumentException("实体类 ${entityClass.simpleName} 没有声明任何字段")
+ }
+
+ // 创建已处理字段名集合,用于记录已经处理过的字段
+ val processedFields = mutableSetOf()
+
+ // 查找实体类中的@Transient注解类
+ val transientAnnotationClasses = listOf(
+ "kotlin.jvm.Transient",
+ "javax.persistence.Transient",
+ "jakarta.persistence.Transient",
+ "java.beans.Transient",
+ "org.aikrai.vertx.db.annotation.Transient"
+ )
+
+ fields.forEach { field ->
+ try {
+ // 检查字段是否有@Transient注解,如果有则跳过
+ val isTransient = field.annotations.any { annotation ->
+ transientAnnotationClasses.any { className ->
+ annotation.annotationClass.qualifiedName == className
+ }
+ }
+
+ if (isTransient) {
+ return@forEach
+ }
+
+ // 获取列信息
+ val columnInfo = ColumnInfo()
+ var foundColumnMapping = false
+
+ // 处理每个列映射 - 这部分处理带有特定注解的字段
+ mapper.columnMappings.forEach { columnMapping ->
+ val nameAnnotation = field.annotations.find {
+ it.annotationClass.qualifiedName == columnMapping.nameMapping.annotationClass.qualifiedName
+ }
+
+ if (nameAnnotation != null) {
+ foundColumnMapping = true
+ try {
+ val nameMethod = nameAnnotation.javaClass.getMethod(columnMapping.nameMapping.propertyName)
+ val columnName = nameMethod.invoke(nameAnnotation) as? String
+ columnInfo.name = if (columnName.isNullOrEmpty()) toSnakeCase(field.name) else columnName
+
+ // 处理类型映射
+ columnMapping.typeMapping?.let { typeMapping ->
+ val typeAnnotation = field.annotations.find {
+ it.annotationClass.qualifiedName == typeMapping.annotationClass.qualifiedName
+ }
+ if (typeAnnotation != null) {
+ try {
+ val typeMethod = typeAnnotation.javaClass.getMethod(typeMapping.propertyName)
+ val typeName = typeMethod.invoke(typeAnnotation) as String
+ columnInfo.type = if (typeName.isEmpty()) {
+ inferSqlType(field.type, columnInfo, mapper)
+ } else {
+ typeName
+ }
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理字段 ${field.name} 的类型映射时出错: ${e.message}", e)
+ }
+ } else {
+ columnInfo.type = inferSqlType(field.type, columnInfo, mapper)
+ }
+ } ?: {
+ columnInfo.type = inferSqlType(field.type, columnInfo, mapper)
+ }
+
+ // 处理可空映射
+ columnMapping.nullableMapping?.let { nullableMapping ->
+ val nullableAnnotation = field.annotations.find {
+ it.annotationClass.qualifiedName == nullableMapping.annotationClass.qualifiedName
+ }
+ if (nullableAnnotation != null) {
+ try {
+ val nullableMethod = nullableAnnotation.javaClass.getMethod(nullableMapping.propertyName)
+ columnInfo.nullable = nullableMethod.invoke(nullableAnnotation) as Boolean
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理字段 ${field.name} 的可空性映射时出错: ${e.message}", e)
+ }
+ }
+ }
+
+ // 处理默认值映射
+ columnMapping.defaultValueMapping?.let { defaultValueMapping ->
+ val defaultValueAnnotation = field.annotations.find {
+ it.annotationClass.qualifiedName == defaultValueMapping.annotationClass.qualifiedName
+ }
+ if (defaultValueAnnotation != null) {
+ try {
+ val defaultValueMethod = defaultValueAnnotation.javaClass.getMethod(defaultValueMapping.propertyName)
+ columnInfo.defaultValue = defaultValueMethod.invoke(defaultValueAnnotation) as String
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理字段 ${field.name} 的默认值映射时出错: ${e.message}", e)
+ }
+ }
+ }
+
+ // 处理长度映射
+ columnMapping.lengthMapping?.let { lengthMapping ->
+ val lengthAnnotation = field.annotations.find {
+ it.annotationClass.qualifiedName == lengthMapping.annotationClass.qualifiedName
+ }
+ if (lengthAnnotation != null) {
+ try {
+ val lengthMethod = lengthAnnotation.javaClass.getMethod(lengthMapping.propertyName)
+ val lengthValue = lengthMethod.invoke(lengthAnnotation)
+ if (lengthValue is Int) {
+ columnInfo.length = lengthValue
+ // 如果是VARCHAR类型,更新类型定义中的长度
+ if (columnInfo.type.startsWith("VARCHAR")) {
+ columnInfo.type = "VARCHAR(${columnInfo.length})"
+ }
+ }
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理字段 ${field.name} 的长度映射时出错: ${e.message}", e)
+ }
+ }
+ }
+
+ // 处理唯一性映射
+ columnMapping.uniqueMapping?.let { uniqueMapping ->
+ val uniqueAnnotation = field.annotations.find {
+ it.annotationClass.qualifiedName == uniqueMapping.annotationClass.qualifiedName
+ }
+ if (uniqueAnnotation != null) {
+ try {
+ val uniqueMethod = uniqueAnnotation.javaClass.getMethod(uniqueMapping.propertyName)
+ val uniqueValue = uniqueMethod.invoke(uniqueAnnotation)
+ if (uniqueValue is Boolean) {
+ columnInfo.unique = uniqueValue
+ }
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理字段 ${field.name} 的唯一性映射时出错: ${e.message}", e)
+ }
+ }
+ }
+
+ sqlInfo.columns.add(columnInfo)
+ processedFields.add(field.name) // 记录已处理的字段
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理字段 ${field.name} 时出错: ${e.message}", e)
+ }
+ }
+ }
+
+ // 处理主键
+ if (mapper.primaryKeyMapping != null) {
+ val pkMapping = mapper.primaryKeyMapping!!
+ val pkAnnotation = field.annotations.find {
+ it.annotationClass.qualifiedName == pkMapping.annotationClass.qualifiedName
+ }
+ if (pkAnnotation != null) {
+ try {
+ val pkMethod = pkAnnotation.javaClass.getMethod(pkMapping.propertyName)
+ val isPk = pkMethod.invoke(pkAnnotation)
+ if (isPk is Boolean && isPk) {
+ // 如果字段还未处理,创建默认列信息
+ if (!processedFields.contains(field.name)) {
+ columnInfo.name = toSnakeCase(field.name)
+ columnInfo.type = inferSqlType(field.type, columnInfo, mapper)
+ columnInfo.isPrimaryKey = true
+ sqlInfo.columns.add(columnInfo)
+ sqlInfo.primaryKeys.add(columnInfo.name)
+ processedFields.add(field.name)
+ } else {
+ // 如果已处理,找到对应列并标记为主键
+ val column = sqlInfo.columns.find { it.name == toSnakeCase(field.name) || it.name == field.name }
+ if (column != null) {
+ column.isPrimaryKey = true
+ if (!sqlInfo.primaryKeys.contains(column.name)) {
+ sqlInfo.primaryKeys.add(column.name)
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理字段 ${field.name} 的主键映射时出错: ${e.message}", e)
+ }
+ }
+ }
+
+ // 如果字段未被处理,并且不是static或transient,添加默认处理
+ if (!processedFields.contains(field.name) &&
+ !java.lang.reflect.Modifier.isStatic(field.modifiers) &&
+ !java.lang.reflect.Modifier.isTransient(field.modifiers)) {
+
+ // 检查字段类型是否可空
+ val isNullable = isNullableType(field)
+
+ // 创建默认列信息
+ val defaultColumnInfo = ColumnInfo(
+ name = toSnakeCase(field.name),
+ type = "", // 先不设置类型
+ nullable = isNullable,
+ defaultValue = "",
+ isPrimaryKey = false
+ )
+
+ // 设置类型并处理枚举值
+ defaultColumnInfo.type = inferSqlType(field.type, defaultColumnInfo, mapper)
+
+ sqlInfo.columns.add(defaultColumnInfo)
+ processedFields.add(field.name)
+ }
+ } catch (e: Exception) {
+ throw IllegalArgumentException("处理实体类 ${entityClass.simpleName} 的字段 ${field.name} 时出错: ${e.message}", e)
+ }
+ }
+
+ // 验证结果
+ if (sqlInfo.tableName.isEmpty()) {
+ throw IllegalArgumentException("实体类 ${entityClass.simpleName} 的表名为空,请检查表名注解")
+ }
+
+ if (sqlInfo.columns.isEmpty()) {
+ throw IllegalArgumentException("实体类 ${entityClass.simpleName} 没有可用的列信息,请检查列注解")
+ }
+
+ // 处理表级别的索引注解
+ processTableIndexes(entityClass, sqlInfo, mapper)
+
+ return sqlInfo
+ }
+
+ /**
+ * 验证类是否为实体类
+ */
+ private fun validateEntityClass(entityClass: KClass<*>, mapper: SqlAnnotationMapper): Boolean {
+ // 如果没有设置实体类映射,则认为所有类都是有效的实体类
+ if (mapper.entityMapping == null) {
+ return true
+ }
+
+ val entityMapping = mapper.entityMapping!!
+ return entityClass.annotations.any {
+ it.annotationClass.qualifiedName == entityMapping.annotationClass.qualifiedName
+ }
+ }
+
+ /**
+ * 处理表级别的索引注解
+ */
+ private fun processTableIndexes(entityClass: KClass<*>, sqlInfo: SqlInfo, mapper: SqlAnnotationMapper) {
+ // 只有当有indexMapping配置时才处理
+ if (mapper.indexMapping == null) {
+ return
+ }
+
+ try {
+ val indexMapping = mapper.indexMapping!!
+ // 查找类上的所有TableIndex注解
+ val tableIndexAnnotations = entityClass.annotations.filter {
+ it.annotationClass.qualifiedName == indexMapping.annotationClass.qualifiedName
+ }
+
+ // 处理每个TableIndex注解
+ tableIndexAnnotations.forEach { annotation ->
+ try {
+ // 提取索引信息
+ val indexInfo = IndexInfo()
+
+ // 获取索引名称
+ val nameMethod = annotation.javaClass.getMethod("name")
+ val indexName = nameMethod.invoke(annotation) as String
+ indexInfo.name = if (indexName.isNotEmpty()) indexName else "idx_${sqlInfo.tableName}_${System.currentTimeMillis()}"
+
+ // 获取唯一性
+ val uniqueMethod = annotation.javaClass.getMethod("unique")
+ indexInfo.unique = uniqueMethod.invoke(annotation) as Boolean
+
+ // 获取并发创建选项
+ val concurrentMethod = annotation.javaClass.getMethod("concurrent")
+ indexInfo.concurrent = concurrentMethod.invoke(annotation) as Boolean
+
+ // 获取列名列表
+ val columnNamesMethod = annotation.javaClass.getMethod("columnNames")
+ val columnNames = columnNamesMethod.invoke(annotation) as Array<*>
+ indexInfo.columnNames = columnNames.map { it.toString() }
+
+ // 获取自定义定义
+ val definitionMethod = annotation.javaClass.getMethod("definition")
+ val definition = definitionMethod.invoke(annotation) as String
+ indexInfo.definition = definition
+
+ // 只有当至少有一个列名或自定义定义时才添加索引
+ if (indexInfo.columnNames.isNotEmpty() || indexInfo.definition.isNotEmpty()) {
+ sqlInfo.indexes.add(indexInfo)
+ }
+ } catch (e: Exception) {
+ // 处理单个索引注解失败时记录错误但继续处理其他索引
+ println("处理索引注解时出错: ${e.message}")
+ }
+ }
+ } catch (e: Exception) {
+ // 处理索引注解整体失败时记录错误
+ println("处理表 ${sqlInfo.tableName} 的索引注解时出错: ${e.message}")
+ }
+ }
+
+ /**
+ * 将驼峰命名转换为蛇形命名
+ */
+ private fun toSnakeCase(camelCase: String): String {
+ return camelCase.replace(Regex("([a-z])([A-Z])"), "$1_$2").toLowerCase()
+ }
+
+ /**
+ * 根据Java类型推断SQL类型
+ */
+ private fun inferSqlType(javaType: Class<*>, columnInfo: ColumnInfo? = null, mapper: SqlAnnotationMapper? = null): String {
+ val sqlType = when {
+ javaType == String::class.java -> {
+ if (columnInfo != null) {
+ "VARCHAR(${columnInfo.length})"
+ } else {
+ "VARCHAR(255)"
+ }
+ }
+ javaType == Int::class.java || javaType == Integer::class.java -> "INTEGER"
+ javaType == Long::class.java || javaType == java.lang.Long::class.java -> "BIGINT"
+ javaType == Double::class.java || javaType == java.lang.Double::class.java -> "DOUBLE PRECISION"
+ javaType == Float::class.java || javaType == java.lang.Float::class.java -> "REAL"
+ javaType == Boolean::class.java || javaType == java.lang.Boolean::class.java -> "BOOLEAN"
+ javaType == Char::class.java || javaType == Character::class.java -> "CHAR(1)"
+ javaType == java.util.Date::class.java || javaType == java.sql.Date::class.java -> "DATE"
+ javaType == java.sql.Timestamp::class.java -> "TIMESTAMPTZ"
+ javaType == LocalDateTime::class.java -> "TIMESTAMPTZ"
+ javaType == ByteArray::class.java -> "BYTEA"
+ javaType.name.contains("Map") || javaType.name.contains("HashMap") -> "JSONB"
+ javaType.name.contains("List") || javaType.name.contains("ArrayList") -> "JSONB"
+ javaType.name.contains("Set") || javaType.name.contains("HashSet") -> "JSONB"
+ javaType.name.endsWith("DTO") || javaType.name.endsWith("Dto") -> "JSONB"
+ javaType.name.contains("Json") || javaType.name.contains("JSON") -> "JSONB"
+ javaType.isEnum -> {
+ // 处理枚举类型,提取枚举值并保存到columnInfo中
+ if (columnInfo != null) {
+ try {
+ // 获取枚举类中的所有枚举常量
+ val enumConstants = javaType.enumConstants
+ if (enumConstants != null && enumConstants.isNotEmpty()) {
+ // 查找带有EnumValue注解的方法
+ val enumValues = if (mapper?.enumValueMapping != null) {
+ val enumValueMethod = javaType.methods.find { method ->
+ method.annotations.any {
+ it.annotationClass.qualifiedName == mapper.enumValueMapping!!.annotationClass.qualifiedName
+ }
+ }
+ if (enumValueMethod != null) {
+ // 使用EnumValue标注的方法获取枚举值
+ enumConstants.map { enumValueMethod.invoke(it).toString() }
+ } else {
+ // 如果没有找到EnumValue注解的方法,使用枚举名称
+ enumConstants.map { (it as Enum<*>).name }
+ }
+ } else {
+ // 默认使用枚举的toString()
+ enumConstants.map { it.toString() }
+ }
+ columnInfo.enumValues = enumValues
+ }
+ } catch (e: Exception) {
+ // 忽略枚举值提取失败的情况
+ println("提取枚举值失败: ${e.message}")
+ }
+ }
+ "VARCHAR(50)"
+ }
+ else -> "VARCHAR(255)"
+ }
+
+ return sqlType
+ }
+
+ /**
+ * 判断字段类型是否可空
+ */
+ private fun isNullableType(field: java.lang.reflect.Field): Boolean {
+ // 检查字段类型名称中是否包含Nullable标记
+ val typeName = field.genericType.typeName
+
+ // 1. 先检查字段类型名是否包含"?",这是Kotlin可空类型的标志
+ if (typeName.contains("?")) {
+ return true
+ }
+
+ // 2. 检查字段是否为Java原始类型,这些类型不可为空
+ if (field.type.isPrimitive) {
+ return false
+ }
+
+ // 3. 通过Java反射获取字段的声明可空性
+ try {
+ // 检查是否有@Nullable相关注解
+ val hasNullableAnnotation = field.annotations.any {
+ val name = it.annotationClass.qualifiedName ?: ""
+ name.contains("Nullable") || name.contains("nullable")
+ }
+
+ if (hasNullableAnnotation) {
+ return true
+ }
+ } catch (e: Exception) {
+ // 忽略注解检查错误
+ }
+
+ // 4. 检查字段的类型并判断其可空性
+ // Kotlin的String类型不可为空,而Java的String类型可为空
+ if (field.type == String::class.java) {
+ // 尝试通过字段的初始值判断
+ try {
+ field.isAccessible = true
+ // 如果是非静态字段且初始值为null,则可能为可空类型
+ if (!java.lang.reflect.Modifier.isStatic(field.modifiers)) {
+ // 对于具有初始值的非静态字段,如果初始值为非null字符串,则认为是非可空类型
+ // 如果字段名以OrNull或Optional结尾,认为是可空类型
+ if (field.name.endsWith("OrNull") || field.name.endsWith("Optional")) {
+ return true
+ }
+
+ // 对于Kotlin中的非空String类型,如果有初始值"",则不可为空
+ // 检查是否为Kotlin类型
+ val isKotlinType = typeName.startsWith("kotlin.")
+ if (isKotlinType) {
+ return false // Kotlin中的String类型不可为空
+ }
+ }
+ } catch (e: Exception) {
+ // 忽略访问字段值的错误
+ }
+ }
+
+ // 5. 检查字段是否为其他Kotlin基本类型且非可空
+ if (field.type == Int::class.java ||
+ field.type == Long::class.java ||
+ field.type == Boolean::class.java ||
+ field.type == Float::class.java ||
+ field.type == Double::class.java ||
+ field.type == Char::class.java ||
+ field.type == Byte::class.java ||
+ field.type == Short::class.java) {
+ // Kotlin基本类型不带?就不可为空
+ return false
+ }
+
+ // 6. 默认情况: 引用类型默认认为是可空的
+ return true
+ }
+ }
+}
\ No newline at end of file
diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlGenerator.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlGenerator.kt
index a7406d1..dbe6151 100644
--- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlGenerator.kt
+++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlGenerator.kt
@@ -55,6 +55,12 @@ class SqlGenerator {
sb.append(columnDefinitions.joinToString(",\n"))
+ // 添加唯一约束
+ val uniqueColumns = columns.filter { it.unique && !it.isPrimaryKey }
+ for (column in uniqueColumns) {
+ sb.append(",\n CONSTRAINT uk_${tableName}_${column.name} UNIQUE (${column.name})")
+ }
+
// 添加枚举约束 - 识别枚举类型的列并添加CHECK约束
val enumColumns = columns.filter { it.type.contains("VARCHAR") && it.enumValues != null && it.enumValues!!.isNotEmpty() }
for (column in enumColumns) {
@@ -71,6 +77,53 @@ class SqlGenerator {
}
sb.append("\n);")
+
+ // 添加索引创建语句
+ val indexSql = generateCreateIndexSql(sqlInfo)
+ if (indexSql.isNotEmpty()) {
+ sb.append("\n\n")
+ sb.append(indexSql)
+ }
+
+ return sb.toString()
+ }
+
+ /**
+ * 生成创建索引的SQL语句
+ * @param sqlInfo SQL信息
+ * @return 创建索引SQL语句
+ */
+ private fun generateCreateIndexSql(sqlInfo: SqlInfo): String {
+ val sb = StringBuilder()
+
+ sqlInfo.indexes.forEach { index ->
+ if (index.columnNames.isNotEmpty() || index.definition.isNotEmpty()) {
+ sb.append("CREATE ")
+
+ if (index.unique) {
+ sb.append("UNIQUE ")
+ }
+
+ sb.append("INDEX ")
+
+ if (index.concurrent) {
+ sb.append("CONCURRENTLY ")
+ }
+
+ sb.append("${index.name} ON ${sqlInfo.tableName}")
+
+ if (index.definition.isNotEmpty()) {
+ // 使用自定义索引定义
+ sb.append(" ${index.definition}")
+ } else {
+ // 使用列名列表
+ sb.append(" (${index.columnNames.joinToString(", ")})")
+ }
+
+ sb.append(";\n")
+ }
+ }
+
return sb.toString()
}
diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlInfo.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlInfo.kt
new file mode 100644
index 0000000..d4923c7
--- /dev/null
+++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlInfo.kt
@@ -0,0 +1,37 @@
+package org.aikrai.vertx.db.migration
+
+/**
+ * SQL信息类
+ * 存储从实体类中提取的SQL相关信息
+ */
+data class SqlInfo(
+ var tableName: String = "",
+ val columns: MutableList = mutableListOf(),
+ val primaryKeys: MutableList = mutableListOf(),
+ val indexes: MutableList = mutableListOf()
+)
+
+/**
+ * 列信息类
+ */
+data class ColumnInfo(
+ var name: String = "",
+ var type: String = "",
+ var nullable: Boolean = true,
+ var defaultValue: String = "",
+ var isPrimaryKey: Boolean = false,
+ var enumValues: List? = null,
+ var unique: Boolean = false,
+ var length: Int = 255
+)
+
+/**
+ * 索引信息类
+ */
+data class IndexInfo(
+ var name: String = "",
+ var columnNames: List = listOf(),
+ var unique: Boolean = false,
+ var concurrent: Boolean = false,
+ var definition: String = ""
+)
\ No newline at end of file