1
This commit is contained in:
parent
dbd2246a09
commit
726c388966
@ -41,16 +41,8 @@ tasks.test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.compileKotlin {
|
kotlin {
|
||||||
kotlinOptions {
|
jvmToolchain(17)
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.compileTestKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
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