1
This commit is contained in:
parent
7677bab363
commit
cc415e82d0
@ -30,17 +30,22 @@ object GenerateMigration {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
try {
|
||||||
// 创建SQL注解映射器
|
// 创建SQL注解映射器
|
||||||
val mapper = createSqlAnnotationMapper()
|
val mapper = createSqlAnnotationMapper()
|
||||||
|
|
||||||
// 设置迁移生成器
|
// 设置迁移生成器
|
||||||
val dbMigration = DbMigration.create()
|
val dbMigration = DbMigration.create()
|
||||||
dbMigration.setEntityPackage("org.aikrai.vertx.entity") // 替换为你的实体类包路径
|
dbMigration.setEntityPackage("app.data.domain") // 指定实体类包路径
|
||||||
dbMigration.setSqlAnnotationMapper(mapper)
|
dbMigration.setSqlAnnotationMapper(mapper)
|
||||||
|
|
||||||
// 生成迁移
|
// 生成迁移
|
||||||
val migrationVersion = dbMigration.generateMigration()
|
val migrationVersion = dbMigration.generateMigration()
|
||||||
println("生成的迁移版本: $migrationVersion")
|
println("生成的迁移版本: $migrationVersion")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("生成迁移失败: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,7 +66,7 @@ object GenerateMigration {
|
|||||||
ColumnMapping(
|
ColumnMapping(
|
||||||
nameMapping = AnnotationMapping(
|
nameMapping = AnnotationMapping(
|
||||||
annotationClass = TableField::class,
|
annotationClass = TableField::class,
|
||||||
propertyName = "name"
|
propertyName = "value"
|
||||||
),
|
),
|
||||||
typeMapping = AnnotationMapping(
|
typeMapping = AnnotationMapping(
|
||||||
annotationClass = TableField::class,
|
annotationClass = TableField::class,
|
||||||
@ -73,7 +78,7 @@ object GenerateMigration {
|
|||||||
),
|
),
|
||||||
defaultValueMapping = AnnotationMapping(
|
defaultValueMapping = AnnotationMapping(
|
||||||
annotationClass = TableField::class,
|
annotationClass = TableField::class,
|
||||||
propertyName = "defaultValue"
|
propertyName = "default"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -46,7 +46,9 @@ annotation class TableField(
|
|||||||
// val property: String = "",
|
// val property: String = "",
|
||||||
// val numericScale: String = ""
|
// val numericScale: String = ""
|
||||||
val type: String = "",
|
val type: String = "",
|
||||||
val nullable: Boolean = false,
|
val length: Int = 255,
|
||||||
|
val nullable: Boolean = true,
|
||||||
|
val unique: Boolean = false,
|
||||||
val default: String = ""
|
val default: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.aikrai.vertx.db.migration
|
package org.aikrai.vertx.db.migration
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,84 +85,343 @@ class SqlAnnotationMapperGenerator {
|
|||||||
val sqlInfo = SqlInfo()
|
val sqlInfo = SqlInfo()
|
||||||
|
|
||||||
// 获取表名
|
// 获取表名
|
||||||
mapper.tableName?.let { tableNameMapping ->
|
if (mapper.tableName == null) {
|
||||||
val annotation = entityClass.annotations.find { it.annotationClass.toString() == tableNameMapping.annotationClass.toString() }
|
throw IllegalArgumentException("表名映射未设置,请检查SqlAnnotationMapper中的tableName配置")
|
||||||
annotation?.let {
|
|
||||||
val method = it.javaClass.getMethod(tableNameMapping.propertyName)
|
|
||||||
sqlInfo.tableName = method.invoke(it) as String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 fields = entityClass.java.declaredFields
|
||||||
|
if (fields.isEmpty()) {
|
||||||
|
throw IllegalArgumentException("实体类 ${entityClass.simpleName} 没有声明任何字段")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建已处理字段名集合,用于记录已经处理过的字段
|
||||||
|
val processedFields = mutableSetOf<String>()
|
||||||
|
|
||||||
|
// 查找实体类中的@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()
|
val columnInfo = ColumnInfo()
|
||||||
|
var foundColumnMapping = false
|
||||||
|
|
||||||
// 处理每个列映射
|
// 处理每个列映射 - 这部分处理带有特定注解的字段
|
||||||
mapper.columnMappings.forEach { columnMapping ->
|
mapper.columnMappings.forEach { columnMapping ->
|
||||||
val nameAnnotation = field.annotations.find {
|
val nameAnnotation = field.annotations.find {
|
||||||
it.annotationClass.java == columnMapping.nameMapping.annotationClass.java
|
it.annotationClass.qualifiedName == columnMapping.nameMapping.annotationClass.qualifiedName
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAnnotation != null) {
|
if (nameAnnotation != null) {
|
||||||
|
foundColumnMapping = true
|
||||||
|
try {
|
||||||
val nameMethod = nameAnnotation.javaClass.getMethod(columnMapping.nameMapping.propertyName)
|
val nameMethod = nameAnnotation.javaClass.getMethod(columnMapping.nameMapping.propertyName)
|
||||||
columnInfo.name = nameMethod.invoke(nameAnnotation) as String? ?: field.name
|
val columnName = nameMethod.invoke(nameAnnotation) as? String
|
||||||
|
columnInfo.name = if (columnName.isNullOrEmpty()) toSnakeCase(field.name) else columnName
|
||||||
|
|
||||||
// 处理类型映射
|
// 处理类型映射
|
||||||
columnMapping.typeMapping?.let { typeMapping ->
|
columnMapping.typeMapping?.let { typeMapping ->
|
||||||
val typeAnnotation = field.annotations.find {
|
val typeAnnotation = field.annotations.find {
|
||||||
it.annotationClass.java == typeMapping.annotationClass.java
|
it.annotationClass.qualifiedName == typeMapping.annotationClass.qualifiedName
|
||||||
}
|
}
|
||||||
if (typeAnnotation != null) {
|
if (typeAnnotation != null) {
|
||||||
|
try {
|
||||||
val typeMethod = typeAnnotation.javaClass.getMethod(typeMapping.propertyName)
|
val typeMethod = typeAnnotation.javaClass.getMethod(typeMapping.propertyName)
|
||||||
columnInfo.type = typeMethod.invoke(typeAnnotation) as String
|
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 ->
|
columnMapping.nullableMapping?.let { nullableMapping ->
|
||||||
val nullableAnnotation = field.annotations.find {
|
val nullableAnnotation = field.annotations.find {
|
||||||
it.annotationClass.java == nullableMapping.annotationClass.java
|
it.annotationClass.qualifiedName == nullableMapping.annotationClass.qualifiedName
|
||||||
}
|
}
|
||||||
if (nullableAnnotation != null) {
|
if (nullableAnnotation != null) {
|
||||||
|
try {
|
||||||
val nullableMethod = nullableAnnotation.javaClass.getMethod(nullableMapping.propertyName)
|
val nullableMethod = nullableAnnotation.javaClass.getMethod(nullableMapping.propertyName)
|
||||||
columnInfo.nullable = nullableMethod.invoke(nullableAnnotation) as Boolean
|
columnInfo.nullable = nullableMethod.invoke(nullableAnnotation) as Boolean
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalArgumentException("处理字段 ${field.name} 的可空性映射时出错: ${e.message}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理默认值映射
|
// 处理默认值映射
|
||||||
columnMapping.defaultValueMapping?.let { defaultValueMapping ->
|
columnMapping.defaultValueMapping?.let { defaultValueMapping ->
|
||||||
val defaultValueAnnotation = field.annotations.find {
|
val defaultValueAnnotation = field.annotations.find {
|
||||||
it.annotationClass.java == defaultValueMapping.annotationClass.java
|
it.annotationClass.qualifiedName == defaultValueMapping.annotationClass.qualifiedName
|
||||||
}
|
}
|
||||||
if (defaultValueAnnotation != null) {
|
if (defaultValueAnnotation != null) {
|
||||||
|
try {
|
||||||
val defaultValueMethod = defaultValueAnnotation.javaClass.getMethod(defaultValueMapping.propertyName)
|
val defaultValueMethod = defaultValueAnnotation.javaClass.getMethod(defaultValueMapping.propertyName)
|
||||||
columnInfo.defaultValue = defaultValueMethod.invoke(defaultValueAnnotation) as String
|
columnInfo.defaultValue = defaultValueMethod.invoke(defaultValueAnnotation) as String
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalArgumentException("处理字段 ${field.name} 的默认值映射时出错: ${e.message}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlInfo.columns.add(columnInfo)
|
sqlInfo.columns.add(columnInfo)
|
||||||
|
processedFields.add(field.name) // 记录已处理的字段
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalArgumentException("处理字段 ${field.name} 时出错: ${e.message}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理主键
|
// 处理主键
|
||||||
mapper.primaryKeyMapping?.let { pkMapping ->
|
if (mapper.primaryKeyMapping != null) {
|
||||||
|
val pkMapping = mapper.primaryKeyMapping!!
|
||||||
val pkAnnotation = field.annotations.find {
|
val pkAnnotation = field.annotations.find {
|
||||||
it.annotationClass.java == pkMapping.annotationClass.java
|
it.annotationClass.qualifiedName == pkMapping.annotationClass.qualifiedName
|
||||||
}
|
}
|
||||||
if (pkAnnotation != null) {
|
if (pkAnnotation != null) {
|
||||||
|
try {
|
||||||
val pkMethod = pkAnnotation.javaClass.getMethod(pkMapping.propertyName)
|
val pkMethod = pkAnnotation.javaClass.getMethod(pkMapping.propertyName)
|
||||||
val isPk = pkMethod.invoke(pkAnnotation)
|
val isPk = pkMethod.invoke(pkAnnotation)
|
||||||
if (isPk is Boolean && isPk) {
|
if (isPk is Boolean && isPk) {
|
||||||
|
// 如果字段还未处理,创建默认列信息
|
||||||
|
if (!processedFields.contains(field.name)) {
|
||||||
|
columnInfo.name = toSnakeCase(field.name)
|
||||||
|
columnInfo.type = inferSqlType(field.type, columnInfo)
|
||||||
columnInfo.isPrimaryKey = true
|
columnInfo.isPrimaryKey = true
|
||||||
|
sqlInfo.columns.add(columnInfo)
|
||||||
sqlInfo.primaryKeys.add(columnInfo.name)
|
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
|
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 type: String = "",
|
||||||
var nullable: Boolean = true,
|
var nullable: Boolean = true,
|
||||||
var defaultValue: String = "",
|
var defaultValue: String = "",
|
||||||
var isPrimaryKey: Boolean = false
|
var isPrimaryKey: Boolean = false,
|
||||||
|
var enumValues: List<String>? = null
|
||||||
)
|
)
|
||||||
@ -23,26 +23,87 @@ class SqlGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val sb = StringBuilder()
|
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 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 nullable = if (column.nullable) "" else " NOT NULL"
|
||||||
val defaultValue = if (column.defaultValue.isNotEmpty()) " DEFAULT ${column.defaultValue}" else ""
|
" ${column.name} ${column.type}$defaultValue$nullable"
|
||||||
" ${column.name} ${column.type}$nullable$defaultValue"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.append(columnDefinitions.joinToString(",\n"))
|
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()) {
|
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);")
|
sb.append("\n);")
|
||||||
return sb.toString()
|
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
|
* 生成插入SQL
|
||||||
* @param entityClass 实体类
|
* @param entityClass 实体类
|
||||||
|
|||||||
@ -92,21 +92,67 @@ class SqlMigrationGenerator {
|
|||||||
* @param mapper 注解映射中间类
|
* @param mapper 注解映射中间类
|
||||||
*/
|
*/
|
||||||
fun generateMigrations(entityPackage: String, mapper: SqlAnnotationMapper) {
|
fun generateMigrations(entityPackage: String, mapper: SqlAnnotationMapper) {
|
||||||
|
// 确保日志路径存在
|
||||||
|
createDirectories()
|
||||||
|
|
||||||
|
log("开始扫描实体类...")
|
||||||
val entityClasses = scanEntityClasses(entityPackage)
|
val entityClasses = scanEntityClasses(entityPackage)
|
||||||
|
|
||||||
// 创建必要的目录
|
if (entityClasses.isEmpty()) {
|
||||||
createDirectories()
|
throw IllegalArgumentException("未找到任何实体类,请检查包路径: $entityPackage")
|
||||||
|
}
|
||||||
|
|
||||||
|
log("找到 ${entityClasses.size} 个实体类,准备验证...")
|
||||||
|
|
||||||
|
// 先验证所有实体类是否能够正确解析,如果有错误就立刻抛出异常
|
||||||
|
validateEntityClasses(entityClasses, mapper)
|
||||||
|
|
||||||
// 检查是否是初始迁移
|
// 检查是否是初始迁移
|
||||||
val isInitialMigration = isInitialMigration()
|
val isInitialMigration = isInitialMigration()
|
||||||
|
|
||||||
if (isInitialMigration) {
|
if (isInitialMigration) {
|
||||||
|
log("开始生成初始迁移...")
|
||||||
generateInitialMigration(entityClasses, mapper)
|
generateInitialMigration(entityClasses, mapper)
|
||||||
} else {
|
} else {
|
||||||
|
log("开始生成差异迁移...")
|
||||||
generateDiffMigration(entityClasses, mapper)
|
generateDiffMigration(entityClasses, mapper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证所有实体类是否可以正确解析SQL信息
|
||||||
|
*/
|
||||||
|
private fun validateEntityClasses(entityClasses: List<KClass<*>>, mapper: SqlAnnotationMapper) {
|
||||||
|
val errorMessages = mutableListOf<String>()
|
||||||
|
|
||||||
|
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注解的实体类
|
* 扫描包路径下标记了@TableName注解的实体类
|
||||||
*/
|
*/
|
||||||
@ -157,7 +203,12 @@ class SqlMigrationGenerator {
|
|||||||
|
|
||||||
// 添加到模型文件
|
// 添加到模型文件
|
||||||
addCreateTableToModel(modelDocument, changeSetElement, sqlInfo)
|
addCreateTableToModel(modelDocument, changeSetElement, sqlInfo)
|
||||||
|
|
||||||
|
log("已生成 ${entityClass.simpleName} 的创建表SQL")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
// 由于前面已经验证过所有实体类,这里不应该再有错误
|
||||||
|
// 但为了健壮性,还是添加错误处理
|
||||||
|
log("警告: 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}")
|
||||||
sqlBuilder.append("-- 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}\n\n")
|
sqlBuilder.append("-- 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}\n\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,12 +216,14 @@ class SqlMigrationGenerator {
|
|||||||
// 写入SQL文件
|
// 写入SQL文件
|
||||||
val sqlFileName = "$MIGRATION_PATH/$version.sql"
|
val sqlFileName = "$MIGRATION_PATH/$version.sql"
|
||||||
File(sqlFileName).writeText(sqlBuilder.toString())
|
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)
|
writeModelToFile(modelDocument, modelFileName)
|
||||||
|
log("生成模型文件: $modelFileName")
|
||||||
|
|
||||||
println("初始迁移生成完成: $version")
|
log("初始迁移生成完成: $version")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,12 +233,12 @@ class SqlMigrationGenerator {
|
|||||||
// 获取最新的模型文件
|
// 获取最新的模型文件
|
||||||
val latestModelFile = findLatestModelFile()
|
val latestModelFile = findLatestModelFile()
|
||||||
if (latestModelFile == null) {
|
if (latestModelFile == null) {
|
||||||
println("未找到现有模型文件,将生成初始迁移")
|
log("未找到现有模型文件,将生成初始迁移")
|
||||||
generateInitialMigration(entityClasses, mapper)
|
generateInitialMigration(entityClasses, mapper)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
println("找到最新模型文件: ${latestModelFile.name}")
|
log("找到最新模型文件: ${latestModelFile.name}")
|
||||||
|
|
||||||
// 解析最新的模型文件并构建完整模型
|
// 解析最新的模型文件并构建完整模型
|
||||||
val allTables = buildFullModel(latestModelFile)
|
val allTables = buildFullModel(latestModelFile)
|
||||||
@ -193,16 +246,16 @@ class SqlMigrationGenerator {
|
|||||||
// 获取当前版本号和生成新版本号
|
// 获取当前版本号和生成新版本号
|
||||||
val currentVersion = extractVersionFromFileName(latestModelFile.name)
|
val currentVersion = extractVersionFromFileName(latestModelFile.name)
|
||||||
if (!currentVersion.contains(".")) {
|
if (!currentVersion.contains(".")) {
|
||||||
println("错误: 提取的当前版本号 '$currentVersion' 格式不正确,将使用默认的 '1.0'")
|
log("错误: 提取的当前版本号 '$currentVersion' 格式不正确,将使用默认的 '1.0'")
|
||||||
val nextVersion = findNextVersionNumber("1.0")
|
val nextVersion = findNextVersionNumber("1.0")
|
||||||
println("基于默认版本找到下一个版本号: $nextVersion")
|
log("基于默认版本找到下一个版本号: $nextVersion")
|
||||||
continueWithMigration(nextVersion, allTables, entityClasses, mapper)
|
continueWithMigration(nextVersion, allTables, entityClasses, mapper)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否存在已有的版本号,找出当前最大版本号
|
// 检查是否存在已有的版本号,找出当前最大版本号
|
||||||
val nextVersion = findNextVersionNumber(currentVersion)
|
val nextVersion = findNextVersionNumber(currentVersion)
|
||||||
println("当前版本: $currentVersion, 新版本: $nextVersion")
|
log("当前版本: $currentVersion, 新版本: $nextVersion")
|
||||||
|
|
||||||
// 继续生成迁移
|
// 继续生成迁移
|
||||||
continueWithMigration(nextVersion, allTables, entityClasses, mapper)
|
continueWithMigration(nextVersion, allTables, entityClasses, mapper)
|
||||||
@ -223,20 +276,40 @@ class SqlMigrationGenerator {
|
|||||||
try {
|
try {
|
||||||
val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper)
|
val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper)
|
||||||
currentTables[sqlInfo.tableName] = sqlInfo
|
currentTables[sqlInfo.tableName] = sqlInfo
|
||||||
|
log("已提取 ${entityClass.simpleName} 的模型信息,表名: ${sqlInfo.tableName}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("处理实体类 ${entityClass.simpleName} 时出错: ${e.message}")
|
// 由于前面已验证,这里不应该再有错误
|
||||||
|
log("警告: 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 比较差异并生成SQL
|
// 比较差异并生成SQL
|
||||||
|
log("比较数据库结构差异...")
|
||||||
val diffResult = compareTables(allTables, currentTables)
|
val diffResult = compareTables(allTables, currentTables)
|
||||||
|
|
||||||
// 如果没有差异,不生成迁移文件
|
// 如果没有差异,不生成迁移文件
|
||||||
if (diffResult.isEmpty()) {
|
if (diffResult.isEmpty()) {
|
||||||
println("没有发现数据库结构变化,不生成迁移文件")
|
log("没有发现数据库结构变化,不生成迁移文件")
|
||||||
return
|
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 modelDocument = createModelDocument()
|
||||||
val changeSetElement = modelDocument.createElement("changeSet")
|
val changeSetElement = modelDocument.createElement("changeSet")
|
||||||
@ -265,21 +338,21 @@ class SqlMigrationGenerator {
|
|||||||
val sqlFileName = "$MIGRATION_PATH/$nextVersion.sql"
|
val sqlFileName = "$MIGRATION_PATH/$nextVersion.sql"
|
||||||
val sqlFile = File(sqlFileName)
|
val sqlFile = File(sqlFileName)
|
||||||
if (sqlFile.exists()) {
|
if (sqlFile.exists()) {
|
||||||
println("警告: SQL文件 $sqlFileName 已存在,将被覆盖")
|
log("警告: SQL文件 $sqlFileName 已存在,将被覆盖")
|
||||||
}
|
}
|
||||||
sqlFile.writeText(sqlBuilder.toString())
|
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)
|
val modelFile = File(modelFileName)
|
||||||
if (modelFile.exists()) {
|
if (modelFile.exists()) {
|
||||||
println("警告: 模型文件 $modelFileName 已存在,将被覆盖")
|
log("警告: 模型文件 $modelFileName 已存在,将被覆盖")
|
||||||
}
|
}
|
||||||
writeModelToFile(modelDocument, modelFileName)
|
writeModelToFile(modelDocument, modelFileName)
|
||||||
println("生成模型文件: $modelFileName")
|
log("生成模型文件: $modelFileName")
|
||||||
|
|
||||||
println("差异迁移生成完成: $nextVersion")
|
log("差异迁移生成完成: $nextVersion")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user