1
This commit is contained in:
parent
dbd2246a09
commit
726c388966
@ -41,16 +41,8 @@ tasks.test {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
spotless {
|
||||
|
||||
@ -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")
|
||||
|
||||
338
vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGen.kt
Normal file
338
vertx-fw/src/main/kotlin/org/aikrai/vertx/gen/SqlGen.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user