This commit is contained in:
AiKrai 2025-03-19 16:25:38 +08:00
parent 63a18f1dbd
commit 5b1efdf6e3
6 changed files with 102 additions and 489 deletions

View File

@ -1,116 +1,125 @@
---
description: PostgreSQL SQL生成pgsql生成
description: PostgreSQL数据库迁移工具, PostgreSQL SQL生成pgsql生成
globs:
alwaysApply: false
---
# Kotlin实体到PostgreSQL SQL生成专家
# PostgreSQL数据库迁移工具
## 角色定义
你是一位精通Kotlin编程和PostgreSQL数据库的专业工程师专门负责分析Kotlin实体类及其注解并生成对应的PostgreSQL SQL建表语句和常用操作语句。你具备深入理解对象关系映射(ORM)原理的能力能够准确识别各类数据库相关注解并转换为高效、符合最佳实践的SQL语句。当遇到缺少必要注解的情况时你能够根据命名约定和常见实践自动补充合理的默认配置
你是一个专业的PostgreSQL数据库迁移工具负责根据Kotlin实体类及其注解自动生成数据库迁移脚本。你能够智能识别实体类变更生成精确的差异SQL并维护数据库版本演进记录
## 专业知识
- 精通Kotlin语言特性及其反射机制
- 深入理解各类ORM框架的注解系统如JPA、自定义注解等
- 熟悉PostgreSQL特有的数据类型、约束和索引功能
- 掌握数据库设计最佳实践和性能优化技巧
- 了解各种字段类型映射关系和转换规则
- 熟悉命名约定和代码规范,能够推断缺失的注解信息
- 精通实体类到数据库表的自动映射规则
- 精通PostgreSQL数据库SQL语法和最佳实践
- 熟悉Kotlin实体类结构和常用ORM注解如JPA、Hibernate等
- 掌握数据库版本控制和迁移管理概念
- 了解XML格式数据存储和解析
- 具备文件操作和版本号管理能力
## 目标
根据用户提供的Kotlin实体类及其注解生成完整、准确的PostgreSQL建表语句和基本CRUD操作SQL。对于缺少必要注解的情况能够自动补充合理的默认值确保生成的SQL语句完整可用同时提供索引和约束建议确保数据库结构优化且符合最佳实践
为用户提供一个自动化工具根据Kotlin实体类生成PostgreSQL数据库迁移脚本实现数据库结构与代码模型的同步演进减少手动维护数据库脚本的工作量
## 约束
- 严格遵循PostgreSQL语法规范不混用其他数据库的特有功能
- 确保生成的SQL语句语法正确可直接执行不会报错
- 对缺失的注解信息做出合理推断,并明确指出做了哪些推断
- 不对实体类结构提出主观评价专注于SQL转换工作
- 保持生成的SQL简洁高效避免冗余构造
- 提供必要的SQL注释以提高可读性和可维护性
1. 仅生成PostgreSQL兼容的SQL语句
2. 遵循数据库迁移最佳实践确保生成的SQL安全可靠
3. 不删除或修改用户自定义的SQL文件
4. 确保版本号按顺序递增,避免版本混乱
5. 操作前对实体类进行验证防止生成错误的SQL
## 工作流程
1. 分析输入的Kotlin实体类代码及其注解
2. 识别实体类名称和映射表名(通过@TableName等注解
- 若缺少@TableName则使用类名转下划线命名作为表名
3. 解析所有字段及其对应的属性(字段名、类型、是否主键等)
4. 识别特殊注解如@TableId、@TableField、@TableIndex等
- 若缺少@TableId尝试识别命名为id、entityId等的字段作为主键
- 若缺少@TableField使用字段名转下划线命名作为列名
5. 将Kotlin数据类型映射到PostgreSQL数据类型
6. 生成CREATE TABLE语句包含所有必要的约束和索引
7. 根据需要生成基本的CRUD操作SQL模板
8. 提供其他可能有用的SQL片段视图、存储过程等
9. 明确指出为缺失注解自动补充的默认配置
## 输入格式
提供一个或多个Kotlin实体类的代码包含数据库相关注解部分注解可能缺失
```kotlin
// 可能缺少@TableName
class User {
// 可能缺少@TableId
var id: Long = 0L
// 可能缺少@TableField
var name: String = ""
var email: String = ""
// 其他字段...
}
```
1. **扫描与解析**扫描项目中的Kotlin实体类及其注解
2. **版本确认**检查resource/dbmigration/model目录状态
- 如果为空准备生成初始化脚本1.0版本)
- 如果不为空读取最新版本的XML文件确定下一个版本号
3. **差异分析**
- 初始化场景收集所有实体类信息生成完整建表SQL
- 更新场景比对XML记录与当前实体类识别添加/删除/修改的字段或表
4. **SQL生成**根据差异分析结果生成对应的SQL语句
5. **文件生成**
- 在resource/dbmigration目录生成SQL文件
- 在resource/dbmigration/model目录生成XML模型文件
- 使用正确的版本号命名文件例如1.0__initial.sql或1.1.sql
## 输出格式
生成的结果包含以下几个部分
工具将生成两种类型的文件:
### 1. 补充的注解信息
```
【注解补充说明】
- 缺少@TableName已将类名"User"转换为表名"user"
- 缺少@TableId已将字段"id"识别为主键采用ASSIGN_ID策略
- 缺少@TableField已将字段"name"映射为列名"name"
```
### 2. 建表语句
1. **SQL文件**
```sql
-- 为实体 User 创建表
CREATE TABLE "user" (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
-- 其他字段...
-- 版本: X.Y
-- 生成时间: YYYY-MM-DD HH:MM:SS
-- 描述: [初始化/更新描述]
-- [表操作类型:创建/修改/删除] 表 [表名]
CREATE TABLE user_info (
id SERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL,
email VARCHAR(200) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- [更多SQL语句...]
```
### 3. 索引创建语句
```sql
-- 创建推荐的索引
CREATE INDEX idx_user_email ON "user" (email);
2. **XML模型文件**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<database-model version="X.Y" timestamp="YYYY-MM-DD HH:MM:SS">
<entities>
<entity name="UserInfo" table="user_info">
<fields>
<field name="id" type="Long" column="id" primary-key="true" auto-increment="true"/>
<field name="username" type="String" column="username" nullable="false" length="100"/>
<field name="email" type="String" column="email" unique="true" length="200"/>
<field name="createdAt" type="Date" column="created_at" default="CURRENT_TIMESTAMP"/>
</fields>
</entity>
<!-- 更多实体定义 -->
</entities>
</database-model>
```
### 5. 字段类型映射说明
提供Kotlin类型到PostgreSQL类型的映射说明特别是特殊类型的处理方法。
## 交互模式
1. **配置阶段**
- 指定实体类包路径
- 设置输出目录默认为resource/dbmigration
- 配置特殊处理规则(可选)
## 注解识别与补充规则
能够识别并正确处理以下注解,同时为缺失的注解提供合理默认值:
- @TableName表名映射
- 缺失时:将类名转为下划线命名作为表名
- @TableId主键定义
- 缺失时识别命名为id、xxxId的字段为主键默认使用ASSIGN_ID策略
- @TableField字段名映射和填充策略
- 缺失时:将字段名转为下划线命名作为列名
- @TableIndex索引定义
- 缺失时:为可能需要索引的字段(如email、phone、username等)建议添加索引
- 其他自定义注解
2. **执行阶段**
- 触发扫描和生成过程
- 显示进度和结果摘要
## 类型映射规则
提供详细的Kotlin类型到PostgreSQL类型的映射规则例如
- String → VARCHAR(255)
- Long → BIGINT
- Int → INTEGER
- Boolean → BOOLEAN
- LocalDateTime/Timestamp → TIMESTAMP
- 枚举类型 → VARCHAR或自定义enum类型
- 等等
3. **反馈阶段**
- 报告生成结果(生成文件路径、版本号)
- 提供差异摘要(新增表、修改的字段等)
- 显示警告或建议(如有)
## 示例
针对用户提供的每个实体类生成完整的SQL定义和操作语句同时明确指出补充了哪些缺失的注解信息。
**用户输入**
```
扫描实体类并生成数据库迁移脚本:
- 实体类包路径: com.example.domain.entity
- 输出目录: src/main/resources/dbmigration
```
**工具输出**
```
===== 数据库迁移脚本生成报告 =====
检测到model目录为空准备生成初始化脚本。
扫描到的实体类:
- UserEntity → user
- ProductEntity → product
- OrderEntity → order
生成的文件:
- src/main/resources/dbmigration/1.0__initial.sql
- src/main/resources/dbmigration/model/1.0__initial.model.xml
初始化脚本包含:
- 3个表创建语句
- 5个索引创建语句
- 2个外键约束
脚本生成完成!
```

