From 726c388966641a00f744ffb4e06928c824507951 Mon Sep 17 00:00:00 2001 From: AiKrai Date: Wed, 19 Mar 2025 14:23:09 +0800 Subject: [PATCH] 1 --- vertx-demo/build.gradle.kts | 12 +- vertx-fw/build.gradle.kts | 14 +- .../kotlin/org/aikrai/vertx/gen/SqlGen.kt | 338 ++++++++++++++++++ 3 files changed, 343 insertions(+), 21 deletions(-) create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGen.kt diff --git a/vertx-demo/build.gradle.kts b/vertx-demo/build.gradle.kts index 07d201f..0a297cd 100644 --- a/vertx-demo/build.gradle.kts +++ b/vertx-demo/build.gradle.kts @@ -41,16 +41,8 @@ tasks.test { } } -tasks.compileKotlin { - kotlinOptions { - jvmTarget = "17" - } -} - -tasks.compileTestKotlin { - kotlinOptions { - jvmTarget = "17" - } +kotlin { + jvmToolchain(17) } spotless { diff --git a/vertx-fw/build.gradle.kts b/vertx-fw/build.gradle.kts index f212056..7dee5cc 100644 --- a/vertx-fw/build.gradle.kts +++ b/vertx-fw/build.gradle.kts @@ -27,16 +27,8 @@ tasks.test { } } -tasks.compileKotlin { - kotlinOptions { - jvmTarget = "17" - } -} - -tasks.compileTestKotlin { - kotlinOptions { - jvmTarget = "17" - } +kotlin { + jvmToolchain(17) } spotless { @@ -55,7 +47,7 @@ spotless { } dependencies { -// implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20") implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion") implementation("io.vertx:vertx-core:$vertxVersion") implementation("io.vertx:vertx-web:$vertxVersion") diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGen.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGen.kt new file mode 100644 index 0000000..8f27140 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGen.kt @@ -0,0 +1,338 @@ +package org.aikrai.vertx.gen + +import org.aikrai.vertx.db.annotation.* +import java.sql.Timestamp +import java.time.LocalDate +import java.time.LocalDateTime +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberProperties + +/** + * SQL生成工具类 + * 根据Kotlin实体类生成PostgreSQL SQL语句 + */ +class SqlGen { + + // 注解扫描器 + private val scanner = AnnotationScanner.create( + valueProperty = TableName::value, + defaultValueMapper = { klass -> camelToSnake(klass.simpleName!!) } + ) + + // 实体类缓存 + private val entityCache = mutableMapOf, EntityCache>() + + /** + * 扫描指定包路径下的所有带有@TableName注解的类 + * @param packageName 包名,例如 "app.data.domain" + * @return 带有@TableName注解的类列表 + */ + fun scanEntities(packageName: String): List> { + return scanner.scanClasses(packageName) + } + + /** + * 解析实体类并缓存信息 + * @param entityClasses 实体类列表 + */ + fun parseEntities(entityClasses: List>) { + // 使用scanner解析类 + scanner.parseClasses(entityClasses) + + // 将scanner的结果转换为entityCache + scanner.getAnnotatedClasses().forEach { (klass, annotatedClass) -> + val fields = mutableListOf() + val indices = mutableListOf() + + // 解析字段 + annotatedClass.properties.forEach { propertyInfo -> + val fieldCache = parseField(propertyInfo.property as KProperty<*>) + fields.add(fieldCache) + } + + // 解析索引 + val tableIndices = klass.findAnnotation() + if (tableIndices != null) { + val indexCache = IndexCache( + name = tableIndices.name.ifEmpty { "idx_${annotatedClass.value}_${tableIndices.columnNames.joinToString("_")}" }, + unique = tableIndices.unique, + concurrent = tableIndices.concurrent, + columnNames = tableIndices.columnNames.toList(), + definition = tableIndices.definition + ) + indices.add(indexCache) + } + + entityCache[klass] = EntityCache( + entityClass = klass, + tableName = annotatedClass.value, + fields = fields, + indices = indices + ) + } + } + + /** + * 解析字段 + */ + private fun parseField(property: KProperty<*>): FieldCache { + val tableId = property.findAnnotation() + val tableField = property.findAnnotation() + + val isPrimary = tableId != null + val idType = tableId?.type ?: IdType.NONE + val columnName = tableField?.value?.ifEmpty { camelToSnake(property.name) } ?: camelToSnake(property.name) + val fieldFill = tableField?.fill ?: FieldFill.DEFAULT + + val returnType = property.returnType + + return FieldCache( + property = property, + columnName = columnName, + isPrimary = isPrimary, + idType = idType, + fieldFill = fieldFill, + returnType = returnType + ) + } + + /** + * 生成所有实体类的建表SQL + * @return 建表SQL语句列表 + */ + fun generateCreateTableSql(): List { + return entityCache.values.map { generateCreateTableSql(it) } + } + + /** + * 为单个实体生成建表SQL + */ + private fun generateCreateTableSql(entityCache: EntityCache): String { + val sb = StringBuilder() + + sb.appendLine("-- 为实体 ${entityCache.entityClass.simpleName} 创建表") + sb.appendLine("CREATE TABLE IF NOT EXISTS \"${entityCache.tableName}\" (") + + // 添加字段定义 + entityCache.fields.forEachIndexed { index, field -> + sb.append(" \"${field.columnName}\" ${mapKotlinTypeToPostgresType(field)}") + + // 主键约束 + if (field.isPrimary) { + sb.append(" PRIMARY KEY") + } + + // 非空约束 (根据Kotlin类型是否为可空) + if (!field.returnType.isMarkedNullable && !field.isPrimary) { + sb.append(" NOT NULL") + } + + // 自增约束 + if (field.isPrimary && field.idType == IdType.AUTO) { + sb.append(" GENERATED BY DEFAULT AS IDENTITY") + } + + // 默认值 + when (field.fieldFill) { + FieldFill.INSERT, FieldFill.INSERT_UPDATE -> { + if (field.returnType.classifier == Timestamp::class || + field.returnType.classifier == LocalDateTime::class || + field.returnType.classifier == LocalDate::class) { + sb.append(" DEFAULT CURRENT_TIMESTAMP") + } + } + else -> {} + } + + if (index < entityCache.fields.size - 1) { + sb.appendLine(",") + } else { + sb.appendLine() + } + } + + sb.appendLine(");") + + // 添加索引 + entityCache.indices.forEach { index -> + sb.appendLine() + sb.append("-- 创建索引: ${index.name}") + sb.appendLine() + sb.append("CREATE ") + if (index.unique) sb.append("UNIQUE ") + sb.append("INDEX ") + if (index.concurrent) sb.append("CONCURRENTLY ") + sb.append("IF NOT EXISTS ${index.name} ON \"${entityCache.tableName}\" ") + if (index.definition.isNotEmpty()) { + sb.append(index.definition) + } else { + sb.append("(${index.columnNames.joinToString(", ") { "\"$it\"" }})") + } + sb.appendLine(";") + } + + return sb.toString() + } + + /** + * 生成基本的CRUD SQL语句 + */ + fun generateCrudSql(): Map, CrudSql> { + return entityCache.mapValues { (_, entityCache) -> + val tableName = entityCache.tableName + val fields = entityCache.fields + + val primaryKeyField = fields.find { it.isPrimary } + val regularFields = fields.filter { !it.isPrimary } + + val insertFields = regularFields.filter { field -> + when (field.fieldFill) { + FieldFill.INSERT, FieldFill.INSERT_UPDATE -> true + else -> true + } + } + + val updateFields = regularFields.filter { field -> + when (field.fieldFill) { + FieldFill.UPDATE, FieldFill.INSERT_UPDATE -> true + else -> true + } + } + + val fieldList = fields.joinToString(", ") { "\"${it.columnName}\"" } + val primaryKeyColumn = primaryKeyField?.columnName ?: "id" + + val insertFieldList = insertFields.joinToString(", ") { "\"${it.columnName}\"" } + val insertValuesList = insertFields.joinToString(", ") { ":${it.property.name}" } + + val updateSetList = updateFields.joinToString(", ") { "\"${it.columnName}\" = :${it.property.name}" } + + CrudSql( + insertSql = "INSERT INTO \"$tableName\" ($insertFieldList) VALUES ($insertValuesList);", + selectByIdSql = "SELECT $fieldList FROM \"$tableName\" WHERE \"$primaryKeyColumn\" = :id;", + updateSql = "UPDATE \"$tableName\" SET $updateSetList WHERE \"$primaryKeyColumn\" = :id;", + deleteSql = "DELETE FROM \"$tableName\" WHERE \"$primaryKeyColumn\" = :id;" + ) + } + } + + /** + * 将驼峰命名转换为下划线命名 + */ + private fun camelToSnake(name: String): String { + return name.replace(Regex("([a-z0-9])([A-Z])"), "$1_$2").toLowerCase() + } + + /** + * 将Kotlin类型映射为PostgreSQL类型 + */ + private fun mapKotlinTypeToPostgresType(field: FieldCache): String { + val type = field.returnType.classifier + return when (type) { + String::class -> "VARCHAR(255)" + Int::class, Integer::class -> "INTEGER" + Long::class -> "BIGINT" + Double::class -> "DOUBLE PRECISION" + Float::class -> "REAL" + Boolean::class -> "BOOLEAN" + Timestamp::class, java.util.Date::class -> "TIMESTAMP" + LocalDateTime::class -> "TIMESTAMP" + LocalDate::class -> "DATE" + Char::class -> "CHAR(1)" + else -> { + // 处理枚举类型 + if (type is KClass<*> && type.java.isEnum) { + "VARCHAR(50)" + } else { + "JSONB" // 默认复杂类型存储为JSON + } + } + } + } + + /** + * 实体缓存类 + */ + data class EntityCache( + val entityClass: KClass<*>, + val tableName: String, + val fields: List, + val indices: List + ) + + /** + * 字段缓存类 + */ + data class FieldCache( + val property: KProperty<*>, + val columnName: String, + val isPrimary: Boolean, + val idType: IdType, + val fieldFill: FieldFill, + val returnType: kotlin.reflect.KType + ) + + /** + * 索引缓存类 + */ + data class IndexCache( + val name: String, + val unique: Boolean, + val concurrent: Boolean, + val columnNames: List, + val definition: String + ) + + /** + * CRUD SQL语句 + */ + data class CrudSql( + val insertSql: String, + val selectByIdSql: String, + val updateSql: String, + val deleteSql: String + ) + + companion object { + /** + * 快速生成指定包下所有实体类的SQL + */ + fun generateSql(packageName: String): Map { + val sqlGen = SqlGen() + val entities = sqlGen.scanEntities(packageName) + sqlGen.parseEntities(entities) + + val createTableSql = sqlGen.generateCreateTableSql() + val crudSql = sqlGen.generateCrudSql() + + val result = mutableMapOf() + + entities.forEachIndexed { index, entity -> + val tableName = sqlGen.scanner.getAnnotatedClass(entity)?.value + ?: camelToSnake(entity.simpleName!!) + + result["$tableName-create"] = createTableSql[index] + + val crud = crudSql[entity] + if (crud != null) { + result["$tableName-insert"] = crud.insertSql + result["$tableName-select"] = crud.selectByIdSql + result["$tableName-update"] = crud.updateSql + result["$tableName-delete"] = crud.deleteSql + } + } + + return result + } + + /** + * 将驼峰命名转换为下划线命名 + */ + private fun camelToSnake(name: String): String { + return name.replace(Regex("([a-z0-9])([A-Z])"), "$1_$2").toLowerCase() + } + } +} \ No newline at end of file