feat(db): 添加数据库迁移工具和相关功能
This commit is contained in:
parent
5eccf7ceed
commit
e97f3f5519
@ -41,16 +41,8 @@ tasks.test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.compileKotlin {
|
kotlin {
|
||||||
kotlinOptions {
|
jvmToolchain(17)
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.compileTestKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
@ -88,10 +80,14 @@ dependencies {
|
|||||||
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
implementation("io.vertx:vertx-auth-jwt:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-redis-client:$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("com.google.inject:guice:5.1.0")
|
||||||
implementation("org.reflections:reflections:0.10.2")
|
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.core:jackson-databind:2.15.2")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
|
||||||
// implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
// 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("ch.qos.logback:logback-classic:1.4.14")
|
||||||
implementation("org.codehaus.janino:janino:3.1.8")
|
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
|
// db
|
||||||
implementation("org.postgresql:postgresql:42.7.5")
|
implementation("org.postgresql:postgresql:42.7.5")
|
||||||
implementation("com.ongres.scram:client:2.1")
|
implementation("com.ongres.scram:client:2.1")
|
||||||
@ -114,6 +106,9 @@ dependencies {
|
|||||||
// doc
|
// doc
|
||||||
implementation("io.swagger.core.v3:swagger-core:2.2.27")
|
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("io.vertx:vertx-junit5")
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter:$junitJupiterVersion")
|
testImplementation("org.junit.jupiter:junit-jupiter:$junitJupiterVersion")
|
||||||
testImplementation("org.mockito:mockito-core:5.15.2")
|
testImplementation("org.mockito:mockito-core:5.15.2")
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import java.sql.Timestamp
|
|||||||
class Account : BaseEntity() {
|
class Account : BaseEntity() {
|
||||||
|
|
||||||
@TableId(type = IdType.ASSIGN_ID)
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
@TableFieldComment("用户ID")
|
||||||
var userId: Long = 0L
|
var userId: Long = 0L
|
||||||
|
|
||||||
@TableField("user_name")
|
@TableField("user_name")
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package app.data.domain.account
|
|||||||
import app.data.domain.account.modle.AccountRoleAccessDTO
|
import app.data.domain.account.modle.AccountRoleAccessDTO
|
||||||
import app.data.domain.account.modle.AccountRoleDTO
|
import app.data.domain.account.modle.AccountRoleDTO
|
||||||
import com.google.inject.ImplementedBy
|
import com.google.inject.ImplementedBy
|
||||||
import org.aikrai.vertx.db.Repository
|
import org.aikrai.vertx.db.wrapper.Repository
|
||||||
|
|
||||||
@ImplementedBy(AccountRepositoryImpl::class)
|
@ImplementedBy(AccountRepositoryImpl::class)
|
||||||
interface AccountRepository : Repository<Long, Account> {
|
interface AccountRepository : Repository<Long, Account> {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import app.data.domain.account.modle.AccountRoleAccessDTO
|
|||||||
import app.data.domain.account.modle.AccountRoleDTO
|
import app.data.domain.account.modle.AccountRoleDTO
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import io.vertx.sqlclient.SqlClient
|
import io.vertx.sqlclient.SqlClient
|
||||||
import org.aikrai.vertx.db.RepositoryImpl
|
import org.aikrai.vertx.db.wrapper.RepositoryImpl
|
||||||
|
|
||||||
class AccountRepositoryImpl @Inject constructor(
|
class AccountRepositoryImpl @Inject constructor(
|
||||||
sqlClient: SqlClient
|
sqlClient: SqlClient
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package app.data.domain.menu
|
package app.data.domain.menu
|
||||||
|
|
||||||
import com.google.inject.ImplementedBy
|
import com.google.inject.ImplementedBy
|
||||||
import org.aikrai.vertx.db.Repository
|
import org.aikrai.vertx.db.wrapper.Repository
|
||||||
|
|
||||||
@ImplementedBy(MenuRepositoryImpl::class)
|
@ImplementedBy(MenuRepositoryImpl::class)
|
||||||
interface MenuRepository : Repository<Long, Menu> {
|
interface MenuRepository : Repository<Long, Menu> {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package app.data.domain.menu
|
|||||||
|
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import io.vertx.sqlclient.SqlClient
|
import io.vertx.sqlclient.SqlClient
|
||||||
import org.aikrai.vertx.db.RepositoryImpl
|
import org.aikrai.vertx.db.wrapper.RepositoryImpl
|
||||||
|
|
||||||
class MenuRepositoryImpl @Inject constructor(
|
class MenuRepositoryImpl @Inject constructor(
|
||||||
sqlClient: SqlClient
|
sqlClient: SqlClient
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package app.data.domain.role
|
package app.data.domain.role
|
||||||
|
|
||||||
import com.google.inject.ImplementedBy
|
import com.google.inject.ImplementedBy
|
||||||
import org.aikrai.vertx.db.Repository
|
import org.aikrai.vertx.db.wrapper.Repository
|
||||||
|
|
||||||
@ImplementedBy(RoleRepositoryImpl::class)
|
@ImplementedBy(RoleRepositoryImpl::class)
|
||||||
interface RoleRepository : Repository<Long, Role>
|
interface RoleRepository : Repository<Long, Role>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package app.data.domain.role
|
|||||||
|
|
||||||
import com.google.inject.Inject
|
import com.google.inject.Inject
|
||||||
import io.vertx.sqlclient.SqlClient
|
import io.vertx.sqlclient.SqlClient
|
||||||
import org.aikrai.vertx.db.RepositoryImpl
|
import org.aikrai.vertx.db.wrapper.RepositoryImpl
|
||||||
|
|
||||||
class RoleRepositoryImpl @Inject constructor(
|
class RoleRepositoryImpl @Inject constructor(
|
||||||
sqlClient: SqlClient
|
sqlClient: SqlClient
|
||||||
|
|||||||
48
vertx-demo/src/main/resources/dbmigration/1.0__initial.sql
Normal file
48
vertx-demo/src/main/resources/dbmigration/1.0__initial.sql
Normal file
@ -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
|
||||||
|
);
|
||||||
|
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<migration generated="2025-03-21T15:27:45.6470429">
|
||||||
|
<changeSet type="apply">
|
||||||
|
<createTable name="sys_menu" pkName="pk_sys_menu">
|
||||||
|
<column name="menu_id" notnull="true" primaryKey="true" type="BIGINT"/>
|
||||||
|
<column name="menu_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="parent_id" type="BIGINT"/>
|
||||||
|
<column name="order_num" type="INTEGER"/>
|
||||||
|
<column name="path" type="VARCHAR(255)"/>
|
||||||
|
<column name="component" type="VARCHAR(255)"/>
|
||||||
|
<column name="menu_type" type="VARCHAR(255)"/>
|
||||||
|
<column name="visible" type="VARCHAR(255)"/>
|
||||||
|
<column name="perms" type="VARCHAR(255)"/>
|
||||||
|
<column name="parent_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="children" type="JSONB"/>
|
||||||
|
</createTable>
|
||||||
|
<createTable name="sys_user" pkName="pk_sys_user">
|
||||||
|
<column name="user_id" notnull="true" primaryKey="true" type="BIGINT"/>
|
||||||
|
<column name="user_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="user_type" type="VARCHAR(255)"/>
|
||||||
|
<column name="email" type="VARCHAR(255)"/>
|
||||||
|
<column name="phone" type="VARCHAR(255)"/>
|
||||||
|
<column name="avatar" type="VARCHAR(255)"/>
|
||||||
|
<column name="password" type="VARCHAR(255)"/>
|
||||||
|
<column defaultValue="0" name="status" type="INTEGER"/>
|
||||||
|
<column name="del_flag" type="CHAR(1)"/>
|
||||||
|
<column name="login_ip" type="VARCHAR(255)"/>
|
||||||
|
<column name="login_date" type="TIMESTAMPTZ"/>
|
||||||
|
</createTable>
|
||||||
|
<createTable name="sys_role">
|
||||||
|
<column name="role_id" type="BIGINT"/>
|
||||||
|
<column name="role_name" type="VARCHAR(255)"/>
|
||||||
|
<column name="role_key" type="VARCHAR(255)"/>
|
||||||
|
<column name="role_sort" type="INTEGER"/>
|
||||||
|
<column name="data_scope" type="CHAR(1)"/>
|
||||||
|
<column name="status" type="CHAR(1)"/>
|
||||||
|
<column name="del_flag" type="CHAR(1)"/>
|
||||||
|
</createTable>
|
||||||
|
</changeSet>
|
||||||
|
</migration>
|
||||||
114
vertx-demo/src/test/kotlin/app/GenerateMigration.kt
Normal file
114
vertx-demo/src/test/kotlin/app/GenerateMigration.kt
Normal file
@ -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<String>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
apifox:
|
|
||||||
token: APS-xxxxxxxxxxxxxxxxxxxx
|
|
||||||
projectId: xxxxx
|
|
||||||
folderId: xxxxx
|
|
||||||
@ -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
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
server:
|
|
||||||
port: 8080
|
|
||||||
package: app
|
|
||||||
|
|
||||||
|
|
||||||
@ -27,16 +27,8 @@ tasks.test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.compileKotlin {
|
kotlin {
|
||||||
kotlinOptions {
|
jvmToolchain(17)
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.compileTestKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
@ -55,7 +47,7 @@ spotless {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20")
|
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.20")
|
||||||
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
implementation("io.vertx:vertx-lang-kotlin-coroutines:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-core:$vertxVersion")
|
implementation("io.vertx:vertx-core:$vertxVersion")
|
||||||
implementation("io.vertx:vertx-web:$vertxVersion")
|
implementation("io.vertx:vertx-web:$vertxVersion")
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
package org.aikrai.vertx.db
|
|
||||||
|
|
||||||
object SqlHelper {
|
|
||||||
|
|
||||||
fun retBool(result: Int?): Boolean {
|
|
||||||
return null != result && result >= 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -34,6 +34,18 @@ annotation class TableField(
|
|||||||
// val keepGlobalFormat: Boolean = false,
|
// val keepGlobalFormat: Boolean = false,
|
||||||
// val property: String = "",
|
// val property: String = "",
|
||||||
// val numericScale: 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
|
@MustBeDocumented
|
||||||
|
|||||||
@ -0,0 +1,123 @@
|
|||||||
|
package org.aikrai.vertx.db.migration
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成PostgreSQL DDL迁移脚本,基于实体类及其注解的变更。
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 通常在开发人员对模型进行了一组更改后,作为测试阶段的主要方法运行。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h3>示例: 运行生成PostgreSQL迁移脚本</h3>
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
*
|
||||||
|
* val migration = DbMigration.create()
|
||||||
|
*
|
||||||
|
* // 可选:指定版本和名称
|
||||||
|
* migration.setName("添加用户表索引")
|
||||||
|
*
|
||||||
|
* // 设置实体包路径
|
||||||
|
* migration.setEntityPackage("org.aikrai.vertx.entity")
|
||||||
|
*
|
||||||
|
* // 设置SQL注解映射器
|
||||||
|
* migration.setSqlAnnotationMapper(createMapper())
|
||||||
|
*
|
||||||
|
* // 生成迁移
|
||||||
|
* migration.generateMigration()
|
||||||
|
*
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
interface DbMigration {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* 创建DbMigration实现实例
|
||||||
|
*/
|
||||||
|
fun create(): DbMigration {
|
||||||
|
return DefaultDbMigration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置实体类所在的包路径
|
||||||
|
*/
|
||||||
|
fun setEntityPackage(packagePath: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置SQL注解映射器
|
||||||
|
*/
|
||||||
|
fun setSqlAnnotationMapper(mapper: SqlAnnotationMapper)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置资源文件路径
|
||||||
|
* <p>
|
||||||
|
* 默认为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)
|
||||||
|
* <p>
|
||||||
|
* 如果设置为false,生成的SQL中将不包含任何DROP语句。
|
||||||
|
* 但.model.xml文件中仍会记录删除表和字段的信息。
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
fun setGenerateDropStatements(generateDropStatements: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成下一次迁移SQL脚本和相关模型XML
|
||||||
|
* <p>
|
||||||
|
* 不会实际运行迁移或DDL脚本,只是生成它们。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return 生成的迁移版本或null(如果没有变更)
|
||||||
|
*/
|
||||||
|
fun generateMigration(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成包含所有变更的"初始"迁移
|
||||||
|
* <p>
|
||||||
|
* "初始"迁移只能在尚未对其运行任何先前迁移的数据库上执行和使用。
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return 生成的迁移版本
|
||||||
|
*/
|
||||||
|
fun generateInitMigration(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回迁移主目录
|
||||||
|
*/
|
||||||
|
fun migrationDirectory(): File
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ColumnMapping> = mutableListOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键映射信息
|
||||||
|
*/
|
||||||
|
var primaryKeyMapping: AnnotationMapping? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引映射信息
|
||||||
|
*/
|
||||||
|
var indexMapping: AnnotationMapping? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 枚举值映射信息
|
||||||
|
*/
|
||||||
|
var enumValueMapping: AnnotationMapping? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 其他自定义映射
|
||||||
|
*/
|
||||||
|
var customMappings: MutableMap<String, AnnotationMapping> = mutableMapOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个列映射
|
||||||
|
*/
|
||||||
|
fun addColumnMapping(columnMapping: ColumnMapping) {
|
||||||
|
columnMappings.add(columnMapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个自定义映射
|
||||||
|
*/
|
||||||
|
fun addCustomMapping(key: String, mapping: AnnotationMapping) {
|
||||||
|
customMappings[key] = mapping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注解映射类
|
||||||
|
* 记录从哪个注解的哪个属性获取信息
|
||||||
|
*/
|
||||||
|
data class AnnotationMapping(
|
||||||
|
/** 注解类 */
|
||||||
|
val annotationClass: KClass<out Annotation>,
|
||||||
|
/** 注解属性名 */
|
||||||
|
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
|
||||||
|
)
|
||||||
@ -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<String>()
|
||||||
|
|
||||||
|
// 查找实体类中的@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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package org.aikrai.vertx.db.migration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQL信息类
|
||||||
|
* 存储从实体类中提取的SQL相关信息
|
||||||
|
*/
|
||||||
|
data class SqlInfo(
|
||||||
|
var tableName: String = "",
|
||||||
|
val columns: MutableList<ColumnInfo> = mutableListOf(),
|
||||||
|
val primaryKeys: MutableList<String> = mutableListOf(),
|
||||||
|
val indexes: MutableList<IndexInfo> = mutableListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列信息类
|
||||||
|
*/
|
||||||
|
data class ColumnInfo(
|
||||||
|
var name: String = "",
|
||||||
|
var type: String = "",
|
||||||
|
var nullable: Boolean = true,
|
||||||
|
var defaultValue: String = "",
|
||||||
|
var isPrimaryKey: Boolean = false,
|
||||||
|
var enumValues: List<String>? = null,
|
||||||
|
var unique: Boolean = false,
|
||||||
|
var length: Int = 255,
|
||||||
|
var comment: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引信息类
|
||||||
|
*/
|
||||||
|
data class IndexInfo(
|
||||||
|
var name: String = "",
|
||||||
|
var columnNames: List<String> = listOf(),
|
||||||
|
var unique: Boolean = false,
|
||||||
|
var concurrent: Boolean = false,
|
||||||
|
var definition: String = ""
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
package org.aikrai.vertx.db
|
package org.aikrai.vertx.db.wrapper
|
||||||
|
|
||||||
import kotlin.reflect.KProperty1
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.aikrai.vertx.db
|
package org.aikrai.vertx.db.wrapper
|
||||||
|
|
||||||
import io.vertx.kotlin.coroutines.coAwait
|
import io.vertx.kotlin.coroutines.coAwait
|
||||||
import io.vertx.sqlclient.Row
|
import io.vertx.sqlclient.Row
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.aikrai.vertx.db
|
package org.aikrai.vertx.db.wrapper
|
||||||
|
|
||||||
import kotlin.reflect.KProperty1
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
@ -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.IdUtil
|
||||||
import cn.hutool.core.util.StrUtil
|
import cn.hutool.core.util.StrUtil
|
||||||
Loading…
x
Reference in New Issue
Block a user