1
This commit is contained in:
parent
cc415e82d0
commit
4f31d1517f
@ -3,6 +3,8 @@ package app
|
|||||||
import org.aikrai.vertx.db.annotation.TableField
|
import org.aikrai.vertx.db.annotation.TableField
|
||||||
import org.aikrai.vertx.db.annotation.TableId
|
import org.aikrai.vertx.db.annotation.TableId
|
||||||
import org.aikrai.vertx.db.annotation.TableName
|
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.AnnotationMapping
|
||||||
import org.aikrai.vertx.db.migration.ColumnMapping
|
import org.aikrai.vertx.db.migration.ColumnMapping
|
||||||
import org.aikrai.vertx.db.migration.DbMigration
|
import org.aikrai.vertx.db.migration.DbMigration
|
||||||
@ -38,6 +40,7 @@ object GenerateMigration {
|
|||||||
val dbMigration = DbMigration.create()
|
val dbMigration = DbMigration.create()
|
||||||
dbMigration.setEntityPackage("app.data.domain") // 指定实体类包路径
|
dbMigration.setEntityPackage("app.data.domain") // 指定实体类包路径
|
||||||
dbMigration.setSqlAnnotationMapper(mapper)
|
dbMigration.setSqlAnnotationMapper(mapper)
|
||||||
|
dbMigration.setGenerateDropStatements(false) // 不生成删除语句
|
||||||
|
|
||||||
// 生成迁移
|
// 生成迁移
|
||||||
val migrationVersion = dbMigration.generateMigration()
|
val migrationVersion = dbMigration.generateMigration()
|
||||||
@ -55,6 +58,12 @@ object GenerateMigration {
|
|||||||
private fun createSqlAnnotationMapper(): SqlAnnotationMapper {
|
private fun createSqlAnnotationMapper(): SqlAnnotationMapper {
|
||||||
val mapper = SqlAnnotationMapper()
|
val mapper = SqlAnnotationMapper()
|
||||||
|
|
||||||
|
// 设置实体类注解映射
|
||||||
|
mapper.entityMapping = AnnotationMapping(
|
||||||
|
annotationClass = TableName::class,
|
||||||
|
propertyName = "value"
|
||||||
|
)
|
||||||
|
|
||||||
// 设置表名映射
|
// 设置表名映射
|
||||||
mapper.tableName = AnnotationMapping(
|
mapper.tableName = AnnotationMapping(
|
||||||
annotationClass = TableName::class,
|
annotationClass = TableName::class,
|
||||||
@ -79,6 +88,14 @@ object GenerateMigration {
|
|||||||
defaultValueMapping = AnnotationMapping(
|
defaultValueMapping = AnnotationMapping(
|
||||||
annotationClass = TableField::class,
|
annotationClass = TableField::class,
|
||||||
propertyName = "default"
|
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,
|
annotationClass = TableId::class,
|
||||||
propertyName = "value"
|
propertyName = "value"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 设置索引映射
|
||||||
|
mapper.indexMapping = AnnotationMapping(
|
||||||
|
annotationClass = TableIndex::class,
|
||||||
|
propertyName = "name"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 设置枚举值映射
|
||||||
|
mapper.enumValueMapping = AnnotationMapping(
|
||||||
|
annotationClass = EnumValue::class,
|
||||||
|
propertyName = "value"
|
||||||
|
)
|
||||||
|
|
||||||
return mapper
|
return mapper
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package org.aikrai.vertx.db.migration
|
package org.aikrai.vertx.db.migration
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成PostgreSQL DDL迁移脚本,基于实体类及其注解的变更。
|
* 生成PostgreSQL DDL迁移脚本,基于实体类及其注解的变更。
|
||||||
@ -88,6 +86,15 @@ interface DbMigration {
|
|||||||
* 设置是否输出日志到控制台(默认为true)
|
* 设置是否输出日志到控制台(默认为true)
|
||||||
*/
|
*/
|
||||||
fun setLogToSystemOut(logToSystemOut: Boolean)
|
fun setLogToSystemOut(logToSystemOut: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否生成删除语句(默认为false)
|
||||||
|
* <p>
|
||||||
|
* 如果设置为false,生成的SQL中将不包含任何DROP语句。
|
||||||
|
* 但.model.xml文件中仍会记录删除表和字段的信息。
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
fun setGenerateDropStatements(generateDropStatements: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成下一次迁移SQL脚本和相关模型XML
|
* 生成下一次迁移SQL脚本和相关模型XML
|
||||||
|
|||||||
@ -16,6 +16,7 @@ class DefaultDbMigration : DbMigration {
|
|||||||
private var version: String? = null
|
private var version: String? = null
|
||||||
private var name: String? = null
|
private var name: String? = null
|
||||||
private var logToSystemOut: Boolean = true
|
private var logToSystemOut: Boolean = true
|
||||||
|
private var generateDropStatements: Boolean = false
|
||||||
|
|
||||||
override fun setEntityPackage(packagePath: String) {
|
override fun setEntityPackage(packagePath: String) {
|
||||||
this.entityPackage = packagePath
|
this.entityPackage = packagePath
|
||||||
@ -52,6 +53,10 @@ class DefaultDbMigration : DbMigration {
|
|||||||
override fun setLogToSystemOut(logToSystemOut: Boolean) {
|
override fun setLogToSystemOut(logToSystemOut: Boolean) {
|
||||||
this.logToSystemOut = logToSystemOut
|
this.logToSystemOut = logToSystemOut
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setGenerateDropStatements(generateDropStatements: Boolean) {
|
||||||
|
this.generateDropStatements = generateDropStatements
|
||||||
|
}
|
||||||
|
|
||||||
override fun generateMigration(): String? {
|
override fun generateMigration(): String? {
|
||||||
validateConfiguration()
|
validateConfiguration()
|
||||||
@ -66,6 +71,9 @@ class DefaultDbMigration : DbMigration {
|
|||||||
System.setProperty("ddl.migration.name", name!!)
|
System.setProperty("ddl.migration.name", name!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置是否生成删除语句
|
||||||
|
System.setProperty("ddl.migration.generateDropStatements", generateDropStatements.toString())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SqlMigrationGenerator.generateMigrations(entityPackage, sqlAnnotationMapper!!)
|
SqlMigrationGenerator.generateMigrations(entityPackage, sqlAnnotationMapper!!)
|
||||||
return version
|
return version
|
||||||
@ -83,6 +91,7 @@ class DefaultDbMigration : DbMigration {
|
|||||||
if (name != null) {
|
if (name != null) {
|
||||||
System.clearProperty("ddl.migration.name")
|
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.name", name!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置是否生成删除语句
|
||||||
|
System.setProperty("ddl.migration.generateDropStatements", generateDropStatements.toString())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 修改目录结构,强制生成初始迁移
|
// 修改目录结构,强制生成初始迁移
|
||||||
val modelDir = File("${pathToResources}/${migrationPath}/${modelPath}")
|
val modelDir = File("${pathToResources}/${migrationPath}/${modelPath}")
|
||||||
@ -128,6 +140,7 @@ class DefaultDbMigration : DbMigration {
|
|||||||
if (name != null) {
|
if (name != null) {
|
||||||
System.clearProperty("ddl.migration.name")
|
System.clearProperty("ddl.migration.name")
|
||||||
}
|
}
|
||||||
|
System.clearProperty("ddl.migration.generateDropStatements")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.aikrai.vertx.db.migration
|
package org.aikrai.vertx.db.migration
|
||||||
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -8,6 +7,12 @@ import kotlin.reflect.KClass
|
|||||||
* 用于记录从哪些注解获取SQL生成所需的信息
|
* 用于记录从哪些注解获取SQL生成所需的信息
|
||||||
*/
|
*/
|
||||||
class SqlAnnotationMapper {
|
class SqlAnnotationMapper {
|
||||||
|
/**
|
||||||
|
* 实体类注解映射
|
||||||
|
* 用于标识哪个类是实体类
|
||||||
|
*/
|
||||||
|
var entityMapping: AnnotationMapping? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表名映射信息
|
* 表名映射信息
|
||||||
*/
|
*/
|
||||||
@ -23,6 +28,16 @@ class SqlAnnotationMapper {
|
|||||||
*/
|
*/
|
||||||
var primaryKeyMapping: AnnotationMapping? = null
|
var primaryKeyMapping: AnnotationMapping? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引映射信息
|
||||||
|
*/
|
||||||
|
var indexMapping: AnnotationMapping? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 枚举值映射信息
|
||||||
|
*/
|
||||||
|
var enumValueMapping: AnnotationMapping? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 其他自定义映射
|
* 其他自定义映射
|
||||||
*/
|
*/
|
||||||
@ -65,384 +80,9 @@ data class ColumnMapping(
|
|||||||
/** 是否可为空映射,可选 */
|
/** 是否可为空映射,可选 */
|
||||||
val nullableMapping: AnnotationMapping? = null,
|
val nullableMapping: AnnotationMapping? = null,
|
||||||
/** 默认值映射,可选 */
|
/** 默认值映射,可选 */
|
||||||
val defaultValueMapping: AnnotationMapping? = null
|
val defaultValueMapping: AnnotationMapping? = null,
|
||||||
)
|
/** 字段长度映射,可选 */
|
||||||
|
val lengthMapping: AnnotationMapping? = null,
|
||||||
/**
|
/** 是否唯一映射,可选 */
|
||||||
* SQL注解映射生成器
|
val uniqueMapping: AnnotationMapping? = null
|
||||||
* 用于生成和使用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<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()
|
|
||||||
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<ColumnInfo> = mutableListOf(),
|
|
||||||
val primaryKeys: MutableList<String> = mutableListOf()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 列信息类
|
|
||||||
*/
|
|
||||||
data class ColumnInfo(
|
|
||||||
var name: String = "",
|
|
||||||
var type: String = "",
|
|
||||||
var nullable: Boolean = true,
|
|
||||||
var defaultValue: String = "",
|
|
||||||
var isPrimaryKey: Boolean = false,
|
|
||||||
var enumValues: List<String>? = null
|
|
||||||
)
|
|
||||||
@ -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<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()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -55,6 +55,12 @@ class SqlGenerator {
|
|||||||
|
|
||||||
sb.append(columnDefinitions.joinToString(",\n"))
|
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约束
|
// 添加枚举约束 - 识别枚举类型的列并添加CHECK约束
|
||||||
val enumColumns = columns.filter { it.type.contains("VARCHAR") && it.enumValues != null && it.enumValues!!.isNotEmpty() }
|
val enumColumns = columns.filter { it.type.contains("VARCHAR") && it.enumValues != null && it.enumValues!!.isNotEmpty() }
|
||||||
for (column in enumColumns) {
|
for (column in enumColumns) {
|
||||||
@ -71,6 +77,53 @@ class SqlGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sb.append("\n);")
|
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()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
package org.aikrai.vertx.db.migration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL信息类
|
||||||
|
* 存储从实体类中提取的SQL相关信息
|
||||||
|
*/
|
||||||
|
data class SqlInfo(
|
||||||
|
var tableName: String = "",
|
||||||
|
val columns: MutableList<ColumnInfo> = mutableListOf(),
|
||||||
|
val primaryKeys: MutableList<String> = mutableListOf(),
|
||||||
|
val indexes: MutableList<IndexInfo> = mutableListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列信息类
|
||||||
|
*/
|
||||||
|
data class ColumnInfo(
|
||||||
|
var name: String = "",
|
||||||
|
var type: String = "",
|
||||||
|
var nullable: Boolean = true,
|
||||||
|
var defaultValue: String = "",
|
||||||
|
var isPrimaryKey: Boolean = false,
|
||||||
|
var enumValues: List<String>? = null,
|
||||||
|
var unique: Boolean = false,
|
||||||
|
var length: Int = 255
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引信息类
|
||||||
|
*/
|
||||||
|
data class IndexInfo(
|
||||||
|
var name: String = "",
|
||||||
|
var columnNames: List<String> = listOf(),
|
||||||
|
var unique: Boolean = false,
|
||||||
|
var concurrent: Boolean = false,
|
||||||
|
var definition: String = ""
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user