mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-01-12 17:34:12 +00:00
docs: 更新文档导航和解析器指南
- 添加演练场(Playground)文档导航区到主 README - 新增 Python 解析器文档链接(开发指南、测试报告、LSP集成) - 更新前端版本号至 0.1.9b19p - 补充 Python 解析器 requests 库使用章节和官方文档链接 - 添加 JavaScript 和 Python 解析器的语言版本和官方文档 - 优化文档结构,分类为项目文档和外部资源
This commit is contained in:
@@ -68,6 +68,20 @@
|
||||
<version>42.7.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试依赖 -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.38</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -303,7 +303,7 @@ public class CreateTable {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
List<Future<Object>> futures = new ArrayList<>();
|
||||
List<Future<Object>> createFutures = new ArrayList<>();
|
||||
|
||||
for (Class<?> clazz : tableClasses) {
|
||||
List<String> sqlList = getCreateTableSQL(clazz, type);
|
||||
@@ -312,23 +312,41 @@ public class CreateTable {
|
||||
for (String sql : sqlList) {
|
||||
try {
|
||||
pool.query(sql).execute().toCompletionStage().toCompletableFuture().join();
|
||||
futures.add(Future.succeededFuture());
|
||||
createFutures.add(Future.succeededFuture());
|
||||
LOGGER.debug("Executed SQL:\n{}", sql);
|
||||
} catch (Exception e) {
|
||||
String message = e.getMessage();
|
||||
if (message != null && message.contains("Duplicate key name")) {
|
||||
LOGGER.warn("Ignoring duplicate key error: {}", message);
|
||||
futures.add(Future.succeededFuture());
|
||||
createFutures.add(Future.succeededFuture());
|
||||
} else {
|
||||
LOGGER.error("SQL Error: {}\nSQL: {}", message, sql);
|
||||
futures.add(Future.failedFuture(e));
|
||||
createFutures.add(Future.failedFuture(e));
|
||||
throw new RuntimeException(e); // Stop execution for other exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future.all(futures).onSuccess(r -> promise.complete()).onFailure(promise::fail);
|
||||
// 创建表完成后,执行表结构迁移检查
|
||||
Future.all(createFutures)
|
||||
.compose(v -> {
|
||||
LOGGER.info("开始检查表结构变更...");
|
||||
List<Future<Void>> migrationFutures = new ArrayList<>();
|
||||
for (Class<?> clazz : tableClasses) {
|
||||
migrationFutures.add(SchemaMigration.migrateTable(pool, clazz, type));
|
||||
}
|
||||
return Future.all(migrationFutures).mapEmpty();
|
||||
})
|
||||
.onSuccess(v -> {
|
||||
LOGGER.info("表结构检查和变更完成");
|
||||
promise.complete();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
LOGGER.error("表结构变更失败", err);
|
||||
promise.fail(err);
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
|
||||
44
core-database/src/main/java/cn/qaiu/db/ddl/NewField.java
Normal file
44
core-database/src/main/java/cn/qaiu/db/ddl/NewField.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package cn.qaiu.db.ddl;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 标识新增字段,用于数据库表结构迁移
|
||||
* 只有带此注解的字段才会被 SchemaMigration 检查和添加
|
||||
*
|
||||
* <p>使用场景:</p>
|
||||
* <ul>
|
||||
* <li>在现有实体类中添加新字段时,使用此注解标记</li>
|
||||
* <li>应用启动时会自动检测并添加到数据库表中</li>
|
||||
* <li>添加成功后可以移除此注解,避免重复检查</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>示例:</p>
|
||||
* <pre>{@code
|
||||
* @Data
|
||||
* @Table("users")
|
||||
* public class User {
|
||||
* private Long id;
|
||||
* private String name;
|
||||
*
|
||||
* @NewField // 标记为新增字段
|
||||
* @Length(varcharSize = 32)
|
||||
* @Constraint(defaultValue = "active")
|
||||
* private String status;
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface NewField {
|
||||
|
||||
/**
|
||||
* 字段描述(可选)
|
||||
*/
|
||||
String value() default "";
|
||||
}
|
||||
294
core-database/src/main/java/cn/qaiu/db/ddl/SchemaMigration.java
Normal file
294
core-database/src/main/java/cn/qaiu/db/ddl/SchemaMigration.java
Normal file
@@ -0,0 +1,294 @@
|
||||
package cn.qaiu.db.ddl;
|
||||
|
||||
import cn.qaiu.db.pool.JDBCType;
|
||||
import io.vertx.codegen.format.Case;
|
||||
import io.vertx.codegen.format.LowerCamelCase;
|
||||
import io.vertx.codegen.format.SnakeCase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.templates.annotations.Column;
|
||||
import io.vertx.sqlclient.templates.annotations.RowMapped;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 数据库表结构变更处理器
|
||||
* 用于在应用启动时自动检测并添加缺失的字段
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
public class SchemaMigration {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SchemaMigration.class);
|
||||
|
||||
/**
|
||||
* 检查并迁移表结构
|
||||
* 只处理带有 @NewField 注解的字段,避免检查所有字段导致的重复错误
|
||||
*
|
||||
* @param pool 数据库连接池
|
||||
* @param clazz 实体类
|
||||
* @param type 数据库类型
|
||||
* @return Future
|
||||
*/
|
||||
public static Future<Void> migrateTable(Pool pool, Class<?> clazz, JDBCType type) {
|
||||
Promise<Void> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
String tableName = getTableName(clazz);
|
||||
|
||||
// 获取带有 @NewField 注解的字段
|
||||
List<Field> newFields = getNewFields(clazz);
|
||||
|
||||
if (newFields.isEmpty()) {
|
||||
log.debug("表 '{}' 没有标记为 @NewField 的字段,跳过结构检查", tableName);
|
||||
promise.complete();
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
log.info("开始检查表 '{}' 的结构变更,新增字段数: {}", tableName, newFields.size());
|
||||
|
||||
// 获取表的所有字段
|
||||
getTableColumns(pool, tableName, type)
|
||||
.compose(existingColumns -> {
|
||||
// 只添加带有 @NewField 注解且不存在的字段
|
||||
return addNewFields(pool, clazz, tableName, newFields, existingColumns, type);
|
||||
})
|
||||
.onSuccess(v -> {
|
||||
log.info("表 '{}' 结构变更完成", tableName);
|
||||
promise.complete();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("表 '{}' 结构变更失败", tableName, err);
|
||||
promise.fail(err);
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("检查表结构失败", e);
|
||||
promise.fail(e);
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带有 @NewField 注解的字段列表
|
||||
*/
|
||||
private static List<Field> getNewFields(Class<?> clazz) {
|
||||
List<Field> newFields = new ArrayList<>();
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(NewField.class) && !isIgnoredField(field)) {
|
||||
newFields.add(field);
|
||||
String desc = field.getAnnotation(NewField.class).value();
|
||||
if (StringUtils.isNotEmpty(desc)) {
|
||||
log.debug("发现新字段: {} - {}", field.getName(), desc);
|
||||
} else {
|
||||
log.debug("发现新字段: {}", field.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
return newFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表名
|
||||
*/
|
||||
private static String getTableName(Class<?> clazz) {
|
||||
if (clazz.isAnnotationPresent(Table.class)) {
|
||||
Table annotation = clazz.getAnnotation(Table.class);
|
||||
if (StringUtils.isNotEmpty(annotation.value())) {
|
||||
return annotation.value();
|
||||
}
|
||||
}
|
||||
|
||||
// 默认使用类名转下划线命名
|
||||
Case caseFormat = SnakeCase.INSTANCE;
|
||||
if (clazz.isAnnotationPresent(RowMapped.class)) {
|
||||
RowMapped annotation = clazz.getAnnotation(RowMapped.class);
|
||||
caseFormat = getCase(annotation.formatter());
|
||||
}
|
||||
return LowerCamelCase.INSTANCE.to(caseFormat, clazz.getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表的现有字段
|
||||
*/
|
||||
private static Future<Set<String>> getTableColumns(Pool pool, String tableName, JDBCType type) {
|
||||
Promise<Set<String>> promise = Promise.promise();
|
||||
|
||||
String sql = switch (type) {
|
||||
case MySQL -> String.format(
|
||||
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '%s'",
|
||||
tableName
|
||||
);
|
||||
case H2DB -> String.format(
|
||||
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = SCHEMA() AND TABLE_NAME = '%s'",
|
||||
tableName.toUpperCase()
|
||||
);
|
||||
case PostgreSQL -> String.format(
|
||||
"SELECT column_name FROM information_schema.columns WHERE table_name = '%s'",
|
||||
tableName.toLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
pool.query(sql).execute()
|
||||
.onSuccess(rows -> {
|
||||
Set<String> columns = new HashSet<>();
|
||||
rows.forEach(row -> {
|
||||
String columnName = row.getString(0);
|
||||
if (columnName != null) {
|
||||
columns.add(columnName.toLowerCase());
|
||||
}
|
||||
});
|
||||
log.debug("表 '{}' 现有字段: {}", tableName, columns);
|
||||
promise.complete(columns);
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.warn("获取表 '{}' 字段列表失败,可能表不存在: {}", tableName, err.getMessage());
|
||||
promise.complete(new HashSet<>()); // 返回空集合,触发创建表逻辑
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 添加新字段(只处理带 @NewField 注解的字段)
|
||||
*/
|
||||
private static Future<Void> addNewFields(Pool pool, Class<?> clazz, String tableName,
|
||||
List<Field> newFields, Set<String> existingColumns,
|
||||
JDBCType type) {
|
||||
List<Future<Void>> futures = new ArrayList<>();
|
||||
|
||||
Case caseFormat = SnakeCase.INSTANCE;
|
||||
if (clazz.isAnnotationPresent(RowMapped.class)) {
|
||||
RowMapped annotation = clazz.getAnnotation(RowMapped.class);
|
||||
caseFormat = getCase(annotation.formatter());
|
||||
}
|
||||
|
||||
String quotationMarks = type == JDBCType.MySQL ? "`" : "\"";
|
||||
|
||||
for (Field field : newFields) {
|
||||
// 获取字段名
|
||||
String columnName;
|
||||
if (field.isAnnotationPresent(Column.class)) {
|
||||
Column annotation = field.getAnnotation(Column.class);
|
||||
columnName = StringUtils.isNotEmpty(annotation.name())
|
||||
? annotation.name()
|
||||
: LowerCamelCase.INSTANCE.to(caseFormat, field.getName());
|
||||
} else {
|
||||
columnName = LowerCamelCase.INSTANCE.to(caseFormat, field.getName());
|
||||
}
|
||||
|
||||
// 检查字段是否已存在
|
||||
if (existingColumns.contains(columnName.toLowerCase())) {
|
||||
log.warn("字段 '{}' 已存在,请移除 @NewField 注解", columnName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 生成 ALTER TABLE 语句
|
||||
String sql = buildAlterTableSQL(tableName, field, columnName, quotationMarks, type);
|
||||
|
||||
log.info("添加字段: {}", sql);
|
||||
|
||||
Promise<Void> p = Promise.promise();
|
||||
pool.query(sql).execute()
|
||||
.onSuccess(v -> {
|
||||
log.info("字段 '{}' 添加成功", columnName);
|
||||
p.complete();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
String errorMsg = err.getMessage();
|
||||
// 如果字段已存在,忽略错误(可能是并发执行或检测失败)
|
||||
if (errorMsg != null && (errorMsg.contains("Duplicate column") ||
|
||||
errorMsg.contains("already exists") ||
|
||||
errorMsg.contains("duplicate key"))) {
|
||||
log.warn("字段 '{}' 已存在,跳过添加", columnName);
|
||||
p.complete();
|
||||
} else {
|
||||
log.error("字段 '{}' 添加失败", columnName, err);
|
||||
p.fail(err);
|
||||
}
|
||||
});
|
||||
|
||||
futures.add(p.future());
|
||||
}
|
||||
|
||||
return Future.all(futures).mapEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 ALTER TABLE 添加字段的 SQL
|
||||
*/
|
||||
private static String buildAlterTableSQL(String tableName, Field field, String columnName,
|
||||
String quotationMarks, JDBCType type) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ALTER TABLE ").append(quotationMarks).append(tableName).append(quotationMarks)
|
||||
.append(" ADD COLUMN ").append(quotationMarks).append(columnName).append(quotationMarks);
|
||||
|
||||
// 获取字段类型
|
||||
String sqlType = CreateTable.javaProperty2SqlColumnMap.get(field.getType());
|
||||
if (sqlType == null) {
|
||||
sqlType = "VARCHAR";
|
||||
}
|
||||
sb.append(" ").append(sqlType);
|
||||
|
||||
// 添加类型长度
|
||||
int[] decimalSize = {22, 2};
|
||||
int varcharSize = 255;
|
||||
if (field.isAnnotationPresent(Length.class)) {
|
||||
Length length = field.getAnnotation(Length.class);
|
||||
decimalSize = length.decimalSize();
|
||||
varcharSize = length.varcharSize();
|
||||
}
|
||||
|
||||
if ("DECIMAL".equals(sqlType)) {
|
||||
sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
|
||||
} else if ("VARCHAR".equals(sqlType)) {
|
||||
sb.append("(").append(varcharSize).append(")");
|
||||
}
|
||||
|
||||
// 添加约束
|
||||
if (field.isAnnotationPresent(Constraint.class)) {
|
||||
Constraint constraint = field.getAnnotation(Constraint.class);
|
||||
|
||||
if (constraint.notNull()) {
|
||||
sb.append(" NOT NULL");
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(constraint.defaultValue())) {
|
||||
String apostrophe = constraint.defaultValueIsFunction() ? "" : "'";
|
||||
sb.append(" DEFAULT ").append(apostrophe).append(constraint.defaultValue()).append(apostrophe);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否忽略字段
|
||||
*/
|
||||
private static boolean isIgnoredField(Field field) {
|
||||
int modifiers = field.getModifiers();
|
||||
return java.lang.reflect.Modifier.isStatic(modifiers)
|
||||
|| java.lang.reflect.Modifier.isTransient(modifiers)
|
||||
|| field.isAnnotationPresent(TableGenIgnore.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Case 类型
|
||||
*/
|
||||
private static Case getCase(Class<?> clz) {
|
||||
return switch (clz.getName()) {
|
||||
case "io.vertx.codegen.format.CamelCase" -> io.vertx.codegen.format.CamelCase.INSTANCE;
|
||||
case "io.vertx.codegen.format.SnakeCase" -> SnakeCase.INSTANCE;
|
||||
case "io.vertx.codegen.format.LowerCamelCase" -> LowerCamelCase.INSTANCE;
|
||||
default -> SnakeCase.INSTANCE;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package cn.qaiu.db.ddl;
|
||||
|
||||
import cn.qaiu.db.pool.JDBCType;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.jdbcclient.JDBCPool;
|
||||
import io.vertx.sqlclient.templates.annotations.Column;
|
||||
import lombok.Data;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* SchemaMigration 单元测试
|
||||
*/
|
||||
public class SchemaMigrationTest {
|
||||
|
||||
private Vertx vertx;
|
||||
private JDBCPool pool;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
vertx = Vertx.vertx();
|
||||
|
||||
// 创建 H2 内存数据库连接池
|
||||
pool = JDBCPool.pool(vertx,
|
||||
"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
|
||||
"sa",
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (pool != null) {
|
||||
pool.close();
|
||||
}
|
||||
if (vertx != null) {
|
||||
vertx.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试添加新字段
|
||||
*/
|
||||
@Test
|
||||
public void testAddNewField() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 先创建一个基础表
|
||||
String createTableSQL = """
|
||||
CREATE TABLE test_user (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL
|
||||
)
|
||||
""";
|
||||
|
||||
pool.query(createTableSQL).execute()
|
||||
.compose(v -> {
|
||||
// 2. 使用 SchemaMigration 添加新字段
|
||||
return SchemaMigration.migrateTable(pool, TestUserWithNewField.class, JDBCType.H2DB);
|
||||
})
|
||||
.compose(v -> {
|
||||
// 3. 验证新字段是否添加成功
|
||||
return pool.query("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS " +
|
||||
"WHERE TABLE_NAME = 'TEST_USER' AND COLUMN_NAME = 'EMAIL'")
|
||||
.execute();
|
||||
})
|
||||
.onSuccess(rows -> {
|
||||
assertEquals("应该找到新添加的 email 字段", 1, rows.size());
|
||||
latch.countDown();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
fail("测试失败: " + err.getMessage());
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
assertTrue("测试超时", latch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试不添加已存在的字段
|
||||
*/
|
||||
@Test
|
||||
public void testSkipExistingField() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 创建包含 email 字段的表
|
||||
String createTableSQL = """
|
||||
CREATE TABLE test_user2 (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(100)
|
||||
)
|
||||
""";
|
||||
|
||||
pool.query(createTableSQL).execute()
|
||||
.compose(v -> {
|
||||
// 2. 尝试再次添加 email 字段(应该跳过)
|
||||
return SchemaMigration.migrateTable(pool, TestUserWithNewField2.class, JDBCType.H2DB);
|
||||
})
|
||||
.onSuccess(v -> {
|
||||
// 3. 验证表结构正常,没有错误
|
||||
latch.countDown();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
fail("测试失败: " + err.getMessage());
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
assertTrue("测试超时", latch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试没有 @NewField 注解时不执行迁移
|
||||
*/
|
||||
@Test
|
||||
public void testNoNewFieldAnnotation() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 创建基础表
|
||||
String createTableSQL = """
|
||||
CREATE TABLE test_user3 (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL
|
||||
)
|
||||
""";
|
||||
|
||||
pool.query(createTableSQL).execute()
|
||||
.compose(v -> {
|
||||
// 2. 使用没有 @NewField 注解的实体类
|
||||
return SchemaMigration.migrateTable(pool, TestUserNoAnnotation.class, JDBCType.H2DB);
|
||||
})
|
||||
.compose(v -> {
|
||||
// 3. 验证没有添加 email 字段
|
||||
return pool.query("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS " +
|
||||
"WHERE TABLE_NAME = 'TEST_USER3' AND COLUMN_NAME = 'EMAIL'")
|
||||
.execute();
|
||||
})
|
||||
.onSuccess(rows -> {
|
||||
assertEquals("不应该添加没有 @NewField 注解的字段", 0, rows.size());
|
||||
latch.countDown();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
fail("测试失败: " + err.getMessage());
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
assertTrue("测试超时", latch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试多个新字段同时添加
|
||||
*/
|
||||
@Test
|
||||
public void testMultipleNewFields() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 1. 创建基础表
|
||||
String createTableSQL = """
|
||||
CREATE TABLE test_user4 (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL
|
||||
)
|
||||
""";
|
||||
|
||||
pool.query(createTableSQL).execute()
|
||||
.compose(v -> {
|
||||
// 2. 添加多个新字段
|
||||
return SchemaMigration.migrateTable(pool, TestUserMultipleNewFields.class, JDBCType.H2DB);
|
||||
})
|
||||
.compose(v -> {
|
||||
// 3. 验证所有新字段都添加成功
|
||||
return pool.query("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS " +
|
||||
"WHERE TABLE_NAME = 'TEST_USER4' AND COLUMN_NAME IN ('EMAIL', 'PHONE', 'ADDRESS')")
|
||||
.execute();
|
||||
})
|
||||
.onSuccess(rows -> {
|
||||
int count = rows.iterator().next().getInteger(0);
|
||||
assertEquals("应该添加 3 个新字段", 3, count);
|
||||
latch.countDown();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
fail("测试失败: " + err.getMessage());
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
assertTrue("测试超时", latch.await(10, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
// ========== 测试实体类 ==========
|
||||
|
||||
@Data
|
||||
@Table("test_user")
|
||||
static class TestUserWithNewField {
|
||||
@Constraint(autoIncrement = true)
|
||||
private Long id;
|
||||
|
||||
@Length(varcharSize = 50)
|
||||
@Constraint(notNull = true)
|
||||
private String name;
|
||||
|
||||
@NewField("用户邮箱")
|
||||
@Length(varcharSize = 100)
|
||||
private String email;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Table("test_user2")
|
||||
static class TestUserWithNewField2 {
|
||||
@Constraint(autoIncrement = true)
|
||||
private Long id;
|
||||
|
||||
@Length(varcharSize = 50)
|
||||
@Constraint(notNull = true)
|
||||
private String name;
|
||||
|
||||
@NewField("用户邮箱")
|
||||
@Length(varcharSize = 100)
|
||||
private String email;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Table("test_user3")
|
||||
static class TestUserNoAnnotation {
|
||||
@Constraint(autoIncrement = true)
|
||||
private Long id;
|
||||
|
||||
@Length(varcharSize = 50)
|
||||
@Constraint(notNull = true)
|
||||
private String name;
|
||||
|
||||
// 没有 @NewField 注解
|
||||
@Length(varcharSize = 100)
|
||||
private String email;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Table("test_user4")
|
||||
static class TestUserMultipleNewFields {
|
||||
@Constraint(autoIncrement = true)
|
||||
private Long id;
|
||||
|
||||
@Length(varcharSize = 50)
|
||||
@Constraint(notNull = true)
|
||||
private String name;
|
||||
|
||||
@NewField("用户邮箱")
|
||||
@Length(varcharSize = 100)
|
||||
private String email;
|
||||
|
||||
@NewField("手机号")
|
||||
@Length(varcharSize = 20)
|
||||
private String phone;
|
||||
|
||||
@NewField("地址")
|
||||
@Length(varcharSize = 255)
|
||||
private String address;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user