View File

@ -110,6 +110,9 @@ dependencies {
// doc
implementation("io.swagger.core.v3:swagger-core:2.2.27")
// XML解析库
implementation("javax.xml.bind:jaxb-api:2.3.1")
testImplementation("io.vertx:vertx-junit5")
testImplementation("org.junit.jupiter:junit-jupiter:$junitJupiterVersion")
testImplementation("org.mockito:mockito-core:5.15.2")

View File

@ -85,7 +85,7 @@ class SqlAnnotationMapperGenerator {
// 获取表名
mapper.tableName?.let { tableNameMapping ->
val annotation = entityClass.annotations.find { it.annotationClass == tableNameMapping.annotationClass.java }
val annotation = entityClass.annotations.find { it.annotationClass.toString() == tableNameMapping.annotationClass.toString() }
annotation?.let {
val method = it.javaClass.getMethod(tableNameMapping.propertyName)
sqlInfo.tableName = method.invoke(it) as String

View File

@ -1,8 +0,0 @@
package org.aikrai.vertx.db
object SqlHelper {
fun retBool(result: Int?): Boolean {
return null != result && result >= 1
}
}

View File

@ -1,239 +0,0 @@
package org.aikrai.vertx.db.annotation
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
/**
* SQL注解映射中间类
* 用于从实体类及其注解中提取信息以生成SQL
* 支持多套注解系统的映射配置
*/
class SqlAnnotationMapper {
// 表名注解配置
var tableNameAnnotationClass: KClass<out Annotation> = TableName::class
var tableNameProperty: String = "value"
var tableNameDefaultMapper: (KClass<*>) -> String = { klass -> camelToSnake(klass.simpleName!!) }
// 主键注解配置
var tableIdAnnotationClass: KClass<out Annotation> = TableId::class
var tableIdNameProperty: String = "value"
var tableIdTypeProperty: String = "type"
var tableIdDefaultMapper: (KProperty<*>) -> Boolean = { property ->
property.name == "id" || property.name.endsWith("Id")
}
// 字段注解配置
var tableFieldAnnotationClass: KClass<out Annotation> = TableField::class
var tableFieldNameProperty: String = "value"
var tableFieldFillProperty: String = "fill"
var tableFieldDefaultMapper: (KProperty<*>) -> String = { property -> camelToSnake(property.name) }
// 索引注解配置
var tableIndexAnnotationClass: KClass<out Annotation> = TableIndex::class
var tableIndexNameProperty: String = "name"
var tableIndexUniqueProperty: String = "unique"
var tableIndexColumnsProperty: String = "columnNames"
// 枚举值注解配置
var enumValueAnnotationClass: KClass<out Annotation> = EnumValue::class
/**
* 从实体类中获取表名
* @param entityClass 实体类
* @return 表名
*/
fun getTableName(entityClass: KClass<*>): String {
// 获取所有注解
val annotations = entityClass.annotations.filter { it.annotationClass == tableNameAnnotationClass }
return if (annotations.isNotEmpty()) {
val annotation = annotations.first()
// 通过反射获取注解的value属性值
val method = annotation::class.java.getMethod(tableNameProperty)
val value = method.invoke(annotation) as String
if (value.isNotEmpty()) value else tableNameDefaultMapper(entityClass)
} else {
tableNameDefaultMapper(entityClass)
}
}
/**
* 判断属性是否为主键
* @param property 属性
* @return 是否为主键
*/
fun isTableId(property: KProperty<*>): Boolean {
val hasAnnotation = property.annotations.any { it.annotationClass == tableIdAnnotationClass }
return hasAnnotation || tableIdDefaultMapper(property)
}
/**
* 获取属性对应的列名
* @param property 属性
* @return 列名
*/
fun getColumnName(property: KProperty<*>): String {
val annotations = property.annotations.filter { it.annotationClass == tableFieldAnnotationClass }
return if (annotations.isNotEmpty()) {
val annotation = annotations.first()
// 通过反射获取注解的value属性值
val method = annotation::class.java.getMethod(tableFieldNameProperty)
val value = method.invoke(annotation) as String
if (value.isNotEmpty()) value else tableFieldDefaultMapper(property)
} else {
tableFieldDefaultMapper(property)
}
}
/**
* 获取主键类型
* @param property 属性
* @return 主键类型如果有
*/
fun getIdType(property: KProperty<*>): Any? {
val annotations = property.annotations.filter { it.annotationClass == tableIdAnnotationClass }
if (annotations.isEmpty()) return null
val annotation = annotations.first()
// 通过反射获取注解的type属性值
val method = annotation::class.java.getMethod(tableIdTypeProperty)
return method.invoke(annotation)
}
/**
* 获取字段填充策略
* @param property 属性
* @return 填充策略如果有
*/
fun getFieldFill(property: KProperty<*>): Any? {
val annotations = property.annotations.filter { it.annotationClass == tableFieldAnnotationClass }
if (annotations.isEmpty()) return null
val annotation = annotations.first()
// 通过反射获取注解的fill属性值
val method = annotation::class.java.getMethod(tableFieldFillProperty)
return method.invoke(annotation)
}
/**
* 获取实体类的所有索引配置
* @param entityClass 实体类
* @return 索引配置列表
*/
fun getTableIndices(entityClass: KClass<*>): List<TableIndexInfo> {
val annotations = entityClass.annotations.filter { it.annotationClass == tableIndexAnnotationClass }
return annotations.map { annotation ->
val nameMethod = annotation::class.java.getMethod(tableIndexNameProperty)
val uniqueMethod = annotation::class.java.getMethod(tableIndexUniqueProperty)
val columnsMethod = annotation::class.java.getMethod(tableIndexColumnsProperty)
val name = nameMethod.invoke(annotation) as String
val unique = uniqueMethod.invoke(annotation) as Boolean
val columns = columnsMethod.invoke(annotation) as Array<*>
TableIndexInfo(
name = name,
unique = unique,
columnNames = columns.map { it.toString() }
)
}
}
/**
* 获取实体类的所有属性信息
* @param entityClass 实体类
* @return 属性信息列表
*/
fun getEntityProperties(entityClass: KClass<*>): List<PropertyInfo> {
return entityClass.memberProperties.map { property ->
PropertyInfo(
property = property,
columnName = getColumnName(property),
isPrimary = isTableId(property),
idType = getIdType(property),
fieldFill = getFieldFill(property)
)
}
}
/**
* 从实体类中提取所有SQL相关信息
* @param entityClass 实体类
* @return 实体类SQL信息
*/
fun extractEntityInfo(entityClass: KClass<*>): EntityInfo {
return EntityInfo(
entityClass = entityClass,
tableName = getTableName(entityClass),
properties = getEntityProperties(entityClass),
indices = getTableIndices(entityClass)
)
}
/**
* 驼峰命名转下划线命名
* @param name 驼峰命名
* @return 下划线命名
*/
private fun camelToSnake(name: String): String {
return name.replace(Regex("([a-z0-9])([A-Z])"), "$1_$2").lowercase()
}
/**
* 表索引信息
*/
data class TableIndexInfo(
val name: String,
val unique: Boolean,
val columnNames: List<String>
)
/**
* 属性信息
*/
data class PropertyInfo(
val property: KProperty<*>,
val columnName: String,
val isPrimary: Boolean,
val idType: Any?,
val fieldFill: Any?
)
/**
* 实体类SQL信息
*/
data class EntityInfo(
val entityClass: KClass<*>,
val tableName: String,
val properties: List<PropertyInfo>,
val indices: List<TableIndexInfo>
)
companion object {
/**
* 创建默认配置的SQL注解映射器
* @return SQL注解映射器
*/
fun createDefault(): SqlAnnotationMapper {
return SqlAnnotationMapper()
}
/**
* 创建自定义配置的SQL注解映射器
* @param configure 配置函数
* @return SQL注解映射器
*/
fun create(configure: SqlAnnotationMapper.() -> Unit): SqlAnnotationMapper {
val mapper = SqlAnnotationMapper()
mapper.configure()
return mapper
}
}
}

View File

@ -1,152 +0,0 @@
package org.aikrai.vertx.db.annotation
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance
/**
* SqlAnnotationMapper 使用示例
*/
class SqlAnnotationMapperExample {
/**
* 使用默认配置的中间类示例
*/
fun defaultMapperExample(entityClass: KClass<*>) {
// 创建默认配置的SQL注解映射器
val mapper = SqlAnnotationMapper.createDefault()
// 从实体类中提取信息
val entityInfo = mapper.extractEntityInfo(entityClass)
// 输出实体信息
println("表名: ${entityInfo.tableName}")
println("字段:")
entityInfo.properties.forEach { prop ->
println(" ${prop.columnName} - 主键: ${prop.isPrimary} - 类型: ${prop.property.returnType}")
if (prop.idType != null) {
println(" ID类型: ${prop.idType}")
}
if (prop.fieldFill != null) {
println(" 填充策略: ${prop.fieldFill}")
}
}
println("索引:")
entityInfo.indices.forEach { index ->
println(" ${index.name} - 唯一: ${index.unique} - 列: ${index.columnNames.joinToString(", ")}")
}
}
/**
* 配置自定义注解映射示例
*/
fun customMapperExample(entityClass: KClass<*>) {
// 创建自定义配置的SQL注解映射器
val mapper = SqlAnnotationMapper.create {
// 假设我们使用了另一套注解系统例如JPA注解
// 配置表名注解 - 使用JPA的@Table注解
tableNameAnnotationClass = Table::class
tableNameProperty = "name"
// 配置主键注解 - 使用JPA的@Id注解
tableIdAnnotationClass = Id::class
// 配置字段注解 - 使用JPA的@Column注解
tableFieldAnnotationClass = Column::class
tableFieldNameProperty = "name"
// 自定义名称转换规则
tableNameDefaultMapper = { kClass -> kClass.simpleName!!.lowercase() }
tableFieldDefaultMapper = { property -> property.name.lowercase() }
}
// 从实体类中提取信息
val entityInfo = mapper.extractEntityInfo(entityClass)
// 输出实体信息
println("表名: ${entityInfo.tableName}")
println("字段:")
entityInfo.properties.forEach { prop ->
println(" ${prop.columnName} - 主键: ${prop.isPrimary}")
}
}
/**
* SQL生成器示例展示如何根据提取的信息生成SQL
*/
fun sqlGeneratorExample(entityClass: KClass<*>) {
// 创建SQL注解映射器
val mapper = SqlAnnotationMapper.createDefault()
// 提取实体信息
val entityInfo = mapper.extractEntityInfo(entityClass)
// 生成建表SQL
val createTableSql = generateCreateTableSql(entityInfo)
println("建表SQL:")
println(createTableSql)
}
/**
* 生成建表SQL
*/
private fun generateCreateTableSql(entityInfo: SqlAnnotationMapper.EntityInfo): String {
val sb = StringBuilder()
sb.append("CREATE TABLE IF NOT EXISTS ${entityInfo.tableName} (\n")
// 添加字段定义
val columns = entityInfo.properties.map { prop ->
val primaryKey = if (prop.isPrimary) " PRIMARY KEY" else ""
" ${prop.columnName} ${mapTypeToPostgresType(prop)} ${primaryKey}"
}
sb.append(columns.joinToString(",\n"))
sb.append("\n);")
// 添加索引
entityInfo.indices.forEach { index ->
val unique = if (index.unique) "UNIQUE " else ""
sb.append("\n\nCREATE ${unique}INDEX IF NOT EXISTS ${index.name} ON ${entityInfo.tableName} (${index.columnNames.joinToString(", ")});")
}
return sb.toString()
}
/**
* 将属性类型映射为PostgreSQL类型简化示例
*/
private fun mapTypeToPostgresType(property: SqlAnnotationMapper.PropertyInfo): String {
val typeName = property.property.returnType.toString()
return when {
typeName.contains("String") -> "VARCHAR(255)"
typeName.contains("Int") -> "INTEGER"
typeName.contains("Long") -> "BIGINT"
typeName.contains("Boolean") -> "BOOLEAN"
typeName.contains("Double") -> "DOUBLE PRECISION"
typeName.contains("Float") -> "REAL"
typeName.contains("java.sql.Timestamp") || typeName.contains("java.util.Date") -> "TIMESTAMP"
typeName.contains("LocalDateTime") -> "TIMESTAMP"
typeName.contains("LocalDate") -> "DATE"
typeName.contains("Char") -> "CHAR(1)"
else -> "JSONB" // 默认复杂类型存储为JSON
}
}
}
// JPA注解模拟 - 仅用于示例
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Table(val name: String = "")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class Id
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class Column(val name: String = "")