From 4f31d1517f66bbe53bc20fd2692b363533b93bd4 Mon Sep 17 00:00:00 2001 From: AiKrai Date: Thu, 20 Mar 2025 16:06:37 +0800 Subject: [PATCH] 1 --- .../src/test/kotlin/app/GenerateMigration.kt | 29 + .../aikrai/vertx/db/migration/DbMigration.kt | 11 +- .../vertx/db/migration/DefaultDbMigration.kt | 13 + .../vertx/db/migration/SqlAnnotationMapper.kt | 404 +------------- .../migration/SqlAnnotationMapperGenerator.kt | 516 ++++++++++++++++++ .../aikrai/vertx/db/migration/SqlGenerator.kt | 53 ++ .../org/aikrai/vertx/db/migration/SqlInfo.kt | 37 ++ 7 files changed, 679 insertions(+), 384 deletions(-) create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapperGenerator.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlInfo.kt 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