From e97f3f5519fdff8ba24aeb95b0a6bbdf27f57556 Mon Sep 17 00:00:00 2001 From: AiKrai Date: Fri, 21 Mar 2025 15:48:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20=E6=B7=BB=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=BF=81=E7=A7=BB=E5=B7=A5=E5=85=B7=E5=92=8C?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vertx-demo/build.gradle.kts | 25 +- .../kotlin/app/data/domain/account/Account.kt | 1 + .../data/domain/account/AccountRepository.kt | 2 +- .../domain/account/AccountRepositoryImpl.kt | 2 +- .../app/data/domain/menu/MenuRepository.kt | 2 +- .../data/domain/menu/MenuRepositoryImpl.kt | 2 +- .../app/data/domain/role/RoleRepository.kt | 2 +- .../data/domain/role/RoleRepositoryImpl.kt | 2 +- .../resources/dbmigration/1.0__initial.sql | 48 + .../dbmigration/model/1.0__initial.model.xml | 40 + .../src/test/kotlin/app/GenerateMigration.kt | 114 ++ .../app/controller/AuthControllerTest.kt | 83 -- .../test/resources/application-apifox.yaml | 4 - .../test/resources/application-database.yaml | 14 - .../src/test/resources/application.yaml | 5 - vertx-fw/build.gradle.kts | 14 +- .../kotlin/org/aikrai/vertx/db/SqlHelper.kt | 8 - .../aikrai/vertx/db/annotation/Annotation.kt | 12 + .../aikrai/vertx/db/migration/DbMigration.kt | 123 ++ .../vertx/db/migration/DefaultDbMigration.kt | 181 +++ .../vertx/db/migration/SqlAnnotationMapper.kt | 90 ++ .../migration/SqlAnnotationMapperGenerator.kt | 617 +++++++++ .../aikrai/vertx/db/migration/SqlGenerator.kt | 333 +++++ .../org/aikrai/vertx/db/migration/SqlInfo.kt | 38 + .../db/migration/SqlMigrationGenerator.kt | 1157 +++++++++++++++++ .../vertx/db/{ => wrapper}/QueryWrapper.kt | 2 +- .../db/{ => wrapper}/QueryWrapperImpl.kt | 2 +- .../vertx/db/{ => wrapper}/Repository.kt | 2 +- .../vertx/db/{ => wrapper}/RepositoryImpl.kt | 2 +- 29 files changed, 2777 insertions(+), 150 deletions(-) create mode 100644 vertx-demo/src/main/resources/dbmigration/1.0__initial.sql create mode 100644 vertx-demo/src/main/resources/dbmigration/model/1.0__initial.model.xml create mode 100644 vertx-demo/src/test/kotlin/app/GenerateMigration.kt delete mode 100644 vertx-demo/src/test/kotlin/app/controller/AuthControllerTest.kt delete mode 100644 vertx-demo/src/test/resources/application-apifox.yaml delete mode 100644 vertx-demo/src/test/resources/application-database.yaml delete mode 100644 vertx-demo/src/test/resources/application.yaml delete mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/SqlHelper.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DbMigration.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DefaultDbMigration.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapper.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapperGenerator.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlGenerator.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlInfo.kt create mode 100644 vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlMigrationGenerator.kt rename vertx-fw/src/main/kotlin/org/aikrai/vertx/db/{ => wrapper}/QueryWrapper.kt (97%) rename vertx-fw/src/main/kotlin/org/aikrai/vertx/db/{ => wrapper}/QueryWrapperImpl.kt (99%) rename vertx-fw/src/main/kotlin/org/aikrai/vertx/db/{ => wrapper}/Repository.kt (93%) rename vertx-fw/src/main/kotlin/org/aikrai/vertx/db/{ => wrapper}/RepositoryImpl.kt (99%) diff --git a/vertx-demo/build.gradle.kts b/vertx-demo/build.gradle.kts index ec9f00d..8e035a5 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 { @@ -88,10 +80,14 @@ dependencies { implementation("io.vertx:vertx-auth-jwt:$vertxVersion") implementation("io.vertx:vertx-redis-client:$vertxVersion") + implementation("dev.langchain4j:langchain4j-open-ai:1.0.0-beta1") + implementation("dev.langchain4j:langchain4j:1.0.0-beta1") implementation("com.google.inject:guice:5.1.0") implementation("org.reflections:reflections:0.10.2") - implementation("cn.hutool:hutool-all:5.8.24") + implementation("cn.hutool:hutool-core:5.8.24") + implementation("cn.hutool:hutool-json:5.8.24") + implementation("cn.hutool:hutool-crypto:5.8.24") implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2") // implementation("com.github.ben-manes.caffeine:caffeine:3.1.8") @@ -103,10 +99,6 @@ dependencies { implementation("ch.qos.logback:logback-classic:1.4.14") implementation("org.codehaus.janino:janino:3.1.8") - // jpa -// implementation("jakarta.persistence:jakarta.persistence-api:3.2.0") -// implementation("jakarta.validation:jakarta.validation-api:3.1.0") - // db implementation("org.postgresql:postgresql:42.7.5") implementation("com.ongres.scram:client:2.1") @@ -114,6 +106,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") diff --git a/vertx-demo/src/main/kotlin/app/data/domain/account/Account.kt b/vertx-demo/src/main/kotlin/app/data/domain/account/Account.kt index a23cd6f..5f8a051 100644 --- a/vertx-demo/src/main/kotlin/app/data/domain/account/Account.kt +++ b/vertx-demo/src/main/kotlin/app/data/domain/account/Account.kt @@ -10,6 +10,7 @@ import java.sql.Timestamp class Account : BaseEntity() { @TableId(type = IdType.ASSIGN_ID) + @TableFieldComment("用户ID") var userId: Long = 0L @TableField("user_name") diff --git a/vertx-demo/src/main/kotlin/app/data/domain/account/AccountRepository.kt b/vertx-demo/src/main/kotlin/app/data/domain/account/AccountRepository.kt index 555e0ed..e747f0f 100644 --- a/vertx-demo/src/main/kotlin/app/data/domain/account/AccountRepository.kt +++ b/vertx-demo/src/main/kotlin/app/data/domain/account/AccountRepository.kt @@ -3,7 +3,7 @@ package app.data.domain.account import app.data.domain.account.modle.AccountRoleAccessDTO import app.data.domain.account.modle.AccountRoleDTO import com.google.inject.ImplementedBy -import org.aikrai.vertx.db.Repository +import org.aikrai.vertx.db.wrapper.Repository @ImplementedBy(AccountRepositoryImpl::class) interface AccountRepository : Repository { diff --git a/vertx-demo/src/main/kotlin/app/data/domain/account/AccountRepositoryImpl.kt b/vertx-demo/src/main/kotlin/app/data/domain/account/AccountRepositoryImpl.kt index 59a7a00..a1a269e 100644 --- a/vertx-demo/src/main/kotlin/app/data/domain/account/AccountRepositoryImpl.kt +++ b/vertx-demo/src/main/kotlin/app/data/domain/account/AccountRepositoryImpl.kt @@ -4,7 +4,7 @@ import app.data.domain.account.modle.AccountRoleAccessDTO import app.data.domain.account.modle.AccountRoleDTO import com.google.inject.Inject import io.vertx.sqlclient.SqlClient -import org.aikrai.vertx.db.RepositoryImpl +import org.aikrai.vertx.db.wrapper.RepositoryImpl class AccountRepositoryImpl @Inject constructor( sqlClient: SqlClient diff --git a/vertx-demo/src/main/kotlin/app/data/domain/menu/MenuRepository.kt b/vertx-demo/src/main/kotlin/app/data/domain/menu/MenuRepository.kt index 1bcc314..3eed7b5 100644 --- a/vertx-demo/src/main/kotlin/app/data/domain/menu/MenuRepository.kt +++ b/vertx-demo/src/main/kotlin/app/data/domain/menu/MenuRepository.kt @@ -1,7 +1,7 @@ package app.data.domain.menu import com.google.inject.ImplementedBy -import org.aikrai.vertx.db.Repository +import org.aikrai.vertx.db.wrapper.Repository @ImplementedBy(MenuRepositoryImpl::class) interface MenuRepository : Repository { diff --git a/vertx-demo/src/main/kotlin/app/data/domain/menu/MenuRepositoryImpl.kt b/vertx-demo/src/main/kotlin/app/data/domain/menu/MenuRepositoryImpl.kt index 27668eb..8e16acf 100644 --- a/vertx-demo/src/main/kotlin/app/data/domain/menu/MenuRepositoryImpl.kt +++ b/vertx-demo/src/main/kotlin/app/data/domain/menu/MenuRepositoryImpl.kt @@ -2,7 +2,7 @@ package app.data.domain.menu import com.google.inject.Inject import io.vertx.sqlclient.SqlClient -import org.aikrai.vertx.db.RepositoryImpl +import org.aikrai.vertx.db.wrapper.RepositoryImpl class MenuRepositoryImpl @Inject constructor( sqlClient: SqlClient diff --git a/vertx-demo/src/main/kotlin/app/data/domain/role/RoleRepository.kt b/vertx-demo/src/main/kotlin/app/data/domain/role/RoleRepository.kt index e0a8148..4f1d5bf 100644 --- a/vertx-demo/src/main/kotlin/app/data/domain/role/RoleRepository.kt +++ b/vertx-demo/src/main/kotlin/app/data/domain/role/RoleRepository.kt @@ -1,7 +1,7 @@ package app.data.domain.role import com.google.inject.ImplementedBy -import org.aikrai.vertx.db.Repository +import org.aikrai.vertx.db.wrapper.Repository @ImplementedBy(RoleRepositoryImpl::class) interface RoleRepository : Repository diff --git a/vertx-demo/src/main/kotlin/app/data/domain/role/RoleRepositoryImpl.kt b/vertx-demo/src/main/kotlin/app/data/domain/role/RoleRepositoryImpl.kt index d8ba1d2..761bb62 100644 --- a/vertx-demo/src/main/kotlin/app/data/domain/role/RoleRepositoryImpl.kt +++ b/vertx-demo/src/main/kotlin/app/data/domain/role/RoleRepositoryImpl.kt @@ -2,7 +2,7 @@ package app.data.domain.role import com.google.inject.Inject import io.vertx.sqlclient.SqlClient -import org.aikrai.vertx.db.RepositoryImpl +import org.aikrai.vertx.db.wrapper.RepositoryImpl class RoleRepositoryImpl @Inject constructor( sqlClient: SqlClient diff --git a/vertx-demo/src/main/resources/dbmigration/1.0__initial.sql b/vertx-demo/src/main/resources/dbmigration/1.0__initial.sql new file mode 100644 index 0000000..668e732 --- /dev/null +++ b/vertx-demo/src/main/resources/dbmigration/1.0__initial.sql @@ -0,0 +1,48 @@ +-- apply changes +CREATE TABLE sys_menu ( + menu_id BIGINT DEFAULT 0 NOT NULL, + menu_name VARCHAR(255) DEFAULT '', + parent_id BIGINT DEFAULT 0, + order_num INTEGER DEFAULT 0, + path VARCHAR(255) DEFAULT '', + component VARCHAR(255) DEFAULT '', + menu_type VARCHAR(255) DEFAULT '', + visible VARCHAR(255) DEFAULT '', + perms VARCHAR(255) DEFAULT '', + parent_name VARCHAR(255) DEFAULT '', + children JSONB DEFAULT '{}', + CONSTRAINT pk_sys_menu PRIMARY KEY (menu_id) +); + +CREATE TABLE sys_user ( + user_id BIGINT DEFAULT 0 NOT NULL, + user_name VARCHAR(255) DEFAULT '', + user_type VARCHAR(255) DEFAULT '', + email VARCHAR(255) DEFAULT '', + phone VARCHAR(255) DEFAULT '', + avatar VARCHAR(255) DEFAULT '', + password VARCHAR(255) DEFAULT '', + status INTEGER DEFAULT 0, + del_flag CHAR(1) DEFAULT 0, + login_ip VARCHAR(255) DEFAULT '', + login_date TIMESTAMPTZ, + CONSTRAINT pk_sys_user PRIMARY KEY (user_id) +); + +-- 添加字段注释 +COMMENT ON COLUMN sys_user.user_id IS '用户ID'; + + +CREATE UNIQUE INDEX idx_phone ON sys_user (phone); + + +CREATE TABLE sys_role ( + role_id BIGINT DEFAULT 0, + role_name VARCHAR(255) DEFAULT '', + role_key VARCHAR(255) DEFAULT '', + role_sort INTEGER DEFAULT 0, + data_scope CHAR(1), + status CHAR(1) DEFAULT 0, + del_flag CHAR(1) DEFAULT 0 +); + diff --git a/vertx-demo/src/main/resources/dbmigration/model/1.0__initial.model.xml b/vertx-demo/src/main/resources/dbmigration/model/1.0__initial.model.xml new file mode 100644 index 0000000..1cdeb89 --- /dev/null +++ b/vertx-demo/src/main/resources/dbmigration/model/1.0__initial.model.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vertx-demo/src/test/kotlin/app/GenerateMigration.kt b/vertx-demo/src/test/kotlin/app/GenerateMigration.kt new file mode 100644 index 0000000..49b3538 --- /dev/null +++ b/vertx-demo/src/test/kotlin/app/GenerateMigration.kt @@ -0,0 +1,114 @@ +package app + +import org.aikrai.vertx.db.annotation.TableField +import org.aikrai.vertx.db.annotation.TableId +import org.aikrai.vertx.db.annotation.TableName +import org.aikrai.vertx.db.annotation.TableIndex +import org.aikrai.vertx.db.annotation.EnumValue +import org.aikrai.vertx.db.annotation.TableFieldComment +import org.aikrai.vertx.db.migration.AnnotationMapping +import org.aikrai.vertx.db.migration.ColumnMapping +import org.aikrai.vertx.db.migration.DbMigration +import org.aikrai.vertx.db.migration.SqlAnnotationMapper + +/** + * PostgreSQL数据库迁移生成工具 + */ +object GenerateMigration { + /** + * 生成数据库迁移脚本 + */ + @JvmStatic + fun main(args: Array) { + try { + // 创建SQL注解映射器 + val mapper = createSqlAnnotationMapper() + + // 设置迁移生成器 + val dbMigration = DbMigration.create() + dbMigration.setPathToResources("vertx-demo/src/main/resources") + dbMigration.setEntityPackage("app.data.domain") // 指定实体类包路径 + dbMigration.setSqlAnnotationMapper(mapper) + dbMigration.setGenerateDropStatements(false) // 不生成删除语句 + + // 生成迁移 + val migrationVersion = dbMigration.generateMigration() + println("生成的迁移版本: $migrationVersion") + } catch (e: Exception) { + println("生成迁移失败: ${e.message}") + e.printStackTrace() + } + } + + /** + * 创建SQL注解映射器 + * 根据项目中使用的注解配置映射关系 + */ + private fun createSqlAnnotationMapper(): SqlAnnotationMapper { + val mapper = SqlAnnotationMapper() + + // 设置实体类注解映射 + mapper.entityMapping = AnnotationMapping( + annotationClass = TableName::class, + ) + + // 设置表名映射 + mapper.tableName = AnnotationMapping( + annotationClass = TableName::class, + propertyName = "value" + ) + + // 设置列映射 + mapper.addColumnMapping( + ColumnMapping( + nameMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "value" + ), + typeMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "type" + ), + nullableMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "nullable" + ), + defaultValueMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "default" + ), + lengthMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "length" + ), + uniqueMapping = AnnotationMapping( + annotationClass = TableField::class, + propertyName = "unique" + ), + commentMapping = AnnotationMapping( + annotationClass = TableFieldComment::class, + propertyName = "value" + ) + ) + ) + + // 设置主键映射 + mapper.primaryKeyMapping = AnnotationMapping( + annotationClass = TableId::class, + ) + + // 设置索引映射 + mapper.indexMapping = AnnotationMapping( + annotationClass = TableIndex::class, + propertyName = "name" + ) + + // 设置枚举值映射 + mapper.enumValueMapping = AnnotationMapping( + annotationClass = EnumValue::class, + propertyName = "value" + ) + + return mapper + } +} \ No newline at end of file diff --git a/vertx-demo/src/test/kotlin/app/controller/AuthControllerTest.kt b/vertx-demo/src/test/kotlin/app/controller/AuthControllerTest.kt deleted file mode 100644 index 5b638c5..0000000 --- a/vertx-demo/src/test/kotlin/app/controller/AuthControllerTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -package app.controller - -import app.config.InjectConfig -import app.domain.account.LoginDTO -import app.verticle.MainVerticle -import io.vertx.core.Vertx -import io.vertx.core.json.JsonObject -import io.vertx.ext.web.client.WebClient -import io.vertx.junit5.VertxExtension -import io.vertx.junit5.VertxTestContext -import kotlinx.coroutines.runBlocking -import org.aikrai.vertx.config.Config -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -/** - * AuthControllerTest - */ -@ExtendWith(VertxExtension::class) -class AuthControllerTest { - private var port = 8080 - private var basePath = "/api" - - /** - * Test case for doSign - */ - @Test - fun doSign(vertx: Vertx, testContext: VertxTestContext) { - val client = WebClient.create(vertx) - val loginDTO = LoginDTO("运若汐", "123456") - client.post(port, "127.0.0.1", "$basePath/auth/doSign") - .sendJson(loginDTO) - .onSuccess { response -> - val body = JsonObject(response.body()) - assertEquals("Success", body.getString("message")) - testContext.completeNow() - } - .onFailure { error -> - testContext.failNow(error) - } - } - - /** - * Test case for doLogin - */ - @Test - fun doLogin(vertx: Vertx, testContext: VertxTestContext) { - val client = WebClient.create(vertx) - val loginDTO = LoginDTO("运若汐", "123456") - client.post(port, "127.0.0.1", "$basePath/auth/doLogin") - .sendJson(loginDTO) - .onSuccess { response -> - val body = JsonObject(response.body()) - assertEquals("Success", body.getString("message")) - testContext.completeNow() - } - .onFailure { error -> - testContext.failNow(error) - } - } - - @BeforeEach - fun startServer(vertx: Vertx, testContext: VertxTestContext) { - runBlocking { Config.init(vertx) } - val getIt = InjectConfig.configure(vertx) - val mainVerticle = getIt.getInstance(MainVerticle::class.java) - vertx.deployVerticle(mainVerticle).onComplete { ar -> - if (ar.succeeded()) { - Config.getKey("server.port")?.let { - port = it.toString().toInt() - } - Config.getKey("server.context")?.let { - basePath = "/$it".replace("//", "/") - } - vertx.setTimer(5000) { testContext.completeNow() } - } else { - testContext.failNow(ar.cause()) - } - } - } -} diff --git a/vertx-demo/src/test/resources/application-apifox.yaml b/vertx-demo/src/test/resources/application-apifox.yaml deleted file mode 100644 index e5235a5..0000000 --- a/vertx-demo/src/test/resources/application-apifox.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apifox: - token: APS-xxxxxxxxxxxxxxxxxxxx - projectId: xxxxx - folderId: xxxxx \ No newline at end of file diff --git a/vertx-demo/src/test/resources/application-database.yaml b/vertx-demo/src/test/resources/application-database.yaml deleted file mode 100644 index 5f3bd71..0000000 --- a/vertx-demo/src/test/resources/application-database.yaml +++ /dev/null @@ -1,14 +0,0 @@ -databases: - name: vertx-demo - host: 127.0.0.1 - port: 5432 - username: root - password: 123456 - -redis: - host: 127.0.0.1 - port: 6379 - database: 0 - password: xxx - maxPoolSize: 8 - maxPoolWaiting: 2000 \ No newline at end of file diff --git a/vertx-demo/src/test/resources/application.yaml b/vertx-demo/src/test/resources/application.yaml deleted file mode 100644 index ebb6a31..0000000 --- a/vertx-demo/src/test/resources/application.yaml +++ /dev/null @@ -1,5 +0,0 @@ -server: - port: 8080 - package: app - - 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/db/SqlHelper.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/SqlHelper.kt deleted file mode 100644 index 3f16260..0000000 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/SqlHelper.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.aikrai.vertx.db - -object SqlHelper { - - fun retBool(result: Int?): Boolean { - return null != result && result >= 1 - } -} diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/Annotation.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/Annotation.kt index 297ced8..c560263 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/Annotation.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/annotation/Annotation.kt @@ -34,6 +34,18 @@ annotation class TableField( // val keepGlobalFormat: Boolean = false, // val property: String = "", // val numericScale: String = "" + val type: String = "", + val length: Int = 255, + val nullable: Boolean = true, + val unique: Boolean = false, + val default: String = "" +) + +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD, AnnotationTarget.ANNOTATION_CLASS) +annotation class TableFieldComment( + val value: String = "", ) @MustBeDocumented diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DbMigration.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DbMigration.kt new file mode 100644 index 0000000..0228419 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DbMigration.kt @@ -0,0 +1,123 @@ +package org.aikrai.vertx.db.migration + +import java.io.File + +/** + * 生成PostgreSQL DDL迁移脚本,基于实体类及其注解的变更。 + * + *

+ * 通常在开发人员对模型进行了一组更改后,作为测试阶段的主要方法运行。 + *

+ * + *

示例: 运行生成PostgreSQL迁移脚本

+ * + *
{@code
+ *
+ *    val migration = DbMigration.create()
+ *
+ *    // 可选:指定版本和名称
+ *    migration.setName("添加用户表索引")
+ *
+ *    // 设置实体包路径
+ *    migration.setEntityPackage("org.aikrai.vertx.entity")
+ *
+ *    // 设置SQL注解映射器
+ *    migration.setSqlAnnotationMapper(createMapper())
+ *
+ *    // 生成迁移
+ *    migration.generateMigration()
+ *
+ * }
+ */ +interface DbMigration { + + companion object { + /** + * 创建DbMigration实现实例 + */ + fun create(): DbMigration { + return DefaultDbMigration() + } + } + + /** + * 设置实体类所在的包路径 + */ + fun setEntityPackage(packagePath: String) + + /** + * 设置SQL注解映射器 + */ + fun setSqlAnnotationMapper(mapper: SqlAnnotationMapper) + + /** + * 设置资源文件路径 + *

+ * 默认为Maven风格的'src/main/resources' + */ + fun setPathToResources(pathToResources: String) + + /** + * 设置迁移文件生成的路径(默认为"dbmigration") + */ + fun setMigrationPath(migrationPath: String) + + /** + * 设置模型文件生成的路径(默认为"model") + */ + fun setModelPath(modelPath: String) + + /** + * 设置模型文件后缀(默认为".model.xml") + */ + fun setModelSuffix(modelSuffix: String) + + /** + * 设置迁移的版本号 + */ + fun setVersion(version: String) + + /** + * 设置迁移的名称 + */ + fun setName(name: String) + + /** + * 设置是否输出日志到控制台(默认为true) + */ + fun setLogToSystemOut(logToSystemOut: Boolean) + + /** + * 设置是否生成删除语句(默认为false) + *

+ * 如果设置为false,生成的SQL中将不包含任何DROP语句。 + * 但.model.xml文件中仍会记录删除表和字段的信息。 + *

+ */ + fun setGenerateDropStatements(generateDropStatements: Boolean) + + /** + * 生成下一次迁移SQL脚本和相关模型XML + *

+ * 不会实际运行迁移或DDL脚本,只是生成它们。 + *

+ * + * @return 生成的迁移版本或null(如果没有变更) + */ + fun generateMigration(): String? + + /** + * 生成包含所有变更的"初始"迁移 + *

+ * "初始"迁移只能在尚未对其运行任何先前迁移的数据库上执行和使用。 + *

+ * + * @return 生成的迁移版本 + */ + fun generateInitMigration(): String? + + /** + * 返回迁移主目录 + */ + fun migrationDirectory(): File +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DefaultDbMigration.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DefaultDbMigration.kt new file mode 100644 index 0000000..ab75972 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/DefaultDbMigration.kt @@ -0,0 +1,181 @@ +package org.aikrai.vertx.db.migration + +import java.io.File + +/** + * PostgreSQL数据库迁移工具的默认实现 + */ +class DefaultDbMigration : DbMigration { + private var entityPackage: String = "" + private var sqlAnnotationMapper: SqlAnnotationMapper? = null + private var pathToResources: String = "src/main/resources" + private var migrationPath: String = "dbmigration" + private var modelPath: String = "model" + private var modelSuffix: String = ".model.xml" + private var version: String? = null + private var name: String? = null + private var logToSystemOut: Boolean = true + private var generateDropStatements: Boolean = false + + override fun setEntityPackage(packagePath: String) { + this.entityPackage = packagePath + } + + override fun setSqlAnnotationMapper(mapper: SqlAnnotationMapper) { + this.sqlAnnotationMapper = mapper + } + + override fun setPathToResources(pathToResources: String) { + this.pathToResources = pathToResources + } + + override fun setMigrationPath(migrationPath: String) { + this.migrationPath = migrationPath + } + + override fun setModelPath(modelPath: String) { + this.modelPath = modelPath + } + + override fun setModelSuffix(modelSuffix: String) { + this.modelSuffix = modelSuffix + } + + override fun setVersion(version: String) { + this.version = version + } + + override fun setName(name: String) { + this.name = name + } + + override fun setLogToSystemOut(logToSystemOut: Boolean) { + this.logToSystemOut = logToSystemOut + } + + override fun setGenerateDropStatements(generateDropStatements: Boolean) { + this.generateDropStatements = generateDropStatements + } + + override fun generateMigration(): String? { + validateConfiguration() + + configureMigrationGenerator() + + // 将版本和名称设置到系统属性中,以便SqlMigrationGenerator能够读取 + if (version != null) { + System.setProperty("ddl.migration.version", version!!) + } + if (name != null) { + System.setProperty("ddl.migration.name", name!!) + } + + // 设置是否生成删除语句 + System.setProperty("ddl.migration.generateDropStatements", generateDropStatements.toString()) + + try { + SqlMigrationGenerator.generateMigrations(entityPackage, sqlAnnotationMapper!!) + return version + } catch (e: Exception) { + if (logToSystemOut) { + println("生成迁移失败: ${e.message}") + e.printStackTrace() + } + throw e + } finally { + // 清理系统属性 + if (version != null) { + System.clearProperty("ddl.migration.version") + } + if (name != null) { + System.clearProperty("ddl.migration.name") + } + System.clearProperty("ddl.migration.generateDropStatements") + } + } + + override fun generateInitMigration(): String? { + validateConfiguration() + + configureMigrationGenerator() + + // 将版本和名称设置到系统属性中,以便SqlMigrationGenerator能够读取 + if (version != null) { + System.setProperty("ddl.migration.version", version!!) + } + if (name != null) { + System.setProperty("ddl.migration.name", name!!) + } + + // 设置是否生成删除语句 + System.setProperty("ddl.migration.generateDropStatements", generateDropStatements.toString()) + + try { + // 修改目录结构,强制生成初始迁移 + val modelDir = File("${pathToResources}/${migrationPath}/${modelPath}") + if (modelDir.exists()) { + // 备份原有文件 + val backupDir = File("${pathToResources}/${migrationPath}/${modelPath}_backup_${System.currentTimeMillis()}") + modelDir.renameTo(backupDir) + if (logToSystemOut) { + println("已将现有模型文件备份到: ${backupDir.absolutePath}") + } + } + + // 生成初始迁移 + SqlMigrationGenerator.generateMigrations(entityPackage, sqlAnnotationMapper!!) + return version + } catch (e: Exception) { + if (logToSystemOut) { + println("生成初始迁移失败: ${e.message}") + e.printStackTrace() + } + throw e + } finally { + // 清理系统属性 + if (version != null) { + System.clearProperty("ddl.migration.version") + } + if (name != null) { + System.clearProperty("ddl.migration.name") + } + System.clearProperty("ddl.migration.generateDropStatements") + } + } + + override fun migrationDirectory(): File { + return File("${pathToResources}/${migrationPath}") + } + + /** + * 验证配置,确保必要的配置项已经设置 + */ + private fun validateConfiguration() { + if (entityPackage.isEmpty()) { + throw IllegalStateException("实体包路径未设置,请调用setEntityPackage()") + } + + if (sqlAnnotationMapper == null) { + throw IllegalStateException("SQL注解映射器未设置,请调用setSqlAnnotationMapper()") + } + + if (sqlAnnotationMapper?.entityMapping == null) { + throw IllegalStateException("实体注解映射未设置,请配置entityMapping") + } + } + + /** + * 配置迁移生成器 + */ + private fun configureMigrationGenerator() { + // 设置静态字段,以便SqlMigrationGenerator能够使用配置的路径 + SqlMigrationGenerator.setResourcePath(pathToResources) + SqlMigrationGenerator.setMigrationPath("${pathToResources}/${migrationPath}") + SqlMigrationGenerator.setModelPath("${pathToResources}/${migrationPath}/${modelPath}") + SqlMigrationGenerator.setModelSuffix(modelSuffix) + SqlMigrationGenerator.setLogToSystemOut(logToSystemOut) + + // 同时设置SqlAnnotationMapperGenerator的日志配置 + SqlAnnotationMapperGenerator.setLogToSystemOut(logToSystemOut) + } +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapper.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapper.kt new file mode 100644 index 0000000..92a0ff3 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapper.kt @@ -0,0 +1,90 @@ +package org.aikrai.vertx.db.migration + +import kotlin.reflect.KClass + +/** + * SQL注解映射中间类 + * 用于记录从哪些注解获取SQL生成所需的信息 + */ +class SqlAnnotationMapper { + /** + * 实体类注解映射 + * 用于标识哪个类是实体类 + */ + var entityMapping: AnnotationMapping? = null + + /** + * 表名映射信息 + */ + var tableName: AnnotationMapping? = null + + /** + * 列名映射信息列表 + */ + var columnMappings: MutableList = mutableListOf() + + /** + * 主键映射信息 + */ + var primaryKeyMapping: AnnotationMapping? = null + + /** + * 索引映射信息 + */ + var indexMapping: AnnotationMapping? = null + + /** + * 枚举值映射信息 + */ + var enumValueMapping: AnnotationMapping? = null + + /** + * 其他自定义映射 + */ + var customMappings: MutableMap = mutableMapOf() + + /** + * 添加一个列映射 + */ + fun addColumnMapping(columnMapping: ColumnMapping) { + columnMappings.add(columnMapping) + } + + /** + * 添加一个自定义映射 + */ + fun addCustomMapping(key: String, mapping: AnnotationMapping) { + customMappings[key] = mapping + } +} + +/** + * 注解映射类 + * 记录从哪个注解的哪个属性获取信息 + */ +data class AnnotationMapping( + /** 注解类 */ + val annotationClass: KClass, + /** 注解属性名 */ + val propertyName: String = "" +) + +/** + * 列映射信息 + */ +data class ColumnMapping( + /** 字段名称映射 */ + val nameMapping: AnnotationMapping, + /** 字段类型映射,可选 */ + val typeMapping: AnnotationMapping? = null, + /** 是否可为空映射,可选 */ + val nullableMapping: AnnotationMapping? = null, + /** 默认值映射,可选 */ + val defaultValueMapping: AnnotationMapping? = null, + /** 字段长度映射,可选 */ + val lengthMapping: AnnotationMapping? = null, + /** 是否唯一映射,可选 */ + val uniqueMapping: AnnotationMapping? = null, + /** 字段注释映射,可选 */ + val commentMapping: AnnotationMapping? = null +) \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapperGenerator.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapperGenerator.kt new file mode 100644 index 0000000..91f0da9 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlAnnotationMapperGenerator.kt @@ -0,0 +1,617 @@ +package org.aikrai.vertx.db.migration + +import cn.hutool.core.util.StrUtil +import java.time.LocalDateTime +import kotlin.reflect.KClass + +/** + * SQL注解映射生成器 + * 用于生成和使用SQL注解映射中间类 + */ +class SqlAnnotationMapperGenerator { + + companion object { + // 是否输出日志到控制台 + private var LOG_TO_SYSTEM_OUT = true + + /** + * 设置是否输出日志到控制台 + */ + fun setLogToSystemOut(log: Boolean) { + LOG_TO_SYSTEM_OUT = log + } + + /** + * 记录日志信息 + */ + private fun log(message: String) { + if (LOG_TO_SYSTEM_OUT) { + println("DbMigration> $message") + } + } + + /** + * 从实体类获取SQL信息 + * @param entityClass 实体类 + * @param mapper 注解映射中间类 + * @return SQL信息 + */ + fun extractSqlInfo(entityClass: KClass<*>, mapper: SqlAnnotationMapper): SqlInfo { + val sqlInfo = SqlInfo() + + // 获取表名 - 优先从表名注解中获取,如果没有则使用类名转换 + try { + if (mapper.tableName != null) { + val tableNameMapping = mapper.tableName!! + val annotation = entityClass.annotations.find { + it.annotationClass.qualifiedName == tableNameMapping.annotationClass.qualifiedName + } + + if (annotation != null) { + try { + val method = annotation.javaClass.getMethod(tableNameMapping.propertyName) + val tableName = method.invoke(annotation) as String + + if (tableName.isNotBlank()) { + sqlInfo.tableName = tableName + } else { + // 使用类名转蛇形命名作为表名 + sqlInfo.tableName = StrUtil.toUnderlineCase(entityClass.simpleName ?: "") + } + } catch (e: Exception) { + // 使用类名转蛇形命名作为表名 + sqlInfo.tableName = StrUtil.toUnderlineCase(entityClass.simpleName ?: "") + } + } else { + // 使用类名转蛇形命名作为表名 + sqlInfo.tableName = StrUtil.toUnderlineCase(entityClass.simpleName ?: "") + } + } else { + // 使用类名转蛇形命名作为表名 + sqlInfo.tableName = StrUtil.toUnderlineCase(entityClass.simpleName ?: "") + } + } catch (e: Exception) { + throw IllegalArgumentException("处理实体类 ${entityClass.simpleName} 的表名时出错: ${e.message}", e) + } + + // 获取实体类的所有字段 + val fields = entityClass.java.declaredFields + if (fields.isEmpty()) { + throw IllegalArgumentException("实体类 ${entityClass.simpleName} 没有声明任何字段") + } + + // 创建已处理字段名集合,用于记录已经处理过的字段 + val processedFields = mutableSetOf() + + // 查找实体类中的@Transient注解类 + val transientAnnotationClasses = listOf( + "kotlin.jvm.Transient", + "javax.persistence.Transient", + "jakarta.persistence.Transient", + "java.beans.Transient", + "org.aikrai.vertx.db.annotation.Transient" + ) + + fields.forEach { field -> + try { + // 检查字段是否有@Transient注解,如果有则跳过 + val isTransient = field.annotations.any { annotation -> + transientAnnotationClasses.any { className -> + annotation.annotationClass.qualifiedName == className + } + } + + if (isTransient) { + return@forEach + } + + // 获取列信息 + val columnInfo = ColumnInfo() + var foundColumnMapping = false + + // 处理每个列映射 - 这部分处理带有特定注解的字段 + mapper.columnMappings.forEach { columnMapping -> + val nameAnnotation = field.annotations.find { + it.annotationClass.qualifiedName == columnMapping.nameMapping.annotationClass.qualifiedName + } + + + foundColumnMapping = true + try { + val nameMethod = nameAnnotation?.javaClass?.getMethod(columnMapping.nameMapping.propertyName) + val columnName = nameMethod?.invoke(nameAnnotation) as? String + columnInfo.name = if (columnName.isNullOrEmpty()) StrUtil.toUnderlineCase(field.name) else columnName + + // 处理类型映射 + columnMapping.typeMapping?.let { typeMapping -> + val typeAnnotation = field.annotations.find { + it.annotationClass.qualifiedName == typeMapping.annotationClass.qualifiedName + } + if (typeAnnotation != null) { + try { + val typeMethod = typeAnnotation.javaClass.getMethod(typeMapping.propertyName) + val typeName = typeMethod.invoke(typeAnnotation) as String + columnInfo.type = if (typeName.isEmpty()) { + inferSqlType(field.type, columnInfo, mapper) + } else { + typeName + } + } catch (e: Exception) { + throw IllegalArgumentException("处理字段 ${field.name} 的类型映射时出错: ${e.message}", e) + } + } else { + columnInfo.type = inferSqlType(field.type, columnInfo, mapper) + } + } ?: { + columnInfo.type = inferSqlType(field.type, columnInfo, mapper) + } + + // 检查字段是否为枚举类型,并且有默认初始值 + if (field.type.isEnum) { + try { + // 使字段可访问 + field.isAccessible = true + + // 获取声明类的实例(暂时创建一个实例) + val declaringClass = field.declaringClass + val instance = try { + declaringClass.getDeclaredConstructor().newInstance() + } catch (e: Exception) { + null + } + + if (instance != null) { + // 获取默认枚举值 + val defaultEnumValue = field.get(instance) + if (defaultEnumValue != null) { + // 如果有EnumValue注解的方法,使用它获取枚举值 + if (mapper.enumValueMapping != null) { + val enumValueMethod = field.type.methods.find { method -> + method.annotations.any { + it.annotationClass.qualifiedName == mapper.enumValueMapping!!.annotationClass.qualifiedName + } + } + + if (enumValueMethod != null) { + // 获取枚举值并设置为默认值 + val enumValue = enumValueMethod.invoke(defaultEnumValue) + if (enumValue != null) { + columnInfo.defaultValue = enumValue.toString() + } + } + } + } + } + } catch (e: Exception) { + // 忽略获取默认值失败的情况 + log("获取枚举默认值失败: ${e.message}") + } + } + + // 处理可空映射 + columnMapping.nullableMapping?.let { nullableMapping -> + val nullableAnnotation = field.annotations.find { + it.annotationClass.qualifiedName == nullableMapping.annotationClass.qualifiedName + } + if (nullableAnnotation != null) { + try { + val nullableMethod = nullableAnnotation.javaClass.getMethod(nullableMapping.propertyName) + columnInfo.nullable = nullableMethod.invoke(nullableAnnotation) as Boolean + } catch (e: Exception) { + throw IllegalArgumentException("处理字段 ${field.name} 的可空性映射时出错: ${e.message}", e) + } + } + } + + // 处理长度映射 + columnMapping.lengthMapping?.let { lengthMapping -> + val lengthAnnotation = field.annotations.find { + it.annotationClass.qualifiedName == lengthMapping.annotationClass.qualifiedName + } + if (lengthAnnotation != null) { + try { + val lengthMethod = lengthAnnotation.javaClass.getMethod(lengthMapping.propertyName) + val lengthValue = lengthMethod.invoke(lengthAnnotation) + if (lengthValue is Int) { + columnInfo.length = lengthValue + // 如果是VARCHAR类型,更新类型定义中的长度 + if (columnInfo.type.startsWith("VARCHAR")) { + columnInfo.type = "VARCHAR(${columnInfo.length})" + } + } + } catch (e: Exception) { + throw IllegalArgumentException("处理字段 ${field.name} 的长度映射时出错: ${e.message}", e) + } + } + } + + // 处理唯一性映射 + columnMapping.uniqueMapping?.let { uniqueMapping -> + val uniqueAnnotation = field.annotations.find { + it.annotationClass.qualifiedName == uniqueMapping.annotationClass.qualifiedName + } + if (uniqueAnnotation != null) { + try { + val uniqueMethod = uniqueAnnotation.javaClass.getMethod(uniqueMapping.propertyName) + val uniqueValue = uniqueMethod.invoke(uniqueAnnotation) + if (uniqueValue is Boolean) { + columnInfo.unique = uniqueValue + } + } catch (e: Exception) { + throw IllegalArgumentException("处理字段 ${field.name} 的唯一性映射时出错: ${e.message}", e) + } + } + } + + // 处理注释映射 + columnMapping.commentMapping?.let { commentMapping -> + val commentAnnotation = field.annotations.find { + it.annotationClass.qualifiedName == commentMapping.annotationClass.qualifiedName + } + if (commentAnnotation != null) { + try { + val commentMethod = commentAnnotation.javaClass.getMethod(commentMapping.propertyName) + val commentValue = commentMethod.invoke(commentAnnotation) + if (commentValue is String) { + columnInfo.comment = commentValue + } + } catch (e: Exception) { + throw IllegalArgumentException("处理字段 ${field.name} 的注释映射时出错: ${e.message}", e) + } + } + } + + // 处理默认值映射 - 只有在字段不是枚举或枚举但没有初始值时才处理注解中的默认值 + if (columnInfo.defaultValue.isEmpty()) { + columnMapping.defaultValueMapping?.let { defaultValueMapping -> + val defaultValueAnnotation = field.annotations.find { + it.annotationClass.qualifiedName == defaultValueMapping.annotationClass.qualifiedName + } + if (defaultValueAnnotation != null) { + try { + val defaultValueMethod = + defaultValueAnnotation.javaClass.getMethod(defaultValueMapping.propertyName) + columnInfo.defaultValue = defaultValueMethod.invoke(defaultValueAnnotation) as String + } catch (e: Exception) { + throw IllegalArgumentException("处理字段 ${field.name} 的默认值映射时出错: ${e.message}", e) + } + } + } + } + + sqlInfo.columns.add(columnInfo) + processedFields.add(field.name) // 记录已处理的字段 + } catch (e: Exception) { + throw IllegalArgumentException("处理字段 ${field.name} 时出错: ${e.message}", e) + } + + } + + // 处理主键 + if (mapper.primaryKeyMapping != null) { + val pkMapping = mapper.primaryKeyMapping!! + val pkAnnotation = field.annotations.find { + it.annotationClass.qualifiedName == pkMapping.annotationClass.qualifiedName + } + // 只要字段有@TableId注解,不管属性值如何,都视为主键 + if (pkAnnotation != null) { + // 如果字段还未处理,创建默认列信息 + if (!processedFields.contains(field.name)) { + columnInfo.name = StrUtil.toUnderlineCase(field.name) + columnInfo.type = inferSqlType(field.type, columnInfo, mapper) + columnInfo.isPrimaryKey = true + // 主键不可为空 + columnInfo.nullable = false + sqlInfo.columns.add(columnInfo) + sqlInfo.primaryKeys.add(columnInfo.name) + processedFields.add(field.name) + } else { + // 如果已处理,找到对应列并标记为主键 + val column = + sqlInfo.columns.find { it.name == StrUtil.toUnderlineCase(field.name) || it.name == field.name } + if (column != null) { + column.isPrimaryKey = true + // 主键不可为空 + column.nullable = false + if (!sqlInfo.primaryKeys.contains(column.name)) { + sqlInfo.primaryKeys.add(column.name) + } + } + } + } + } + + // 如果字段未被处理,并且不是static或transient,添加默认处理 + if (!processedFields.contains(field.name) && + !java.lang.reflect.Modifier.isStatic(field.modifiers) && + !java.lang.reflect.Modifier.isTransient(field.modifiers) + ) { + + // 检查字段类型是否可空 + val isNullable = isNullableType(field) + + // 创建默认列信息 + val defaultColumnInfo = ColumnInfo( + name = StrUtil.toUnderlineCase(field.name), + type = "", // 先不设置类型 + nullable = isNullable, + defaultValue = "", + isPrimaryKey = false + ) + + // 设置类型并处理枚举值 + defaultColumnInfo.type = inferSqlType(field.type, defaultColumnInfo, mapper) + + sqlInfo.columns.add(defaultColumnInfo) + processedFields.add(field.name) + } + } catch (e: Exception) { + throw IllegalArgumentException( + "处理实体类 ${entityClass.simpleName} 的字段 ${field.name} 时出错: ${e.message}", + e + ) + } + } + + // 验证结果 + if (sqlInfo.tableName.isEmpty()) { + throw IllegalArgumentException("实体类 ${entityClass.simpleName} 的表名为空,请检查表名注解") + } + + if (sqlInfo.columns.isEmpty()) { + throw IllegalArgumentException("实体类 ${entityClass.simpleName} 没有可用的列信息,请检查列注解") + } + + // 处理表级别的索引注解 + processTableIndexes(entityClass, sqlInfo, mapper) + + return sqlInfo + } + + /** + * 处理表级别的索引注解 + */ + private fun processTableIndexes(entityClass: KClass<*>, sqlInfo: SqlInfo, mapper: SqlAnnotationMapper) { + // 只有当有indexMapping配置时才处理 + if (mapper.indexMapping == null) { + return + } + + try { + val indexMapping = mapper.indexMapping!! + // 查找类上的所有TableIndex注解 + val tableIndexAnnotations = entityClass.annotations.filter { + it.annotationClass.qualifiedName == indexMapping.annotationClass.qualifiedName + } + + // 处理每个TableIndex注解 + tableIndexAnnotations.forEach { annotation -> + try { + // 提取索引信息 + val indexInfo = IndexInfo() + + // 获取索引名称 + val nameMethod = annotation.javaClass.getMethod("name") + val indexName = nameMethod.invoke(annotation) as String + indexInfo.name = + if (indexName.isNotEmpty()) indexName else "idx_${sqlInfo.tableName}_${System.currentTimeMillis()}" + + // 获取唯一性 + val uniqueMethod = annotation.javaClass.getMethod("unique") + indexInfo.unique = uniqueMethod.invoke(annotation) as Boolean + + // 获取并发创建选项 + val concurrentMethod = annotation.javaClass.getMethod("concurrent") + indexInfo.concurrent = concurrentMethod.invoke(annotation) as Boolean + + // 获取列名列表 + val columnNamesMethod = annotation.javaClass.getMethod("columnNames") + val columnNames = columnNamesMethod.invoke(annotation) as Array<*> + indexInfo.columnNames = columnNames.map { it.toString() } + + // 获取自定义定义 + val definitionMethod = annotation.javaClass.getMethod("definition") + val definition = definitionMethod.invoke(annotation) as String + indexInfo.definition = definition + + // 只有当至少有一个列名或自定义定义时才添加索引 + if (indexInfo.columnNames.isNotEmpty() || indexInfo.definition.isNotEmpty()) { + sqlInfo.indexes.add(indexInfo) + } + } catch (e: Exception) { + // 处理单个索引注解失败时记录错误但继续处理其他索引 + log("处理索引注解时出错: ${e.message}") + } + } + } catch (e: Exception) { + // 处理索引注解整体失败时记录错误 + log("处理表 ${sqlInfo.tableName} 的索引注解时出错: ${e.message}") + } + } + + /** + * 根据Java类型推断SQL类型 + */ + private fun inferSqlType( + javaType: Class<*>, + columnInfo: ColumnInfo? = null, + mapper: SqlAnnotationMapper? = null + ): String { + val sqlType = when { + javaType == String::class.java -> { + if (columnInfo != null) { + "VARCHAR(${columnInfo.length})" + } else { + "VARCHAR(255)" + } + } + + javaType == Int::class.java || javaType == Integer::class.java -> "INTEGER" + javaType == Long::class.java || javaType == java.lang.Long::class.java -> "BIGINT" + javaType == Double::class.java || javaType == java.lang.Double::class.java -> "DOUBLE PRECISION" + javaType == Float::class.java || javaType == java.lang.Float::class.java -> "REAL" + javaType == Boolean::class.java || javaType == java.lang.Boolean::class.java -> "BOOLEAN" + javaType == Char::class.java || javaType == Character::class.java -> "CHAR(1)" + javaType == java.util.Date::class.java || javaType == java.sql.Date::class.java -> "DATE" + javaType == java.sql.Timestamp::class.java -> "TIMESTAMPTZ" + javaType == LocalDateTime::class.java -> "TIMESTAMPTZ" + javaType == ByteArray::class.java -> "BYTEA" + javaType.name.contains("Map") || javaType.name.contains("HashMap") -> "JSONB" + javaType.name.contains("List") || javaType.name.contains("ArrayList") -> "JSONB" + javaType.name.contains("Set") || javaType.name.contains("HashSet") -> "JSONB" + javaType.name.endsWith("DTO") || javaType.name.endsWith("Dto") -> "JSONB" + javaType.name.contains("Json") || javaType.name.contains("JSON") -> "JSONB" + javaType.isEnum -> { + // 处理枚举类型,提取枚举值并保存到columnInfo中 + if (columnInfo != null) { + try { + // 获取枚举类中的所有枚举常量 + val enumConstants = javaType.enumConstants + if (enumConstants != null && enumConstants.isNotEmpty()) { + // 查找带有EnumValue注解的方法 + val enumValues = if (mapper?.enumValueMapping != null) { + val enumValueMethod = javaType.methods.find { method -> + method.annotations.any { + it.annotationClass.qualifiedName == mapper.enumValueMapping!!.annotationClass.qualifiedName + } + } + + if (enumValueMethod != null) { + // 使用EnumValue标注的方法获取枚举值 + enumConstants.map { enumValueMethod.invoke(it).toString() } + } else { + // 如果没有找到EnumValue注解的方法,使用枚举名称 + enumConstants.map { (it as Enum<*>).name } + } + } else { + // 默认使用枚举的toString() + enumConstants.map { it.toString() } + } + columnInfo.enumValues = enumValues + + // 如果字段有默认值,确保默认值是通过带EnumValue的方法获取的 + val defaultEnumValue = javaType.enumConstants.find { (it as Enum<*>).name == columnInfo.defaultValue } + if (defaultEnumValue != null && mapper?.enumValueMapping != null) { + val enumValueMethod = javaType.methods.find { method -> + method.annotations.any { + it.annotationClass.qualifiedName == mapper.enumValueMapping!!.annotationClass.qualifiedName + } + } + if (enumValueMethod != null) { + // 更新默认值为EnumValue方法的返回值 + columnInfo.defaultValue = enumValueMethod.invoke(defaultEnumValue).toString() + } + } + } + } catch (e: Exception) { + // 忽略枚举值提取失败的情况 + log("提取枚举值失败: ${e.message}") + } + } + + // 根据枚举值的类型决定SQL类型 + if (columnInfo != null && columnInfo.enumValues?.isNotEmpty() == true) { + val firstValue = columnInfo.enumValues!!.first() + val enumType = when { + // 尝试将值转换为数字,判断是否是数值型枚举 + firstValue.toIntOrNull() != null -> "INTEGER" + firstValue.toLongOrNull() != null -> "BIGINT" + firstValue.toDoubleOrNull() != null -> "DOUBLE PRECISION" + // 如果值很短,使用CHAR + firstValue.length <= 1 -> "CHAR(1)" + // 找出最长的枚举值,并基于此设置VARCHAR长度 + else -> { + val maxLength = columnInfo.enumValues!!.maxBy { it.length }.length + val safeLength = maxLength + 10 // 增加一些余量 + "VARCHAR($safeLength)" + } + } + enumType + } else { + "VARCHAR(50)" // 默认回退 + } + } + + else -> "VARCHAR(255)" + } + + return sqlType + } + + /** + * 判断字段类型是否可空 + */ + private fun isNullableType(field: java.lang.reflect.Field): Boolean { + // 检查字段类型名称中是否包含Nullable标记 + val typeName = field.genericType.typeName + + // 1. 先检查字段类型名是否包含"?",这是Kotlin可空类型的标志 + if (typeName.contains("?")) { + return true + } + + // 2. 检查字段是否为Java原始类型,这些类型不可为空 + if (field.type.isPrimitive) { + return false + } + + // 3. 通过Java反射获取字段的声明可空性 + try { + // 检查是否有@Nullable相关注解 + val hasNullableAnnotation = field.annotations.any { + val name = it.annotationClass.qualifiedName ?: "" + name.contains("Nullable") || name.contains("nullable") + } + + if (hasNullableAnnotation) { + return true + } + } catch (e: Exception) { + // 忽略注解检查错误 + } + + // 4. 检查字段的类型并判断其可空性 + // Kotlin的String类型不可为空,而Java的String类型可为空 + if (field.type == String::class.java) { + // 尝试通过字段的初始值判断 + try { + field.isAccessible = true + // 如果是非静态字段且初始值为null,则可能为可空类型 + if (!java.lang.reflect.Modifier.isStatic(field.modifiers)) { + // 对于具有初始值的非静态字段,如果初始值为非null字符串,则认为是非可空类型 + // 如果字段名以OrNull或Optional结尾,认为是可空类型 + if (field.name.endsWith("OrNull") || field.name.endsWith("Optional")) { + return true + } + + // 对于Kotlin中的非空String类型,如果有初始值"",则不可为空 + // 检查是否为Kotlin类型 + val isKotlinType = typeName.startsWith("kotlin.") + if (isKotlinType) { + return false // Kotlin中的String类型不可为空 + } + } + } catch (e: Exception) { + // 忽略访问字段值的错误 + } + } + + // 5. 检查字段是否为其他Kotlin基本类型且非可空 + if (field.type == Int::class.java || + field.type == Long::class.java || + field.type == Boolean::class.java || + field.type == Float::class.java || + field.type == Double::class.java || + field.type == Char::class.java || + field.type == Byte::class.java || + field.type == Short::class.java + ) { + // Kotlin基本类型不带?就不可为空 + return false + } + + // 6. 默认情况: 引用类型默认认为是可空的 + return true + } + } +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlGenerator.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlGenerator.kt new file mode 100644 index 0000000..86a68ab --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlGenerator.kt @@ -0,0 +1,333 @@ +package org.aikrai.vertx.db.migration + +import kotlin.reflect.KClass + +/** + * PostgreSQL SQL生成工具类 + */ +class SqlGenerator { + companion object { + /** + * 生成创建表SQL + * @param entityClass 实体类 + * @param mapper 注解映射中间类 + * @return 创建表SQL语句 + */ + fun generateCreateTableSql(entityClass: KClass<*>, mapper: SqlAnnotationMapper): String { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + val tableName = sqlInfo.tableName + val columns = sqlInfo.columns + + if (tableName.isEmpty() || columns.isEmpty()) { + throw IllegalArgumentException("无法生成SQL,表名或列信息为空") + } + + val sb = StringBuilder() + sb.append("CREATE TABLE $tableName (\n") + + // 添加列定义 - 改为ANSI标准格式 + val columnDefinitions = columns.map { column -> + // 特殊处理一些常见约定字段 + val specialFieldDefaults = getSpecialFieldDefaults(column.name, column.type) + var defaultValue = "" + + // 处理默认值 + if (column.defaultValue.isNotEmpty()) { + // 对于时间戳类型字段,特殊处理NOW()函数作为默认值 + if (column.type.contains("TIMESTAMP") && column.defaultValue.equals("now()", ignoreCase = true)) { + defaultValue = " DEFAULT 'now()'" + } else if (column.enumValues != null && column.enumValues!!.isNotEmpty()) { + // 对于枚举类型字段,直接使用默认值 + // 如果是数字,就不带引号,否则加上引号 + val isNumeric = column.defaultValue.matches(Regex("^[0-9]+$")) + if (isNumeric) { + defaultValue = " DEFAULT ${column.defaultValue}" + } else { + defaultValue = " DEFAULT '${column.defaultValue}'" + } + } else { + defaultValue = " DEFAULT ${column.defaultValue}" + } + } else if (specialFieldDefaults.isNotEmpty()) { + defaultValue = specialFieldDefaults + } else { + // 为一些常见类型提供合理的默认值 + when { + column.type.contains("VARCHAR") -> defaultValue = " DEFAULT ''" + column.type == "INTEGER" || column.type == "BIGINT" -> defaultValue = " DEFAULT 0" + column.type == "BOOLEAN" -> defaultValue = " DEFAULT false" + column.type.contains("JSON") -> defaultValue = " DEFAULT '{}'" + } + } + + val nullable = if (column.nullable) "" else " NOT NULL" + // 移除内联注释,改用COMMENT ON语句 + " ${column.name} ${column.type}$defaultValue$nullable" + } + + sb.append(columnDefinitions.joinToString(",\n")) + + // 添加唯一约束 + val uniqueColumns = columns.filter { it.unique && !it.isPrimaryKey } + for (column in uniqueColumns) { + sb.append(",\n CONSTRAINT uk_${tableName}_${column.name} UNIQUE (${column.name})") + } + + // 添加枚举约束 - 识别枚举类型的列并添加CHECK约束 + val enumColumns = columns.filter { it.type.contains("VARCHAR") && it.enumValues != null && it.enumValues!!.isNotEmpty() } + for (column in enumColumns) { + if (column.enumValues != null && column.enumValues!!.isNotEmpty()) { + // 检查枚举值是否都是数字 + val allNumeric = column.enumValues!!.all { it.matches(Regex("^[0-9]+$")) } + + sb.append(",\n CONSTRAINT ck_${tableName}_${column.name} CHECK ( ${column.name} in (") + if (allNumeric) { + // 如果全是数字,不需要加引号 + sb.append(column.enumValues!!.joinToString(",")) + } else { + // 否则加上引号 + sb.append(column.enumValues!!.joinToString(",") { "'$it'" }) + } + sb.append("))") + } + } + + // 添加主键约束 + if (sqlInfo.primaryKeys.isNotEmpty()) { + sb.append(",\n CONSTRAINT pk_$tableName PRIMARY KEY (${sqlInfo.primaryKeys.joinToString(", ")})") + } + + sb.append("\n);") + + // 添加字段注释 - 使用PostgreSQL的COMMENT ON语句 + val columnsWithComment = columns.filter { it.comment.isNotEmpty() } + if (columnsWithComment.isNotEmpty()) { + sb.append("\n\n-- 添加字段注释\n") + for (column in columnsWithComment) { + sb.append("COMMENT ON COLUMN ${tableName}.${column.name} IS '${column.comment.replace("'", "''")}';") + sb.append("\n") + } + } + + // 添加索引创建语句 + val indexSql = generateCreateIndexSql(sqlInfo) + if (indexSql.isNotEmpty()) { + sb.append("\n\n") + sb.append(indexSql) + } + + return sb.toString() + } + + /** + * 生成创建索引的SQL语句 + * @param sqlInfo SQL信息 + * @return 创建索引SQL语句 + */ + private fun generateCreateIndexSql(sqlInfo: SqlInfo): String { + val sb = StringBuilder() + + sqlInfo.indexes.forEach { index -> + if (index.columnNames.isNotEmpty() || index.definition.isNotEmpty()) { + sb.append("CREATE ") + + if (index.unique) { + sb.append("UNIQUE ") + } + + sb.append("INDEX ") + + if (index.concurrent) { + sb.append("CONCURRENTLY ") + } + + sb.append("${index.name} ON ${sqlInfo.tableName}") + + if (index.definition.isNotEmpty()) { + // 使用自定义索引定义 + sb.append(" ${index.definition}") + } else { + // 使用列名列表 + sb.append(" (${index.columnNames.joinToString(", ")})") + } + + sb.append(";\n") + } + } + + return sb.toString() + } + + /** + * 获取特殊字段的默认值定义 + */ + private fun getSpecialFieldDefaults(fieldName: String, fieldType: String): String { + return when { + // 版本字段 + fieldName == "version" && (fieldType == "INTEGER" || fieldType == "BIGINT") -> + " DEFAULT 0" + // 创建时间字段 + fieldName == "created" || fieldName == "create_time" || fieldName == "creation_time" || fieldName == "created_at" -> { + if (fieldType.contains("TIMESTAMP")) " DEFAULT 'now()'" else "" + } + // 更新时间字段 + fieldName == "updated" || fieldName == "update_time" || fieldName == "last_update" || fieldName == "updated_at" -> { + if (fieldType.contains("TIMESTAMP")) " DEFAULT 'now()'" else "" + } + // 是否删除标记 + fieldName == "deleted" || fieldName == "is_deleted" || fieldName == "del_flag" -> { + if (fieldType == "BOOLEAN") " DEFAULT false" + else if (fieldType.contains("INTEGER") || fieldType.contains("CHAR")) " DEFAULT 0" + else "" + } + // 状态字段 + fieldName == "status" || fieldName == "state" -> { + if (fieldType.contains("VARCHAR")) { + // 默认为空值,实际值将通过字段的默认值处理 + "" + } else { + // 对于数字类型状态默认为0 + " DEFAULT 0" + } + } + else -> "" + } + } + + /** + * 生成插入SQL + * @param entityClass 实体类 + * @param mapper 注解映射中间类 + * @return 插入SQL语句模板 + */ + fun generateInsertSql(entityClass: KClass<*>, mapper: SqlAnnotationMapper): String { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + val tableName = sqlInfo.tableName + val columns = sqlInfo.columns + + if (tableName.isEmpty() || columns.isEmpty()) { + throw IllegalArgumentException("无法生成SQL,表名或列信息为空") + } + + val columnNames = columns.map { it.name } + val placeholders = columns.mapIndexed { index, _ -> "$$${index + 1}" } + + return "INSERT INTO $tableName (${columnNames.joinToString(", ")}) VALUES (${placeholders.joinToString(", ")});" + } + + /** + * 生成更新SQL + * @param entityClass 实体类 + * @param mapper 注解映射中间类 + * @return 更新SQL语句模板 + */ + fun generateUpdateSql(entityClass: KClass<*>, mapper: SqlAnnotationMapper): String { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + val tableName = sqlInfo.tableName + val columns = sqlInfo.columns.filter { !it.isPrimaryKey } + val primaryKeys = sqlInfo.columns.filter { it.isPrimaryKey } + + if (tableName.isEmpty() || columns.isEmpty() || primaryKeys.isEmpty()) { + throw IllegalArgumentException("无法生成SQL,表名、列信息或主键为空") + } + + val sb = StringBuilder() + sb.append("UPDATE $tableName SET ") + + // 设置列 + val setStatements = columns.mapIndexed { index, column -> + "${column.name} = $$${index + 1}" + } + sb.append(setStatements.joinToString(", ")) + + // 添加条件 + sb.append(" WHERE ") + val whereStatements = primaryKeys.mapIndexed { index, pk -> + "${pk.name} = $$${columns.size + index + 1}" + } + sb.append(whereStatements.joinToString(" AND ")) + + sb.append(";") + return sb.toString() + } + + /** + * 生成删除SQL + * @param entityClass 实体类 + * @param mapper 注解映射中间类 + * @return 删除SQL语句模板 + */ + fun generateDeleteSql(entityClass: KClass<*>, mapper: SqlAnnotationMapper): String { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + val tableName = sqlInfo.tableName + val primaryKeys = sqlInfo.columns.filter { it.isPrimaryKey } + + if (tableName.isEmpty() || primaryKeys.isEmpty()) { + throw IllegalArgumentException("无法生成SQL,表名或主键为空") + } + + val sb = StringBuilder() + sb.append("DELETE FROM $tableName WHERE ") + + // 添加条件 + val whereStatements = primaryKeys.mapIndexed { index, pk -> + "${pk.name} = $$${index + 1}" + } + sb.append(whereStatements.joinToString(" AND ")) + + sb.append(";") + return sb.toString() + } + + /** + * 生成查询SQL + * @param entityClass 实体类 + * @param mapper 注解映射中间类 + * @return 查询SQL语句 + */ + fun generateSelectSql(entityClass: KClass<*>, mapper: SqlAnnotationMapper): String { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + val tableName = sqlInfo.tableName + val columns = sqlInfo.columns + + if (tableName.isEmpty() || columns.isEmpty()) { + throw IllegalArgumentException("无法生成SQL,表名或列信息为空") + } + + val columnNames = columns.map { it.name } + + return "SELECT ${columnNames.joinToString(", ")} FROM $tableName;" + } + + /** + * 生成根据主键查询SQL + * @param entityClass 实体类 + * @param mapper 注解映射中间类 + * @return 根据主键查询SQL语句 + */ + fun generateSelectByPrimaryKeySql(entityClass: KClass<*>, mapper: SqlAnnotationMapper): String { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + val tableName = sqlInfo.tableName + val columns = sqlInfo.columns + val primaryKeys = sqlInfo.columns.filter { it.isPrimaryKey } + + if (tableName.isEmpty() || columns.isEmpty() || primaryKeys.isEmpty()) { + throw IllegalArgumentException("无法生成SQL,表名、列信息或主键为空") + } + + val columnNames = columns.map { it.name } + + val sb = StringBuilder() + sb.append("SELECT ${columnNames.joinToString(", ")} FROM $tableName WHERE ") + + // 添加条件 + val whereStatements = primaryKeys.mapIndexed { index, pk -> + "${pk.name} = $$${index + 1}" + } + sb.append(whereStatements.joinToString(" AND ")) + + sb.append(";") + return sb.toString() + } + } +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlInfo.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlInfo.kt new file mode 100644 index 0000000..6dd32d1 --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlInfo.kt @@ -0,0 +1,38 @@ +package org.aikrai.vertx.db.migration + +/** + * SQL信息类 + * 存储从实体类中提取的SQL相关信息 + */ +data class SqlInfo( + var tableName: String = "", + val columns: MutableList = mutableListOf(), + val primaryKeys: MutableList = mutableListOf(), + val indexes: MutableList = mutableListOf() +) + +/** + * 列信息类 + */ +data class ColumnInfo( + var name: String = "", + var type: String = "", + var nullable: Boolean = true, + var defaultValue: String = "", + var isPrimaryKey: Boolean = false, + var enumValues: List? = null, + var unique: Boolean = false, + var length: Int = 255, + var comment: String = "" +) + +/** + * 索引信息类 + */ +data class IndexInfo( + var name: String = "", + var columnNames: List = listOf(), + var unique: Boolean = false, + var concurrent: Boolean = false, + var definition: String = "" +) \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlMigrationGenerator.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlMigrationGenerator.kt new file mode 100644 index 0000000..8ebef3b --- /dev/null +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/migration/SqlMigrationGenerator.kt @@ -0,0 +1,1157 @@ +package org.aikrai.vertx.db.migration + +import org.reflections.Reflections +import org.w3c.dom.Document +import org.w3c.dom.Element +import java.io.File +import java.io.FileOutputStream +import java.nio.file.Files +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import kotlin.reflect.KClass + +/** + * PostgreSQL 数据库迁移工具 + * 根据实体类及其注解生成数据库迁移脚本和模型文件 + */ +class SqlMigrationGenerator { + companion object { + private var RESOURCE_PATH = "src/main/resources" + private var MIGRATION_PATH = "$RESOURCE_PATH/dbmigration" + private var MODEL_PATH = "$MIGRATION_PATH/model" + private var MODEL_SUFFIX = ".model.xml" + private const val INITIAL_VERSION = "1.0" + private const val INITIAL_SUFFIX = "__initial" + private var LOG_TO_SYSTEM_OUT = true + + /** + * 设置资源目录路径 + */ + fun setResourcePath(path: String) { + RESOURCE_PATH = path + // 更新依赖于资源路径的其他路径 + if (!MIGRATION_PATH.startsWith(path)) { + MIGRATION_PATH = "$path/dbmigration" + } + if (!MODEL_PATH.startsWith(MIGRATION_PATH)) { + MODEL_PATH = "$MIGRATION_PATH/model" + } + } + + /** + * 设置迁移目录路径 + */ + fun setMigrationPath(path: String) { + MIGRATION_PATH = path + // 更新依赖于迁移路径的模型路径 + if (!MODEL_PATH.startsWith(path)) { + MODEL_PATH = "$path/model" + } + } + + /** + * 设置模型目录路径 + */ + fun setModelPath(path: String) { + MODEL_PATH = path + } + + /** + * 设置模型文件后缀 + */ + fun setModelSuffix(suffix: String) { + MODEL_SUFFIX = suffix + } + + /** + * 设置是否输出日志到控制台 + */ + fun setLogToSystemOut(log: Boolean) { + LOG_TO_SYSTEM_OUT = log + } + + /** + * 记录日志信息 + */ + private fun log(message: String) { + if (LOG_TO_SYSTEM_OUT) { + println("DbMigration> $message") + } + } + + /** + * 生成数据库迁移脚本 + * @param entityClasses 实体类列表 + * @param mapper 注解映射中间类 + */ + fun generateMigrations(entityPackage: String, mapper: SqlAnnotationMapper) { + // 确保日志路径存在 + createDirectories() + + log("开始扫描实体类...") + val entityClasses = scanEntityClasses(mapper, entityPackage) + + if (entityClasses.isEmpty()) { + throw IllegalArgumentException("未找到任何实体类,请检查包路径: $entityPackage") + } + + log("找到 ${entityClasses.size} 个实体类,准备验证...") + + // 先验证所有实体类是否能够正确解析,如果有错误就立刻抛出异常 + validateEntityClasses(entityClasses, mapper) + + // 检查是否是初始迁移 + val isInitialMigration = isInitialMigration() + + if (isInitialMigration) { + log("开始生成初始迁移...") + generateInitialMigration(entityClasses, mapper) + } else { + log("开始生成差异迁移...") + generateDiffMigration(entityClasses, mapper) + } + } + + /** + * 验证所有实体类是否可以正确解析SQL信息 + */ + private fun validateEntityClasses(entityClasses: List>, mapper: SqlAnnotationMapper) { + val errorMessages = mutableListOf() + + entityClasses.forEach { entityClass -> + try { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + + // 验证表名 + if (sqlInfo.tableName.isEmpty()) { + errorMessages.add("实体类 ${entityClass.simpleName} 的表名为空") + } + + // 验证是否有列 + if (sqlInfo.columns.isEmpty()) { + errorMessages.add("实体类 ${entityClass.simpleName} 没有任何列信息") + } + } catch (e: Exception) { + // 记录错误信息 + errorMessages.add("验证实体类 ${entityClass.simpleName} 失败: ${e.message}") + } + } + + // 如果有任何错误,抛出异常并停止生成 + if (errorMessages.isNotEmpty()) { + val errorMessage = "验证实体类时发现以下错误:\n" + errorMessages.joinToString("\n") + throw IllegalArgumentException(errorMessage) + } + + log("所有实体类验证通过,可以生成迁移") + } + + /** + * 扫描包路径下标记了@TableName注解的实体类 + */ + private fun scanEntityClasses(mapper: SqlAnnotationMapper, packagePath: String): List> { + val entityAnnotationClass = mapper.entityMapping?.annotationClass?.java + ?: throw IllegalStateException("实体类注解映射未设置,请配置SqlAnnotationMapper.entityMapping") + val reflections = Reflections(packagePath) + val entityClasses = reflections.getTypesAnnotatedWith(entityAnnotationClass) + return entityClasses.map { it.kotlin } + } + + /** + * 创建必要的目录 + */ + private fun createDirectories() { + Files.createDirectories(Paths.get(MIGRATION_PATH)) + Files.createDirectories(Paths.get(MODEL_PATH)) + } + + /** + * 检查是否是初始迁移 + */ + private fun isInitialMigration(): Boolean { + val modelDir = File(MODEL_PATH) + return modelDir.listFiles()?.isEmpty() ?: true + } + + /** + * 生成初始迁移 + */ + private fun generateInitialMigration(entityClasses: List>, mapper: SqlAnnotationMapper) { + val version = "$INITIAL_VERSION$INITIAL_SUFFIX" + val sqlBuilder = StringBuilder() + sqlBuilder.append("-- apply changes\n") + + // 创建XML模型文档 + val modelDocument = createModelDocument() + val changeSetElement = modelDocument.createElement("changeSet") + changeSetElement.setAttribute("type", "apply") + modelDocument.documentElement.appendChild(changeSetElement) + + // 为每个实体类生成创建表SQL + entityClasses.forEach { entityClass -> + try { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + val createTableSql = SqlGenerator.generateCreateTableSql(entityClass, mapper) + + // 添加到SQL文件 + sqlBuilder.append("$createTableSql\n\n") + + // 添加到模型文件 + addCreateTableToModel(modelDocument, changeSetElement, sqlInfo) + + log("已生成 ${entityClass.simpleName} 的创建表SQL") + } catch (e: Exception) { + // 由于前面已经验证过所有实体类,这里不应该再有错误 + // 但为了健壮性,还是添加错误处理 + log("警告: 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}") + sqlBuilder.append("-- 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}\n\n") + } + } + + // 写入SQL文件 + val sqlFileName = "$MIGRATION_PATH/$version.sql" + File(sqlFileName).writeText(sqlBuilder.toString()) + log("生成SQL文件: $sqlFileName") + + // 写入模型文件 + val modelFileName = "$MODEL_PATH/$version$MODEL_SUFFIX" + writeModelToFile(modelDocument, modelFileName) + log("生成模型文件: $modelFileName") + + log("初始迁移生成完成: $version") + } + + /** + * 生成差异迁移 + */ + private fun generateDiffMigration(entityClasses: List>, mapper: SqlAnnotationMapper) { + // 获取最新的模型文件 + val latestModelFile = findLatestModelFile() + if (latestModelFile == null) { + log("未找到现有模型文件,将生成初始迁移") + generateInitialMigration(entityClasses, mapper) + return + } + + log("找到最新模型文件: ${latestModelFile.name}") + + // 解析最新的模型文件并构建完整模型 + val allTables = buildFullModel(latestModelFile) + + // 获取当前版本号和生成新版本号 + val currentVersion = extractVersionFromFileName(latestModelFile.name) + if (!currentVersion.contains(".")) { + log("错误: 提取的当前版本号 '$currentVersion' 格式不正确,将使用默认的 '1.0'") + val nextVersion = findNextVersionNumber("1.0") + log("基于默认版本找到下一个版本号: $nextVersion") + continueWithMigration(nextVersion, allTables, entityClasses, mapper) + return + } + + // 检查是否存在已有的版本号,找出当前最大版本号 + val nextVersion = findNextVersionNumber(currentVersion) + log("当前版本: $currentVersion, 新版本: $nextVersion") + + // 继续生成迁移 + continueWithMigration(nextVersion, allTables, entityClasses, mapper) + } + + /** + * 继续生成迁移的逻辑 + */ + private fun continueWithMigration( + nextVersion: String, + allTables: Map, + entityClasses: List>, + mapper: SqlAnnotationMapper + ) { + // 生成当前实体类的模型信息 + val currentTables = mutableMapOf() + entityClasses.forEach { entityClass -> + try { + val sqlInfo = SqlAnnotationMapperGenerator.extractSqlInfo(entityClass, mapper) + currentTables[sqlInfo.tableName] = sqlInfo + log("已提取 ${entityClass.simpleName} 的模型信息,表名: ${sqlInfo.tableName}") + } catch (e: Exception) { + // 由于前面已验证,这里不应该再有错误 + log("警告: 处理实体类 ${entityClass.simpleName} 时出错: ${e.message}") + } + } + + // 比较差异并生成SQL + log("比较数据库结构差异...") + val diffResult = compareTables(allTables, currentTables) + + // 如果没有差异,不生成迁移文件 + if (diffResult.isEmpty()) { + log("没有发现数据库结构变化,不生成迁移文件") + return + } + + log("发现以下变更:") + if (diffResult.tablesToCreate.isNotEmpty()) { + log("- 新增表: ${diffResult.tablesToCreate.map { it.tableName }.joinToString(", ")}") + } + if (diffResult.tablesToDrop.isNotEmpty()) { + log("- 删除表: ${diffResult.tablesToDrop.joinToString(", ")}") + } + if (diffResult.columnsToAdd.isNotEmpty()) { + log("- 新增列: ${diffResult.columnsToAdd.entries.joinToString(", ") { "${it.key}(${it.value.size}列)" }}") + } + if (diffResult.columnsToDrop.isNotEmpty()) { + log("- 删除列: ${diffResult.columnsToDrop.entries.joinToString(", ") { "${it.key}(${it.value.size}列)" }}") + } + if (diffResult.columnsToAlter.isNotEmpty()) { + log("- 修改列: ${diffResult.columnsToAlter.entries.joinToString(", ") { "${it.key}(${it.value.size}列)" }}") + } + + // 创建新的模型文档,只包含变更 + val modelDocument = createModelDocument() + val changeSetElement = modelDocument.createElement("changeSet") + changeSetElement.setAttribute("type", "apply") + modelDocument.documentElement.appendChild(changeSetElement) + + val sqlBuilder = StringBuilder() + sqlBuilder.append("-- apply changes\n") + + // 处理创建表 + processCreateTables(diffResult.tablesToCreate, sqlBuilder, modelDocument, changeSetElement) + + // 处理删除表 + processDropTables(diffResult.tablesToDrop, sqlBuilder, modelDocument, changeSetElement) + + // 处理添加列 + processAddColumns(diffResult.columnsToAdd, sqlBuilder, modelDocument, changeSetElement) + + // 处理删除列 + processDropColumns(diffResult.columnsToDrop, sqlBuilder, modelDocument, changeSetElement) + + // 处理修改列 + processAlterColumns(diffResult.columnsToAlter, sqlBuilder, modelDocument, changeSetElement) + + // 写入SQL文件 + val sqlFileName = "$MIGRATION_PATH/$nextVersion.sql" + val sqlFile = File(sqlFileName) + if (sqlFile.exists()) { + log("警告: SQL文件 $sqlFileName 已存在,将被覆盖") + } + sqlFile.writeText(sqlBuilder.toString()) + log("生成SQL文件: $sqlFileName") + + // 写入模型文件 + val modelFileName = "$MODEL_PATH/$nextVersion$MODEL_SUFFIX" + val modelFile = File(modelFileName) + if (modelFile.exists()) { + log("警告: 模型文件 $modelFileName 已存在,将被覆盖") + } + writeModelToFile(modelDocument, modelFileName) + log("生成模型文件: $modelFileName") + + log("差异迁移生成完成: $nextVersion") + } + + /** + * 查找下一个可用的版本号 + */ + private fun findNextVersionNumber(baseVersion: String): String { + // 打印基础版本,便于调试 + log("查找下一个版本号,基础版本: $baseVersion") + + val parts = baseVersion.split(".") + if (parts.size != 2) { + log("无效的基础版本格式,使用默认值 1.1") + return "1.1" + } + + val major = parts[0].toInt() + val minor = parts[1].toInt() + + // 扫描现有文件,找出最大版本号 + val modelDir = File(MODEL_PATH) + val sqlDir = File(MIGRATION_PATH) + + // 获取所有版本号 + val existingVersions = mutableSetOf() + + // 检查模型文件 + val modelFiles = modelDir.listFiles() + if (modelFiles != null) { + for (file in modelFiles) { + if (file.isFile && file.name.endsWith(MODEL_SUFFIX)) { + val fileVersion = extractVersionFromFileName(file.name) + if (!fileVersion.contains("__")) { // 排除初始版本 + existingVersions.add(fileVersion) + log("发现模型文件版本: $fileVersion (${file.name})") + } + } + } + } + + // 检查SQL文件 + val sqlFiles = sqlDir.listFiles() + if (sqlFiles != null) { + for (file in sqlFiles) { + if (file.isFile && file.name.endsWith(".sql") && !file.name.contains("__")) { + val fileVersion = extractVersionFromFileName(file.name) + existingVersions.add(fileVersion) + log("发现SQL文件版本: $fileVersion (${file.name})") + } + } + } + + // 找出最大版本号 + var maxMajor = major + var maxMinor = minor + + for (version in existingVersions) { + try { + val vParts = version.split(".") + if (vParts.size == 2) { + val vMajor = vParts[0].toInt() + val vMinor = vParts[1].toInt() + + if (vMajor > maxMajor || (vMajor == maxMajor && vMinor > maxMinor)) { + maxMajor = vMajor + maxMinor = vMinor + log("更新最大版本: $vMajor.$vMinor") + } + } + } catch (e: NumberFormatException) { + log("警告: 版本号 '$version' 中包含非数字部分,忽略") + } + } + + // 生成下一个候选版本号 + var candidateVersion: String + var candidateMinor = maxMinor + 1 + + do { + candidateVersion = "$maxMajor.$candidateMinor" + + // 检查此版本号是否已存在对应的文件 + val modelFileExists = File("$MODEL_PATH/$candidateVersion.model.xml").exists() + val sqlFileExists = File("$MIGRATION_PATH/$candidateVersion.sql").exists() + + if (modelFileExists || sqlFileExists) { + log("版本号 $candidateVersion 已存在文件,尝试下一个版本") + candidateMinor++ + } else { + break + } + } while (true) + + log("确定下一个版本号: $candidateVersion") + return candidateVersion + } + + /** + * 处理创建表 + */ + private fun processCreateTables( + tablesToCreate: List, + sqlBuilder: StringBuilder, + doc: Document, + changeSetElement: Element + ) { + tablesToCreate.forEach { sqlInfo -> + // 生成SQL + sqlBuilder.append(generateCreateTableSql(sqlInfo)) + sqlBuilder.append("\n\n") + + // 添加到XML + addCreateTableToModel(doc, changeSetElement, sqlInfo) + } + } + + /** + * 处理删除表 + */ + private fun processDropTables( + tablesToDrop: List, + sqlBuilder: StringBuilder, + doc: Document, + changeSetElement: Element + ) { + // 检查是否需要生成删除语句 + val generateDropStatements = System.getProperty("ddl.migration.generateDropStatements", "false").toBoolean() + + tablesToDrop.forEach { tableName -> + // 只有当配置为生成删除语句时才添加到SQL中 + if (generateDropStatements) { + // 生成SQL + sqlBuilder.append("drop table if exists $tableName cascade;\n") + } + + // 无论如何都添加到XML中,以记录历史变更 + val dropTableElement = doc.createElement("dropTable") + dropTableElement.setAttribute("name", tableName) + changeSetElement.appendChild(dropTableElement) + } + + if (generateDropStatements && tablesToDrop.isNotEmpty()) { + sqlBuilder.append("\n") + } + } + + /** + * 处理添加列 + */ + private fun processAddColumns( + columnsToAdd: Map>, + sqlBuilder: StringBuilder, + doc: Document, + changeSetElement: Element + ) { + if (columnsToAdd.isNotEmpty()) { + sqlBuilder.append("-- apply alter tables\n") + } + + columnsToAdd.forEach { (tableName, columns) -> + // 为每个表创建一个addColumn元素 + val addColumnElement = doc.createElement("addColumn") + addColumnElement.setAttribute("tableName", tableName) + changeSetElement.appendChild(addColumnElement) + + columns.forEach { column -> + // 生成SQL + val nullable = if (column.nullable) "" else " not null" + val defaultValue = if (column.defaultValue.isNotEmpty()) " default ${column.defaultValue}" else "" + sqlBuilder.append("alter table $tableName\n add column ${column.name} ${column.type}$defaultValue$nullable;\n") + + // 添加到XML + val columnElement = doc.createElement("column") + columnElement.setAttribute("name", column.name) + columnElement.setAttribute("type", column.type) + if (column.defaultValue.isNotEmpty()) { + columnElement.setAttribute("defaultValue", column.defaultValue) + } + if (!column.nullable) { + columnElement.setAttribute("notnull", "true") + } + addColumnElement.appendChild(columnElement) + } + + sqlBuilder.append("\n") + } + } + + /** + * 处理删除列 + */ + private fun processDropColumns( + columnsToDrop: Map>, + sqlBuilder: StringBuilder, + doc: Document, + changeSetElement: Element + ) { + // 检查是否需要生成删除语句 + val generateDropStatements = System.getProperty("ddl.migration.generateDropStatements", "false").toBoolean() + + var addedSql = false + + columnsToDrop.forEach { (tableName, columns) -> + if (columns.isNotEmpty()) { + val alterTableElement = doc.createElement("alterTable") + alterTableElement.setAttribute("name", tableName) + changeSetElement.appendChild(alterTableElement) + + columns.forEach { columnName -> + // 只有当配置为生成删除语句时才添加到SQL中 + if (generateDropStatements) { + sqlBuilder.append("alter table $tableName drop column if exists $columnName;\n") + addedSql = true + } + + // 无论如何都添加到XML中,以记录历史变更 + val dropColumnElement = doc.createElement("dropColumn") + dropColumnElement.setAttribute("columnName", columnName) + alterTableElement.appendChild(dropColumnElement) + } + } + } + + if (addedSql) { + sqlBuilder.append("\n") + } + } + + /** + * 处理修改列 + */ + private fun processAlterColumns( + columnsToAlter: Map>>, + sqlBuilder: StringBuilder, + doc: Document, + changeSetElement: Element + ) { + columnsToAlter.forEach { (tableName, columns) -> + // 为每个表创建一个alterColumn元素 + val alterColumnElement = doc.createElement("alterColumn") + alterColumnElement.setAttribute("tableName", tableName) + changeSetElement.appendChild(alterColumnElement) + + columns.forEach { (oldColumn, newColumn) -> + // 生成SQL和XML + + // 如果类型不同,修改类型 + if (oldColumn.type != newColumn.type) { + sqlBuilder.append("alter table $tableName alter column ${oldColumn.name} type ${newColumn.type} using ${oldColumn.name}::${newColumn.type};\n") + + val columnElement = doc.createElement("column") + columnElement.setAttribute("name", oldColumn.name) + columnElement.setAttribute("type", newColumn.type) + columnElement.setAttribute("currentType", oldColumn.type) + alterColumnElement.appendChild(columnElement) + } + + // 如果可空性不同,修改可空性 + if (oldColumn.nullable != newColumn.nullable) { + if (newColumn.nullable) { + sqlBuilder.append("alter table $tableName alter column ${oldColumn.name} drop not null;\n") + + val columnElement = doc.createElement("column") + columnElement.setAttribute("name", oldColumn.name) + columnElement.setAttribute("notnull", "false") + columnElement.setAttribute("currentNotnull", "true") + alterColumnElement.appendChild(columnElement) + } else { + sqlBuilder.append("alter table $tableName alter column ${oldColumn.name} set not null;\n") + + val columnElement = doc.createElement("column") + columnElement.setAttribute("name", oldColumn.name) + columnElement.setAttribute("notnull", "true") + columnElement.setAttribute("currentNotnull", "false") + alterColumnElement.appendChild(columnElement) + } + } + + // 如果默认值不同,修改默认值 + if (oldColumn.defaultValue != newColumn.defaultValue) { + if (newColumn.defaultValue.isEmpty()) { + sqlBuilder.append("alter table $tableName alter column ${oldColumn.name} drop default;\n") + + val columnElement = doc.createElement("column") + columnElement.setAttribute("name", oldColumn.name) + columnElement.setAttribute("dropDefault", "true") + alterColumnElement.appendChild(columnElement) + } else { + sqlBuilder.append("alter table $tableName alter column ${oldColumn.name} set default ${newColumn.defaultValue};\n") + + val columnElement = doc.createElement("column") + columnElement.setAttribute("name", oldColumn.name) + columnElement.setAttribute("defaultValue", newColumn.defaultValue) + columnElement.setAttribute("currentDefaultValue", oldColumn.defaultValue) + alterColumnElement.appendChild(columnElement) + } + } + } + + sqlBuilder.append("\n") + } + } + + /** + * 从过去的变更中构建完整的数据库模型 + */ + private fun buildFullModel(latestModelFile: File): Map { + val allTables = mutableMapOf() + val modelDir = File(MODEL_PATH) + + // 获取所有模型文件并按版本排序 + val modelFiles = modelDir.listFiles { file -> + file.isFile && file.name.endsWith(".model.xml") + } ?: return allTables + + // 按版本号排序 + val sortedFiles = sortModelFilesByVersion(modelFiles) + + println("按版本排序后的模型文件:") + sortedFiles.forEach { println(" - ${it.name} (${extractVersionFromFileName(it.name)})") } + + // 按顺序应用每个变更 + for (file in sortedFiles) { + try { + val modelDoc = parseModelFile(file) + applyChangesToModel(modelDoc, allTables) + println("已应用模型变更: ${file.name}") + } catch (e: Exception) { + println("处理模型文件 ${file.name} 时出错: ${e.message}") + } + } + + // 打印当前模型状态 + println("当前数据库模型状态:") + allTables.forEach { (tableName, sqlInfo) -> + println(" - 表: $tableName, 列数: ${sqlInfo.columns.size}") + } + + return allTables + } + + /** + * 按版本号排序模型文件 + */ + private fun sortModelFilesByVersion(files: Array): List { + // 构建文件映射和版本值 + val fileInfos = files.map { file -> + val version = extractVersionFromFileName(file.name) + val versionValue = calculateVersionValue(version) + Triple(file, version, versionValue) + } + + // 排序:初始化文件在前,其他按版本号排序 + return fileInfos.sortedWith { a, b -> + val aVersion = a.second + val bVersion = b.second + + // 初始化文件总是最先处理 + if (aVersion.contains("__") && !bVersion.contains("__")) { + -1 + } else if (!aVersion.contains("__") && bVersion.contains("__")) { + 1 + } else { + // 按版本号比较 + a.third.compareTo(b.third) + } + }.map { it.first } + } + + /** + * 将模型文件中的变更应用到模型 + */ + private fun applyChangesToModel(doc: Document, allTables: MutableMap) { + val changeSetNodes = doc.getElementsByTagName("changeSet") + + for (i in 0 until changeSetNodes.length) { + val changeSetNode = changeSetNodes.item(i) as Element + val changeType = changeSetNode.getAttribute("type") + + if (changeType == "apply") { + // 处理创建表 + val createTableNodes = changeSetNode.getElementsByTagName("createTable") + for (j in 0 until createTableNodes.length) { + val createTableNode = createTableNodes.item(j) as Element + val tableName = createTableNode.getAttribute("name") + val sqlInfo = SqlInfo(tableName = tableName) + + // 解析列信息 + val columnNodes = createTableNode.getElementsByTagName("column") + for (k in 0 until columnNodes.length) { + val columnNode = columnNodes.item(k) as Element + val columnInfo = parseColumnFromXml(columnNode) + sqlInfo.columns.add(columnInfo) + + if (columnInfo.isPrimaryKey) { + sqlInfo.primaryKeys.add(columnInfo.name) + } + } + + allTables[tableName] = sqlInfo + } + + // 处理添加列 + val addColumnNodes = changeSetNode.getElementsByTagName("addColumn") + for (j in 0 until addColumnNodes.length) { + val addColumnNode = addColumnNodes.item(j) as Element + val tableName = addColumnNode.getAttribute("tableName") + val table = allTables[tableName] ?: continue + + val columnNodes = addColumnNode.getElementsByTagName("column") + for (k in 0 until columnNodes.length) { + val columnNode = columnNodes.item(k) as Element + val columnInfo = parseColumnFromXml(columnNode) + table.columns.add(columnInfo) + + if (columnInfo.isPrimaryKey) { + table.primaryKeys.add(columnInfo.name) + } + } + } + + // 处理删除列 + val dropColumnNodes = changeSetNode.getElementsByTagName("dropColumn") + for (j in 0 until dropColumnNodes.length) { + val dropColumnNode = dropColumnNodes.item(j) as Element + val tableName = dropColumnNode.getAttribute("tableName") + val table = allTables[tableName] ?: continue + + val columnNodes = dropColumnNode.getElementsByTagName("column") + for (k in 0 until columnNodes.length) { + val columnNode = columnNodes.item(k) as Element + val columnName = columnNode.getAttribute("name") + + // 删除列 + table.columns.removeIf { it.name == columnName } + table.primaryKeys.remove(columnName) + } + } + + // 处理修改列 + val alterColumnNodes = changeSetNode.getElementsByTagName("alterColumn") + for (j in 0 until alterColumnNodes.length) { + val alterColumnNode = alterColumnNodes.item(j) as Element + val tableName = alterColumnNode.getAttribute("tableName") + val table = allTables[tableName] ?: continue + + val columnNodes = alterColumnNode.getElementsByTagName("column") + for (k in 0 until columnNodes.length) { + val columnNode = columnNodes.item(k) as Element + val columnName = columnNode.getAttribute("name") + + // 查找并更新列 + val column = table.columns.find { it.name == columnName } + + if (column != null) { + // 更新类型 + if (columnNode.hasAttribute("type")) { + column.type = columnNode.getAttribute("type") + } + + // 更新可空性 + if (columnNode.hasAttribute("notnull")) { + column.nullable = columnNode.getAttribute("notnull") != "true" + } + + // 更新默认值 + if (columnNode.hasAttribute("defaultValue")) { + column.defaultValue = columnNode.getAttribute("defaultValue") + } else if (columnNode.hasAttribute("dropDefault") && + columnNode.getAttribute("dropDefault") == "true") { + column.defaultValue = "" + } + } + } + } + + // 处理删除表 + val dropTableNodes = changeSetNode.getElementsByTagName("dropTable") + for (j in 0 until dropTableNodes.length) { + val dropTableNode = dropTableNodes.item(j) as Element + val tableName = dropTableNode.getAttribute("name") + allTables.remove(tableName) + } + } + } + } + + /** + * 从XML解析列信息 + */ + private fun parseColumnFromXml(columnNode: Element): ColumnInfo { + val name = columnNode.getAttribute("name") + val type = columnNode.getAttribute("type") + val nullable = !columnNode.hasAttribute("notnull") || columnNode.getAttribute("notnull") != "true" + val defaultValue = if (columnNode.hasAttribute("defaultValue")) { + columnNode.getAttribute("defaultValue") + } else { + "" + } + val isPrimaryKey = columnNode.hasAttribute("primaryKey") && + columnNode.getAttribute("primaryKey") == "true" + + return ColumnInfo( + name = name, + type = type, + nullable = nullable, + defaultValue = defaultValue, + isPrimaryKey = isPrimaryKey + ) + } + + /** + * 添加创建表到模型 + */ + private fun addCreateTableToModel(doc: Document, parentElement: Element, sqlInfo: SqlInfo) { + val createTableElement = doc.createElement("createTable") + createTableElement.setAttribute("name", sqlInfo.tableName) + + if (sqlInfo.primaryKeys.isNotEmpty()) { + createTableElement.setAttribute("pkName", "pk_${sqlInfo.tableName}") + } + + parentElement.appendChild(createTableElement) + + // 添加列 + sqlInfo.columns.forEach { column -> + val columnElement = doc.createElement("column") + columnElement.setAttribute("name", column.name) + columnElement.setAttribute("type", column.type) + + if (!column.nullable) { + columnElement.setAttribute("notnull", "true") + } + + if (column.defaultValue.isNotEmpty()) { + columnElement.setAttribute("defaultValue", column.defaultValue) + } + + if (column.isPrimaryKey) { + columnElement.setAttribute("primaryKey", "true") + } + + createTableElement.appendChild(columnElement) + } + } + + /** + * 比较表差异 + */ + private fun compareTables( + oldTables: Map, + currentTables: Map + ): DiffResult { + val result = DiffResult() + + // 找出需要创建的表 + currentTables.forEach { (tableName, sqlInfo) -> + if (!oldTables.containsKey(tableName)) { + result.tablesToCreate.add(sqlInfo) + } + } + + // 找出需要删除的表 + oldTables.forEach { (tableName, _) -> + if (!currentTables.containsKey(tableName)) { + result.tablesToDrop.add(tableName) + } + } + + // 对于都存在的表,比较列差异 + currentTables.forEach { (tableName, currentSqlInfo) -> + oldTables[tableName]?.let { oldSqlInfo -> + // 当前表中的列名集合 + val currentColumnNames = currentSqlInfo.columns.map { it.name }.toSet() + // 旧表中的列名集合 + val oldColumnNames = oldSqlInfo.columns.map { it.name }.toSet() + + // 找出需要添加的列 + val columnsToAdd = currentSqlInfo.columns.filter { it.name !in oldColumnNames } + if (columnsToAdd.isNotEmpty()) { + result.columnsToAdd[tableName] = columnsToAdd + } + + // 找出需要删除的列 + val columnsToDrop = oldSqlInfo.columns + .filter { it.name !in currentColumnNames } + .map { it.name } + if (columnsToDrop.isNotEmpty()) { + result.columnsToDrop[tableName] = columnsToDrop + } + + // 找出需要修改的列 + val columnsToAlter = mutableListOf>() + + // 遍历两个表中都存在的列 + oldSqlInfo.columns.forEach { oldColumn -> + currentSqlInfo.columns.find { it.name == oldColumn.name }?.let { currentColumn -> + // 检查列定义是否发生变化 + if (oldColumn.type != currentColumn.type || + oldColumn.nullable != currentColumn.nullable || + oldColumn.defaultValue != currentColumn.defaultValue) { + columnsToAlter.add(oldColumn to currentColumn) + } + } + } + + if (columnsToAlter.isNotEmpty()) { + result.columnsToAlter[tableName] = columnsToAlter + } + } + } + + return result + } + + /** + * 查找最新的模型文件 + */ + private fun findLatestModelFile(): File? { + val modelDir = File(MODEL_PATH) + val modelFiles = modelDir.listFiles { file -> + file.isFile && file.name.endsWith(".model.xml") + } ?: return null + + // 先打印所有可能的文件,便于调试 + println("找到所有模型文件:") + modelFiles.forEach { println(" - ${it.name}") } + + // 构建版本号到文件的映射 + val versionToFileMap = mutableMapOf() + val versionToValueMap = mutableMapOf() + + modelFiles.forEach { file -> + val version = extractVersionFromFileName(file.name) + val versionValue = calculateVersionValue(version) + versionToFileMap[version] = file + versionToValueMap[version] = versionValue + } + + // 找出最大版本号 + val maxVersionEntry = versionToValueMap.entries.maxByOrNull { it.value } + if (maxVersionEntry != null) { + val latestFile = versionToFileMap[maxVersionEntry.key] + println("选择最新版本文件: ${latestFile?.name}, 版本值: ${maxVersionEntry.value}") + return latestFile + } + + return null + } + + /** + * 计算版本号的数值表示,用于比较 + */ + private fun calculateVersionValue(version: String): Int { + println("计算版本值: $version") + + if (!version.contains(".")) { + println("警告: 版本号 '$version' 不包含'.',返回默认值0") + return 0 + } + + val parts = version.split(".") + if (parts.size != 2) { + println("警告: 版本号 '$version' 格式不正确,返回默认值0") + return 0 + } + + try { + val major = parts[0].toInt() + val minor = parts[1].toInt() + val value = major * 1000 + minor + println("版本号 '$version' 的数值为: $value") + return value + } catch (e: NumberFormatException) { + println("警告: 版本号 '$version' 中包含非数字部分,返回默认值0") + return 0 + } + } + + /** + * 从文件名提取版本号 + */ + private fun extractVersionFromFileName(fileName: String): String { + // 打印原始文件名,便于调试 + println("提取版本号,原始文件名: $fileName") + + // 处理带有初始化标记的情况 + if (fileName.contains("__")) { + val parts = fileName.split("__") + return parts[0] + } + + // 处理普通版本号 + val fileNameWithoutExt = fileName.substringBeforeLast(".model.xml").substringBeforeLast(".sql") + + // 确保包含点号的完整版本 + if (fileNameWithoutExt.contains(".")) { + println("提取的版本号: $fileNameWithoutExt") + return fileNameWithoutExt + } + + // 如果没有找到有效格式,返回原始部分 + println("未找到有效版本格式,使用: $fileNameWithoutExt") + return fileNameWithoutExt + } + + /** + * 创建模型文档 + */ + private fun createModelDocument(): Document { + val docFactory = DocumentBuilderFactory.newInstance() + val docBuilder = docFactory.newDocumentBuilder() + val doc = docBuilder.newDocument() + + val rootElement = doc.createElement("migration") + rootElement.setAttribute("generated", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)) +// rootElement.setAttribute("xmlns", "http://ebean-orm.github.io/xml/ns/dbmigration") + doc.appendChild(rootElement) + + return doc + } + + /** + * 解析模型文件 + */ + private fun parseModelFile(file: File): Document { + val docFactory = DocumentBuilderFactory.newInstance() + val docBuilder = docFactory.newDocumentBuilder() + return docBuilder.parse(file) + } + + /** + * 写入模型到文件 + */ + private fun writeModelToFile(doc: Document, fileName: String) { + val transformerFactory = TransformerFactory.newInstance() + val transformer = transformerFactory.newTransformer() + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8") + transformer.setOutputProperty(OutputKeys.STANDALONE, "yes") + + val source = DOMSource(doc) + val result = StreamResult(FileOutputStream(fileName)) + transformer.transform(source, result) + } + + /** + * 根据SqlInfo生成创建表SQL + */ + private fun generateCreateTableSql(sqlInfo: SqlInfo): String { + val tableName = sqlInfo.tableName + val columns = sqlInfo.columns + + if (tableName.isEmpty() || columns.isEmpty()) { + throw IllegalArgumentException("无法生成SQL,表名或列信息为空") + } + + val sb = StringBuilder() + sb.append("create table $tableName (\n") + + // 添加列定义 + val columnDefinitions = columns.map { column -> + val nullable = if (column.nullable) "" else " not null" + val defaultValue = if (column.defaultValue.isNotEmpty()) " default ${column.defaultValue}" else "" + " ${column.name} ${column.type}$defaultValue$nullable" + } + + sb.append(columnDefinitions.joinToString(",\n")) + + // 添加主键 + if (sqlInfo.primaryKeys.isNotEmpty()) { + sb.append(",\n constraint pk_${sqlInfo.tableName} primary key (${sqlInfo.primaryKeys.joinToString(", ")})") + } + + sb.append("\n);") + return sb.toString() + } + + /** + * 检查差异结果是否为空 + */ + private fun DiffResult.isEmpty(): Boolean { + return tablesToCreate.isEmpty() && + tablesToDrop.isEmpty() && + columnsToAdd.isEmpty() && + columnsToDrop.isEmpty() && + columnsToAlter.isEmpty() + } + } + + /** + * 差异结果类 + */ + data class DiffResult( + val tablesToCreate: MutableList = mutableListOf(), + val tablesToDrop: MutableList = mutableListOf(), + val columnsToAdd: MutableMap> = mutableMapOf(), + val columnsToDrop: MutableMap> = mutableMapOf(), + val columnsToAlter: MutableMap>> = mutableMapOf() + ) +} \ No newline at end of file diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapper.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/QueryWrapper.kt similarity index 97% rename from vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapper.kt rename to vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/QueryWrapper.kt index 23ef5b6..fe53080 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapper.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/QueryWrapper.kt @@ -1,4 +1,4 @@ -package org.aikrai.vertx.db +package org.aikrai.vertx.db.wrapper import kotlin.reflect.KProperty1 diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapperImpl.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/QueryWrapperImpl.kt similarity index 99% rename from vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapperImpl.kt rename to vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/QueryWrapperImpl.kt index 4f784fc..ab136da 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/QueryWrapperImpl.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/QueryWrapperImpl.kt @@ -1,4 +1,4 @@ -package org.aikrai.vertx.db +package org.aikrai.vertx.db.wrapper import io.vertx.kotlin.coroutines.coAwait import io.vertx.sqlclient.Row diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/Repository.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/Repository.kt similarity index 93% rename from vertx-fw/src/main/kotlin/org/aikrai/vertx/db/Repository.kt rename to vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/Repository.kt index a6892c1..5bd67f0 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/Repository.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/Repository.kt @@ -1,4 +1,4 @@ -package org.aikrai.vertx.db +package org.aikrai.vertx.db.wrapper import kotlin.reflect.KProperty1 diff --git a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/RepositoryImpl.kt b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/RepositoryImpl.kt similarity index 99% rename from vertx-fw/src/main/kotlin/org/aikrai/vertx/db/RepositoryImpl.kt rename to vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/RepositoryImpl.kt index 9766579..906b2b4 100644 --- a/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/RepositoryImpl.kt +++ b/vertx-fw/src/main/kotlin/org/aikrai/vertx/db/wrapper/RepositoryImpl.kt @@ -1,4 +1,4 @@ -package org.aikrai.vertx.db +package org.aikrai.vertx.db.wrapper import cn.hutool.core.util.IdUtil import cn.hutool.core.util.StrUtil