This commit is contained in:
AiKrai 2025-03-19 14:23:09 +08:00
parent dbd2246a09
commit 726c388966
3 changed files with 343 additions and 21 deletions

View File

@ -41,16 +41,8 @@ tasks.test {
} }
} }
tasks.compileKotlin { kotlin {
kotlinOptions { jvmToolchain(17)
jvmTarget = "17"
}
}
tasks.compileTestKotlin {
kotlinOptions {
jvmTarget = "17"
}
} }
spotless { spotless {

View File

@ -27,16 +27,8 @@ tasks.test {
} }
} }
tasks.compileKotlin { kotlin {
kotlinOptions { jvmToolchain(17)
jvmTarget = "17"
}
}
tasks.compileTestKotlin {
kotlinOptions {
jvmTarget = "17"
}
} }
spotless { spotless {
@ -55,7 +47,7 @@ spotless {
} }
dependencies { 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-lang-kotlin-coroutines:$vertxVersion")
implementation("io.vertx:vertx-core:$vertxVersion") implementation("io.vertx:vertx-core:$vertxVersion")
implementation("io.vertx:vertx-web:$vertxVersion") implementation("io.vertx:vertx-web:$vertxVersion")

View File

@ -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<TableName, String>(
valueProperty = TableName::value,
defaultValueMapper = { klass -> camelToSnake(klass.simpleName!!) }
)
// 实体类缓存
private val entityCache = mutableMapOf<KClass<*>, EntityCache>()
/**
* 扫描指定包路径下的所有带有@TableName注解的类
* @param packageName 包名例如 "app.data.domain"
* @return 带有@TableName注解的类列表
*/
fun scanEntities(packageName: String): List<KClass<*>> {
return scanner.scanClasses(packageName)
}
/**
* 解析实体类并缓存信息
* @param entityClasses 实体类列表
*/
fun parseEntities(entityClasses: List<KClass<*>>) {
// 使用scanner解析类
scanner.parseClasses(entityClasses)
// 将scanner的结果转换为entityCache
scanner.getAnnotatedClasses().forEach { (klass, annotatedClass) ->
val fields = mutableListOf<FieldCache>()
val indices = mutableListOf<IndexCache>()
// 解析字段
annotatedClass.properties.forEach { propertyInfo ->
val fieldCache = parseField(propertyInfo.property as KProperty<*>)
fields.add(fieldCache)
}
// 解析索引
val tableIndices = klass.findAnnotation<TableIndex>()
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<TableId>()
val tableField = property.findAnnotation<TableField>()
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<String> {
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<KClass<*>, 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<FieldCache>,
val indices: List<IndexCache>
)
/**
* 字段缓存类
*/
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<String>,
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<String, String> {
val sqlGen = SqlGen()
val entities = sqlGen.scanEntities(packageName)
sqlGen.parseEntities(entities)
val createTableSql = sqlGen.generateCreateTableSql()
val crudSql = sqlGen.generateCrudSql()
val result = mutableMapOf<String, String>()
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()
}
}
}