From cc415e82d0453b93b3626ed684fca289c714f295 Mon Sep 17 00:00:00 2001 From: AiKrai Date: Thu, 20 Mar 2025 15:26:08 +0800 Subject: [PATCH] 1 --- .../src/test/kotlin/app/GenerateMigration.kt | 55 +-- .../aikrai/vertx/db/annotation/Annotation.kt | 4 +- .../vertx/db/migration/SqlAnnotationMapper.kt | 391 +++++++++++++++--- .../aikrai/vertx/db/migration/SqlGenerator.kt | 73 +++- .../db/migration/SqlMigrationGenerator.kt | 111 ++++- 5 files changed, 518 insertions(+), 116 deletions(-) diff --git a/vertx-demo/src/test/kotlin/app/GenerateMigration.kt b/vertx-demo/src/test/kotlin/app/GenerateMigration.kt index 289633d..71a7ba6 100644 --- a/vertx-demo/src/test/kotlin/app/GenerateMigration.kt +++ b/vertx-demo/src/test/kotlin/app/GenerateMigration.kt @@ -30,17 +30,22 @@ object GenerateMigration { */ @JvmStatic fun main(args: Array) { - // 创建SQL注解映射器 - val mapper = createSqlAnnotationMapper() + try { + // 创建SQL注解映射器 + val mapper = createSqlAnnotationMapper() - // 设置迁移生成器 - val dbMigration = DbMigration.create() - dbMigration.setEntityPackage("org.aikrai.vertx.entity") // 替换为你的实体类包路径 - dbMigration.setSqlAnnotationMapper(mapper) + // 设置迁移生成器 + val dbMigration = DbMigration.create() + dbMigration.setEntityPackage("app.data.domain") // 指定实体类包路径 + dbMigration.setSqlAnnotationMapper(mapper) - // 生成迁移 - val migrationVersion = dbMigration.generateMigration() - println("生成的迁移版本: $migrationVersion") + // 生成迁移 + val migrationVersion = dbMigration.generateMigration() + println("生成的迁移版本: $migrationVersion") + } catch (e: Exception) { + println("生成迁移失败: ${e.message}") + e.printStackTrace() + } } /** @@ -59,24 +64,24 @@ object GenerateMigration { // 设置列映射 mapper.addColumnMapping( ColumnMapping( - nameMapping = AnnotationMapping( - annotationClass = TableField::class, - propertyName = "name" - ), - typeMapping = AnnotationMapping( - annotationClass = TableField::class, - propertyName = "type" - ), - nullableMapping = AnnotationMapping( - annotationClass = TableField::class, - propertyName = "nullable" - ), - defaultValueMapping = AnnotationMapping( - annotationClass = TableField::class, - propertyName = "defaultValue" + nameMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "value" + ), + typeMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "type" + ), + nullableMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "nullable" + ), + defaultValueMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "default" + ) ) ) - ) // 设置主键映射 mapper.primaryKeyMapping = AnnotationMapping( diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/Annotation.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/Annotation.kt index 9728170..8443692 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/Annotation.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/Annotation.kt @@ -46,7 +46,9 @@ annotation class TableField( // val property: String = "", // val numericScale: String = "" val type: String = "", - val nullable: Boolean = false, + val length: Int = 255, + val nullable: Boolean = true, + val unique: Boolean = false, val default: String = "" ) 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 012f647..0f71225 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,5 +1,6 @@ package org.aikrai.vertx.db.migration +import java.time.LocalDateTime import kotlin.reflect.KClass /** @@ -84,84 +85,343 @@ class SqlAnnotationMapperGenerator { val sqlInfo = SqlInfo() // 获取表名 - mapper.tableName?.let { tableNameMapping -> - val annotation = entityClass.annotations.find { it.annotationClass.toString() == tableNameMapping.annotationClass.toString() } - annotation?.let { - val method = it.javaClass.getMethod(tableNameMapping.propertyName) - sqlInfo.tableName = method.invoke(it) as String + 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) } // 获取实体类的所有字段 - entityClass.java.declaredFields.forEach { field -> - // 获取列信息 - val columnInfo = ColumnInfo() - - // 处理每个列映射 - mapper.columnMappings.forEach { columnMapping -> - val nameAnnotation = field.annotations.find { - it.annotationClass.java == columnMapping.nameMapping.annotationClass.java + 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 (nameAnnotation != null) { - val nameMethod = nameAnnotation.javaClass.getMethod(columnMapping.nameMapping.propertyName) - columnInfo.name = nameMethod.invoke(nameAnnotation) as String? ?: field.name - - // 处理类型映射 - columnMapping.typeMapping?.let { typeMapping -> - val typeAnnotation = field.annotations.find { - it.annotationClass.java == typeMapping.annotationClass.java - } - if (typeAnnotation != null) { - val typeMethod = typeAnnotation.javaClass.getMethod(typeMapping.propertyName) - columnInfo.type = typeMethod.invoke(typeAnnotation) as String - } - } - - // 处理可空映射 - columnMapping.nullableMapping?.let { nullableMapping -> - val nullableAnnotation = field.annotations.find { - it.annotationClass.java == nullableMapping.annotationClass.java - } - if (nullableAnnotation != null) { - val nullableMethod = nullableAnnotation.javaClass.getMethod(nullableMapping.propertyName) - columnInfo.nullable = nullableMethod.invoke(nullableAnnotation) as Boolean - } - } - - // 处理默认值映射 - columnMapping.defaultValueMapping?.let { defaultValueMapping -> - val defaultValueAnnotation = field.annotations.find { - it.annotationClass.java == defaultValueMapping.annotationClass.java - } - if (defaultValueAnnotation != null) { - val defaultValueMethod = defaultValueAnnotation.javaClass.getMethod(defaultValueMapping.propertyName) - columnInfo.defaultValue = defaultValueMethod.invoke(defaultValueAnnotation) as String - } - } - - sqlInfo.columns.add(columnInfo) + if (isTransient) { + return@forEach } - } - - // 处理主键 - mapper.primaryKeyMapping?.let { pkMapping -> - val pkAnnotation = field.annotations.find { - it.annotationClass.java == pkMapping.annotationClass.java - } - if (pkAnnotation != null) { - val pkMethod = pkAnnotation.javaClass.getMethod(pkMapping.propertyName) - val isPk = pkMethod.invoke(pkAnnotation) - if (isPk is Boolean && isPk) { - columnInfo.isPrimaryKey = true - sqlInfo.primaryKeys.add(columnInfo.name) + + // 获取列信息 + 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 + } } } @@ -183,5 +443,6 @@ data class ColumnInfo( var type: String = "", var nullable: Boolean = true, var defaultValue: String = "", - var isPrimaryKey: Boolean = false + var isPrimaryKey: Boolean = false, + var enumValues: List? = null ) \ 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 bed08e5..a7406d1 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 @@ -23,26 +23,87 @@ class SqlGenerator { } val sb = StringBuilder() - sb.append("CREATE TABLE IF NOT EXISTS $tableName (\n") + sb.append("CREATE TABLE $tableName (\n") - // 添加列定义 + // 添加列定义 - 改为ANSI标准格式 val columnDefinitions = columns.map { column -> + // 特殊处理一些常见约定字段 + val specialFieldDefaults = getSpecialFieldDefaults(column.name, column.type) + val defaultValue = if (column.defaultValue.isNotEmpty()) { + // 对于时间戳类型字段,特殊处理NOW()函数作为默认值 + if (column.type.contains("TIMESTAMP") && column.defaultValue.equals("now()", ignoreCase = true)) { + " DEFAULT 'now()'" + } else { + " DEFAULT ${column.defaultValue}" + } + } else if (specialFieldDefaults.isNotEmpty()) { + specialFieldDefaults + } else { + // 为一些常见类型提供合理的默认值 + when { + column.type.contains("VARCHAR") -> " DEFAULT ''" + column.type == "INTEGER" || column.type == "BIGINT" -> " DEFAULT 0" + column.type == "BOOLEAN" -> " DEFAULT false" + column.type.contains("JSON") -> " DEFAULT '{}'" + else -> "" + } + } + val nullable = if (column.nullable) "" else " NOT NULL" - val defaultValue = if (column.defaultValue.isNotEmpty()) " DEFAULT ${column.defaultValue}" else "" - " ${column.name} ${column.type}$nullable$defaultValue" + " ${column.name} ${column.type}$defaultValue$nullable" } sb.append(columnDefinitions.joinToString(",\n")) - // 添加主键 + // 添加枚举约束 - 识别枚举类型的列并添加CHECK约束 + val enumColumns = columns.filter { it.type.contains("VARCHAR") && it.enumValues != null && it.enumValues!!.isNotEmpty() } + for (column in enumColumns) { + if (column.enumValues != null && column.enumValues!!.isNotEmpty()) { + sb.append(",\n CONSTRAINT ck_${tableName}_${column.name} CHECK ( ${column.name} in (") + sb.append(column.enumValues!!.joinToString(",") { "'$it'" }) + sb.append("))") + } + } + + // 添加主键约束 if (sqlInfo.primaryKeys.isNotEmpty()) { - sb.append(",\n PRIMARY KEY (${sqlInfo.primaryKeys.joinToString(", ")})") + sb.append(",\n CONSTRAINT pk_$tableName PRIMARY KEY (${sqlInfo.primaryKeys.joinToString(", ")})") } sb.append("\n);") return sb.toString() } + /** + * 获取特殊字段的默认值定义 + */ + private fun getSpecialFieldDefaults(fieldName: String, fieldType: String): String { + return when { + // 版本字段 + fieldName == "version" && (fieldType == "INTEGER" || fieldType == "BIGINT") -> + " DEFAULT 0" + // 创建时间字段 + fieldName == "created" || fieldName == "create_time" || fieldName == "creation_time" || fieldName == "created_at" -> { + if (fieldType.contains("TIMESTAMP")) " DEFAULT 'now()'" else "" + } + // 更新时间字段 + fieldName == "updated" || fieldName == "update_time" || fieldName == "last_update" || fieldName == "updated_at" -> { + if (fieldType.contains("TIMESTAMP")) " DEFAULT 'now()'" else "" + } + // 是否删除标记 + fieldName == "deleted" || fieldName == "is_deleted" || fieldName == "del_flag" -> { + if (fieldType == "BOOLEAN") " DEFAULT false" + else if (fieldType.contains("INTEGER") || fieldType.contains("CHAR")) " DEFAULT 0" + else "" + } + // 状态字段 + fieldName == "status" || fieldName == "state" -> { + if (fieldType.contains("VARCHAR")) " DEFAULT 'NORMAL'" else "" + } + else -> "" + } + } + /** * 生成插入SQL * @param entityClass 实体类 diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlMigrationGenerator.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlMigrationGenerator.kt index 63a993f..7125a07 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlMigrationGenerator.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlMigrationGenerator.kt @@ -92,21 +92,67 @@ class SqlMigrationGenerator { * @param mapper 注解映射中间类 */ fun generateMigrations(entityPackage: String, mapper: SqlAnnotationMapper) { - val entityClasses = scanEntityClasses(entityPackage) - - // 创建必要的目录 + // 确保日志路径存在 createDirectories() + log("开始扫描实体类...") + val entityClasses = scanEntityClasses(entityPackage) + + if (entityClasses.isEmpty()) { + throw IllegalArgumentException("未找到任何实体类,请检查包路径: $entityPackage") + } + + log("找到 ${entityClasses.size} 个实体类,准备验证...") + + // 先验证所有实体类是否能够正确解析,如果有错误就立刻抛出异常 + validateEntityClasses(entityClasses, mapper) + // 检查是否是初始迁移 val isInitialMigration = isInitialMigration() if (isInitialMigration) { + log("开始生成初始迁移...") generateInitialMigration(entityClasses, mapper) } else { + log("开始生成差异迁移...") generateDiffMigration(entityClasses, mapper) } } - + + /** + * 验证所有实体类是否可以正确解析SQL信息 + */ + private fun validateEntityClasses(entityClasses: List>, mapper: SqlAnnotationMapper) { + val errorMessages = mutableListOf() + + entityClasses.forEach { entityClass -> + try { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + + // 验证表名 + if (sqlInfo.tableName.isEmpty()) { + errorMessages.add("实体类 ${entityClass.simpleName} 的表名为空") + } + + // 验证是否有列 + if (sqlInfo.columns.isEmpty()) { + errorMessages.add("实体类 ${entityClass.simpleName} 没有任何列信息") + } + } catch (e: Exception) { + // 记录错误信息 + errorMessages.add("验证实体类 ${entityClass.simpleName} 失败: ${e.message}") + } + } + + // 如果有任何错误,抛出异常并停止生成 + if (errorMessages.isNotEmpty()) { + val errorMessage = "验证实体类时发现以下错误:\n" + errorMessages.joinToString("\n") + throw IllegalArgumentException(errorMessage) + } + + log("所有实体类验证通过,可以生成迁移") + } + /** * 扫描包路径下标记了@TableName注解的实体类 */ @@ -157,7 +203,12 @@ class SqlMigrationGenerator { // 添加到模型文件 addCreateTableToModel(modelDocument, changeSetElement, sqlInfo) + + log("已生成 ${entityClass.simpleName} 的创建表SQL") } catch (e: Exception) { + // 由于前面已经验证过所有实体类,这里不应该再有错误 + // 但为了健壮性,还是添加错误处理 + log("警告: 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}") sqlBuilder.append("-- 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}\n\n") } } @@ -165,12 +216,14 @@ class SqlMigrationGenerator { // 写入SQL文件 val sqlFileName = "$MIGRATION_PATH/$version.sql" File(sqlFileName).writeText(sqlBuilder.toString()) + log("生成SQL文件: $sqlFileName") // 写入模型文件 - val modelFileName = "$MODEL_PATH/$version.model.xml" + val modelFileName = "$MODEL_PATH/$version$MODEL_SUFFIX" writeModelToFile(modelDocument, modelFileName) + log("生成模型文件: $modelFileName") - println("初始迁移生成完成: $version") + log("初始迁移生成完成: $version") } /** @@ -180,12 +233,12 @@ class SqlMigrationGenerator { // 获取最新的模型文件 val latestModelFile = findLatestModelFile() if (latestModelFile == null) { - println("未找到现有模型文件,将生成初始迁移") + log("未找到现有模型文件,将生成初始迁移") generateInitialMigration(entityClasses, mapper) return } - println("找到最新模型文件: ${latestModelFile.name}") + log("找到最新模型文件: ${latestModelFile.name}") // 解析最新的模型文件并构建完整模型 val allTables = buildFullModel(latestModelFile) @@ -193,16 +246,16 @@ class SqlMigrationGenerator { // 获取当前版本号和生成新版本号 val currentVersion = extractVersionFromFileName(latestModelFile.name) if (!currentVersion.contains(".")) { - println("错误: 提取的当前版本号 '$currentVersion' 格式不正确,将使用默认的 '1.0'") + log("错误: 提取的当前版本号 '$currentVersion' 格式不正确,将使用默认的 '1.0'") val nextVersion = findNextVersionNumber("1.0") - println("基于默认版本找到下一个版本号: $nextVersion") + log("基于默认版本找到下一个版本号: $nextVersion") continueWithMigration(nextVersion, allTables, entityClasses, mapper) return } // 检查是否存在已有的版本号,找出当前最大版本号 val nextVersion = findNextVersionNumber(currentVersion) - println("当前版本: $currentVersion, 新版本: $nextVersion") + log("当前版本: $currentVersion, 新版本: $nextVersion") // 继续生成迁移 continueWithMigration(nextVersion, allTables, entityClasses, mapper) @@ -223,20 +276,40 @@ class SqlMigrationGenerator { try { val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) currentTables[sqlInfo.tableName] = sqlInfo + log("已提取 ${entityClass.simpleName} 的模型信息,表名: ${sqlInfo.tableName}") } catch (e: Exception) { - println("处理实体类 ${entityClass.simpleName} 时出错: ${e.message}") + // 由于前面已验证,这里不应该再有错误 + log("警告: 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}") } } // 比较差异并生成SQL + log("比较数据库结构差异...") val diffResult = compareTables(allTables, currentTables) // 如果没有差异,不生成迁移文件 if (diffResult.isEmpty()) { - println("没有发现数据库结构变化,不生成迁移文件") + log("没有发现数据库结构变化,不生成迁移文件") return } + log("发现以下变更:") + if (diffResult.tablesToCreate.isNotEmpty()) { + log("- 新增表: ${diffResult.tablesToCreate.map { it.tableName }.joinToString(", ")}") + } + if (diffResult.tablesToDrop.isNotEmpty()) { + log("- 删除表: ${diffResult.tablesToDrop.joinToString(", ")}") + } + if (diffResult.columnsToAdd.isNotEmpty()) { + log("- 新增列: ${diffResult.columnsToAdd.entries.joinToString(", ") { "${it.key}(${it.value.size}列)" }}") + } + if (diffResult.columnsToDrop.isNotEmpty()) { + log("- 删除列: ${diffResult.columnsToDrop.entries.joinToString(", ") { "${it.key}(${it.value.size}列)" }}") + } + if (diffResult.columnsToAlter.isNotEmpty()) { + log("- 修改列: ${diffResult.columnsToAlter.entries.joinToString(", ") { "${it.key}(${it.value.size}列)" }}") + } + // 创建新的模型文档,只包含变更 val modelDocument = createModelDocument() val changeSetElement = modelDocument.createElement("changeSet") @@ -265,21 +338,21 @@ class SqlMigrationGenerator { val sqlFileName = "$MIGRATION_PATH/$nextVersion.sql" val sqlFile = File(sqlFileName) if (sqlFile.exists()) { - println("警告: SQL文件 $sqlFileName 已存在,将被覆盖") + log("警告: SQL文件 $sqlFileName 已存在,将被覆盖") } sqlFile.writeText(sqlBuilder.toString()) - println("生成SQL文件: $sqlFileName") + log("生成SQL文件: $sqlFileName") // 写入模型文件 - val modelFileName = "$MODEL_PATH/$nextVersion.model.xml" + val modelFileName = "$MODEL_PATH/$nextVersion$MODEL_SUFFIX" val modelFile = File(modelFileName) if (modelFile.exists()) { - println("警告: 模型文件 $modelFileName 已存在,将被覆盖") + log("警告: 模型文件 $modelFileName 已存在,将被覆盖") } writeModelToFile(modelDocument, modelFileName) - println("生成模型文件: $modelFileName") + log("生成模型文件: $modelFileName") - println("差异迁移生成完成: $nextVersion") + log("差异迁移生成完成: $nextVersion") } /**