初始化(可选)

零配置启动(默认行为)

// 无需任何初始化——直接使用即可。
// 首次操作时自动以 H2 内存数据库启动,并自动建表(autoCreateTable=true)。
// 同时会打印 WARN 日志提示当前为默认模式,生产环境应显式初始化。
Vostok.Data.insert(user);
List<User> users = Vostok.Data.findAll(User.class);
注意
默认自动初始化使用 H2 内存数据库(jdbc:h2:mem:vostok_default;MODE=MySQL),数据在进程退出后丢失,仅适用于快速原型和测试场景。生产环境请务必显式调用 init() 配置目标数据源。

显式初始化(生产环境推荐)

Vostok.Data.init(
    new VKDataConfig()
        .url("jdbc:mysql://127.0.0.1:3306/mydb")
        .username("root")
        .password("123456")
        .driver("com.mysql.cj.jdbc.Driver")
        .maxActive(20)
        .minIdle(5),
    "com.example.entity"   // 实体扫描包,可传多个
);
说明
支持 MySQL、PostgreSQL、Oracle、DB2、SQL Server,框架通过 dialect 字段自动适配分页等方言差异(也可显式指定 VKDialectType)。

实体定义

@VKEntity(table = "users")
public class User {
    @VKId(auto = true)          // auto=true:数据库自增主键,INSERT 时不包含该字段
    private Long id;

    private String name;

    @VKColumn(name = "email_addr", nullable = false, length = 100, unique = true)  // 非空、长度、唯一约束(自动建表生效)
    private String email;

    @VKColumn(name = "phone", encrypted = true)  // 开启字段透明加密
    private String phone;

    @VKColumn(name = "created_at", updatable = false)  // 只写入 INSERT,UPDATE 中忽略
    private String createdAt;

    @VKVersion                   // 乐观锁版本字段,框架自动维护递增与冲突检测
    @VKColumn(name = "version")
    private Long version;

    @VKLogicDelete(deletedValue = "1", normalValue = "0")  // 逻辑删除标志
    @VKColumn(name = "is_deleted")
    private Integer isDeleted;

    @VKIgnore                   // 标记该字段不参与持久化
    private String tempToken;

    // getters / setters
}

实体注解说明

注解作用范围说明
@VKEntity(table)标记持久化实体,table 指定表名(默认类名转下划线)
@VKId(auto)字段标记主键字段;auto=true 时 INSERT 不写入该列并回填生成的主键
@VKColumn(name, encrypted, keyId, insertable, updatable, nullable, length, unique)字段自定义列名;encrypted=true 开启透明加密;insertable=false 排除 INSERT;updatable=false 排除 UPDATE SET 子句;nullable=false 自动建表生成 NOT NULL;length 指定 VARCHAR 长度(默认 255);unique=true 自动建表生成 UNIQUE 约束
@VKVersion字段乐观锁版本字段(Long/Integer);INSERT 初始化为 0;UPDATE 自动 version+1 并校验旧版本,冲突抛 VKOptimisticLockException
@VKLogicDelete(deletedValue, normalValue)字段逻辑删除标志字段;DELETE 转为软删 UPDATE;所有查询自动注入 normalValue 过滤
@VKIgnore字段排除字段,不参与任何 SQL 操作

CRUD 操作

新增

User user = new User();
user.setName("Alice");
user.setEmail("alice@example.com");
Vostok.Data.insert(user);  // 自增主键自动写回 user.id

// 批量新增(返回总成功行数)
List<User> users = List.of(user1, user2, user3);
int count = Vostok.Data.batchInsert(users);

// 批量新增(获取每条明细)
VKBatchDetailResult detail = Vostok.Data.batchInsertDetail(users);
for (VKBatchItemResult item : detail.getItems()) {
    if (item.isSuccess()) {
        System.out.println("index=" + item.getIndex() + " key=" + item.getKey());
    } else {
        System.out.println("失败: " + item.getError());
    }
}

查询

// 按主键
User user = Vostok.Data.findById(User.class, 1L);

// 查全部
List<User> all = Vostok.Data.findAll(User.class);

// 条件查询(使用 VKQuery + VKCondition)
VKQuery q = VKQuery.create()
    .where(VKCondition.of("email", VKOperator.EQ, "alice@example.com"))
    .limit(10).offset(0);
List<User> list = Vostok.Data.query(User.class, q);

// 查询指定列(其余字段为默认值)
List<User> partial = Vostok.Data.queryColumns(
    User.class, q, "id", "name"
);

更新 / 删除

user.setName("Bob");
Vostok.Data.update(user);  // 按主键更新,返回影响行数

// 批量更新(返回总行数)
int rows = Vostok.Data.batchUpdate(users);

// 按主键删除
Vostok.Data.delete(User.class, 1L);

// 批量删除
Vostok.Data.batchDelete(User.class, List.of(1L, 2L, 3L));

// 批量操作明细(适合 CONTINUE 失败策略场景)
VKBatchDetailResult detail = Vostok.Data.batchDeleteDetail(User.class, ids);
System.out.println("成功: " + detail.totalSuccess() + " 失败: " + detail.totalFail());

任意 SQL 执行(DataResult 游标)

当 Query Builder 无法覆盖复杂场景时,可以直接执行原生 SQL。executeQuery 返回 DataResult,使用方式接近 JDBC ResultSet

import yueyang.vostok.data.DataResult;

// 查询:next() + getXxx()
try (DataResult rs = Vostok.Data.executeQuery(
        "SELECT id, user_name, age FROM t_user WHERE age > ?", 18)) {
    while (rs.next()) {
        long id = rs.getLong("id");
        String name = rs.getString("user_name");
        int age = rs.getInt("age");
        if (rs.wasNull()) {
            // age 列是 SQL NULL 时,getInt 返回 0,需要配合 wasNull() 判断
        }
    }
}

// 更新:返回影响行数
int n = Vostok.Data.executeUpdate(
    "UPDATE t_user SET age = ? WHERE id = ?", 20, 1L
);
DataResult 语义
  • 列索引为 1-based(与 JDBC ResultSet 一致)。
  • next() 到达末尾返回 false,并自动关闭资源。
  • 建议始终使用 try-with-resources 显式关闭。
  • getInt/getLong/getDouble/getBoolean 读取到 SQL NULL 时会返回零值,需用 wasNull() 判断。
安全提示
executeQuery / executeUpdate 不经过 Raw SQL 白名单,请务必使用参数占位符(?)绑定变量,避免字符串拼接导致注入风险。

乐观锁(@VKVersion)

在字段上标注 @VKVersion 即可启用乐观锁机制,无需额外配置。

@VKEntity(table = "orders")
public class Order {
    @VKId
    private Long id;

    private String title;

    @VKVersion
    @VKColumn(name = "version")
    private Long version;  // Long 或 Integer 均可
}

// INSERT:version 未设置时自动初始化为 0
Order order = new Order();
order.setTitle("New Order");
Vostok.Data.insert(order);
// → INSERT INTO orders (title, version) VALUES (?, 0)

// UPDATE:自动追加 version = version + 1,同时校验旧版本
order.setTitle("Updated");
Vostok.Data.update(order);
// → UPDATE orders SET title = ?, version = version + 1 WHERE id = ? AND version = ?
// 成功后 order.getVersion() 自动 +1,与数据库保持同步

// 并发冲突:另一事务已先行更新时,抛出 VKOptimisticLockException
Order copy1 = Vostok.Data.findById(Order.class, 1L);
Order copy2 = Vostok.Data.findById(Order.class, 1L);
Vostok.Data.update(copy1);  // 成功,版本 0→1
try {
    Vostok.Data.update(copy2);  // 失败:copy2 版本仍为 0,与数据库 1 不匹配
} catch (VKOptimisticLockException e) {
    // 重新读取最新数据后重试
    copy2 = Vostok.Data.findById(Order.class, 1L);
    Vostok.Data.update(copy2);
}
说明
  • 版本字段类型支持 LonglongIntegerint,每个实体类最多一个 @VKVersion
  • 批量更新(batchUpdate)同样在 WHERE 中带版本条件,但不抛异常——通过 VKBatchItemResult.getCount()==0 判断冲突。
  • update() 成功后,框架自动将实体版本字段 +1,无需手动同步。

逻辑删除(@VKLogicDelete)

标注 @VKLogicDelete 后,框架将所有物理删除转换为字段标记更新,所有查询自动过滤已删除记录,对业务代码完全透明。

@VKEntity(table = "articles")
public class Article {
    @VKId
    private Long id;

    private String title;

    @VKLogicDelete(deletedValue = "1", normalValue = "0")
    @VKColumn(name = "is_deleted")
    private Integer isDeleted;
}

// INSERT:is_deleted 未设置时自动初始化为 normalValue(0)
Article a = new Article();
a.setTitle("Hello");
Vostok.Data.insert(a);
// → INSERT INTO articles (title, is_deleted) VALUES (?, 0)

// DELETE:转为软删 UPDATE,物理记录保留
Vostok.Data.delete(Article.class, a.getId());
// → UPDATE articles SET is_deleted = 1 WHERE id = ?

// findById:自动过滤已删除记录,软删后返回 null
Article found = Vostok.Data.findById(Article.class, a.getId());
// found == null(记录被软删)
// → SELECT ... FROM articles WHERE id = ? AND is_deleted = 0

// findAll / query / count:自动注入 is_deleted = 0 过滤
List<Article> list = Vostok.Data.findAll(Article.class);
// → SELECT ... FROM articles WHERE is_deleted = 0

// 恢复逻辑删除记录:直接 update isDeleted = 0
Article restored = new Article();
restored.setId(a.getId());
restored.setTitle("Hello");
restored.setIsDeleted(0);  // 恢复为 normalValue
Vostok.Data.update(restored);
说明
  • 字段类型支持 StringInteger/intLong/longBoolean/boolean,注解的 deletedValue/normalValue 会按字段类型自动转换。
  • 每个实体类最多一个 @VKLogicDelete 字段。
  • 逻辑删除过滤对 query()count()aggregate() 等动态查询同样生效,调用方无需手动添加条件。
  • 如需查询所有记录(含已删除),请使用原生 SQL 或注册 Raw SQL 白名单后执行。

列约束(nullable / length / unique)

通过 @VKColumnnullablelengthunique 属性可以声明列级约束。这些属性仅在自动建表(autoCreateTable=true)时生效,不影响运行时的 INSERT / UPDATE 行为。

@VKEntity(table = "users")
public class User {
    @VKId(auto = true)
    private Long id;

    // nullable=false → NOT NULL;length=100 → VARCHAR(100);unique=true → UNIQUE
    @VKColumn(name = "email", nullable = false, length = 100, unique = true)
    private String email;

    // nullable=false, length=50 → VARCHAR(50) NOT NULL
    @VKColumn(name = "username", nullable = false, length = 50)
    private String username;

    // 默认:nullable=true, length=255 → VARCHAR(255)
    @VKColumn(name = "bio")
    private String bio;

    // Integer 包装类型 + nullable=false → INT NOT NULL
    @VKColumn(name = "score", nullable = false)
    private Integer score;
}

// 以上实体的自动建表 SQL(MySQL 方言):
// CREATE TABLE users (
//   id BIGINT AUTO_INCREMENT NOT NULL,
//   email VARCHAR(100) NOT NULL UNIQUE,
//   username VARCHAR(50) NOT NULL,
//   bio VARCHAR(255),
//   score INT NOT NULL,
//   PRIMARY KEY (id)
// )
属性类型默认值说明
nullablebooleantrue列是否允许为空;false 时自动建表生成 NOT NULL。对基本类型(int、long 等)无论此属性如何,始终生成 NOT NULL
lengthint255字符串(String)及枚举类型对应的 VARCHAR 长度。非字符串类型(INT、BIGINT 等)忽略此属性。加密字段固定使用 VARCHAR(1024),忽略 length
uniquebooleanfalse列是否添加唯一约束;true 时自动建表在列定义后追加 UNIQUE。主键列本身通过 PRIMARY KEY 保证唯一,无需设置此属性。
说明
  • 这三个属性仅影响 DDL 自动建表,框架在运行时不会校验或拦截违反约束的值——约束由数据库本身强制执行。
  • 对于已存在的表,修改这些属性不会触发 ALTER TABLE;需手动执行 DDL 或先删除表再让框架重建。
  • lengthBLOBDATETIMESTAMPDECIMAL 等固定格式类型无效。

查询构建器 VKQuery

所有条件通过 VKCondition 构建,再组合到 VKQuery 中。

VKQuery q = VKQuery.create()
    // AND 条件:status = 'active'
    .where(VKCondition.of("status", VKOperator.EQ, "active"))
    // AND 条件:age >= 18
    .where(VKCondition.of("age", VKOperator.GE, 18))
    // OR 条件组:role = 'admin' OR role = 'superuser'
    .or(
        VKCondition.of("role", VKOperator.EQ, "admin"),
        VKCondition.of("role", VKOperator.EQ, "superuser")
    )
    .orderBy(VKOrder.desc("created_at"))
    .orderBy(VKOrder.asc("name"))
    .limit(20).offset(40)
    .groupBy("department")
    // HAVING 条件:count(*) > 5
    .having(VKCondition.raw("count(*)", VKOperator.GT, 5));

List<User> result = Vostok.Data.query(User.class, q);

VKCondition 工厂方法

方法说明
VKCondition.of(field, op, values...)字段条件,支持所有 VKOperator
VKCondition.raw(expr, op, values...)原始 SQL 表达式条件,如 raw("count(*)", GT, 5)
VKCondition.inSubquery(field, subquery, params...)字段 IN (子查询)
VKCondition.notInSubquery(field, subquery, params...)字段 NOT IN (子查询)
VKCondition.exists(subquery, params...)EXISTS (子查询)
VKCondition.notExists(subquery, params...)NOT EXISTS (子查询)

VKOperator 枚举

枚举值SQL 语义
EQ= ?
NE!= ?
GT> ?
GE>= ?
LT< ?
LE<= ?
LIKELIKE ?
ININ (...)
NOT_INNOT IN (...)
BETWEENBETWEEN ? AND ?(传两个值)
IS_NULLIS NULL
IS_NOT_NULLIS NOT NULL
EXISTSEXISTS (...)
NOT_EXISTSNOT EXISTS (...)

VKOrder

VKOrder.asc("name")          // ORDER BY name ASC
VKOrder.desc("created_at")   // ORDER BY created_at DESC

复合条件组 VKConditionGroup

// 手动构造 AND 组或 OR 组
VKConditionGroup andGroup = VKConditionGroup.and(
    VKCondition.of("status", VKOperator.EQ, "active"),
    VKCondition.of("age", VKOperator.GE, 18)
);
VKConditionGroup orGroup = VKConditionGroup.or(
    VKCondition.of("role", VKOperator.EQ, "admin"),
    VKCondition.of("role", VKOperator.EQ, "moderator")
);
VKQuery q = VKQuery.create()
    .whereGroup(andGroup)
    .whereGroup(orGroup);

子查询示例

// 先注册子查询 SQL 白名单
Vostok.Data.registerSubquery("SELECT id FROM orders WHERE amount > ?");

VKQuery q = VKQuery.create()
    .where(VKCondition.inSubquery(
        "id",
        "SELECT id FROM orders WHERE amount > ?",
        1000
    ));
List<User> users = Vostok.Data.query(User.class, q);

聚合查询

VKQuery q = VKQuery.create()
    .where(VKCondition.of("status", VKOperator.EQ, "active"));

// COUNT 统计
long total = Vostok.Data.count(User.class, q);

// 多维聚合,返回 List<Object[]>,每个 Object[] 对应一行结果
// 列顺序与 VKAggregate 传入顺序一致
List<Object[]> rows = Vostok.Data.aggregate(
    User.class, q,
    VKAggregate.count("id", "total"),
    VKAggregate.avg("age", "avg_age"),
    VKAggregate.max("score", "max_score")
);
for (Object[] row : rows) {
    System.out.println("total=" + row[0] + " avg_age=" + row[1] + " max_score=" + row[2]);
}

VKAggregate 工厂方法

方法SQL
VKAggregate.count(field, alias)COUNT(field) AS alias
VKAggregate.countAll(alias)COUNT(*) AS alias
VKAggregate.sum(field, alias)SUM(field) AS alias
VKAggregate.avg(field, alias)AVG(field) AS alias
VKAggregate.min(field, alias)MIN(field) AS alias
VKAggregate.max(field, alias)MAX(field) AS alias

事务

Lambda 事务(推荐)

// 无返回值事务(默认传播:REQUIRED,隔离:DEFAULT)
Vostok.Data.tx(() -> {
    Vostok.Data.insert(order);
    Vostok.Data.update(inventory);
});

// 有返回值事务
Order saved = Vostok.Data.tx(() -> {
    Vostok.Data.insert(order);
    return Vostok.Data.findById(Order.class, order.getId());
});

// 指定传播行为与隔离级别
Vostok.Data.tx(
    () -> { Vostok.Data.insert(order); },
    VKTxPropagation.REQUIRES_NEW,
    VKTxIsolation.READ_COMMITTED
);

// 只读事务
List<User> users = Vostok.Data.tx(
    () -> Vostok.Data.findAll(User.class),
    VKTxPropagation.REQUIRED,
    VKTxIsolation.REPEATABLE_READ,
    true   // readOnly
);

手动事务管理

Vostok.Data.beginTx();
try {
    Vostok.Data.insert(order);
    Vostok.Data.update(inventory);
    Vostok.Data.commitTx();
} catch (Exception e) {
    Vostok.Data.rollbackTx();
    throw e;
}

// 指定传播与隔离级别
Vostok.Data.beginTx(VKTxPropagation.REQUIRED, VKTxIsolation.READ_COMMITTED);
// 只读
Vostok.Data.beginTx(VKTxPropagation.REQUIRED, VKTxIsolation.DEFAULT, true);

VKTxPropagation 枚举

枚举值说明
REQUIRED有事务则加入,无则新建(默认)
REQUIRES_NEW总是新建事务(挂起当前事务)
SUPPORTS有事务则加入,无则非事务执行
NOT_SUPPORTED总是非事务执行
MANDATORY必须在已有事务中执行,否则抛出异常
NEVER不能在事务中执行,否则抛出异常

VKTxIsolation 枚举

枚举值说明
DEFAULT使用数据库默认隔离级别
READ_UNCOMMITTED读未提交
READ_COMMITTED读已提交
REPEATABLE_READ可重复读
SERIALIZABLE串行化

多数据源

// 注册额外数据源
Vostok.Data.registerDataSource("analytics", new VKDataConfig()
    .url("jdbc:postgresql://analytics-db:5432/stats")
    .username("reader")
    .password("secret"));

// 切换数据源执行(无返回值)
Vostok.Data.withDataSource("analytics", () -> {
    Vostok.Data.insert(report);
});

// 切换数据源执行(有返回值)
List<Report> reports = Vostok.Data.withDataSource("analytics", () ->
    Vostok.Data.findAll(Report.class)
);

异步线程上下文传播

// 方式一:先捕获上下文,再传入异步线程
VostokContext ctx = Vostok.Data.captureContext();
CompletableFuture.runAsync(() -> ctx.run(() -> {
    Vostok.Data.findAll(User.class);
}));

// 方式二:使用 wrap() 自动捕获当前上下文,直接包装 Runnable / Supplier
Runnable task = Vostok.Data.wrap(() -> {
    Vostok.Data.findAll(User.class);
});
CompletableFuture.runAsync(task);

Supplier<List<User>> supplier = Vostok.Data.wrap(() ->
    Vostok.Data.findAll(User.class)
);
CompletableFuture.supplyAsync(supplier);

// 方式三:使用显式上下文包装
VostokContext ctx2 = Vostok.Data.captureContext();
Runnable task2 = Vostok.Data.wrap(ctx2, () -> {
    Vostok.Data.findAll(User.class);
});
executor.submit(task2);

SQL 拦截器

// VKInterceptor 接口:beforeExecute 和 afterExecute 均为默认方法,按需覆写
Vostok.Data.registerInterceptor(new VKInterceptor() {
    @Override
    public void beforeExecute(String sql, Object[] params) {
        System.out.println("执行 SQL: " + sql);
    }

    @Override
    public void afterExecute(String sql, Object[] params,
                              long costMs, boolean success, Throwable error) {
        if (costMs > 200) {
            Vostok.Log.warn("慢 SQL: {} ms → {}", costMs, sql);
        }
        if (!success && error != null) {
            Vostok.Log.error("SQL 失败: {}", error.getMessage());
        }
    }
});

// 清除所有拦截器
Vostok.Data.clearInterceptors();

SQL 白名单(安全防护)

安全说明
子查询和原始 SQL 表达式(VKCondition.raw / inSubquery / exists ...)需要先注册到白名单,否则框架会拒绝执行,防止 SQL 注入风险。
executeQuery / executeUpdate 不走白名单校验。
// 注册允许执行的原始 SQL
Vostok.Data.registerRawSql(
    "SELECT count(*) FROM audit_log WHERE created_at > ?"
);

// 注册子查询 SQL
Vostok.Data.registerSubquery(
    "SELECT id FROM orders WHERE status = ?"
);

// 针对指定数据源注册
Vostok.Data.registerRawSql("analytics", new String[]{
    "SELECT user_id FROM sessions WHERE active = ?"
});
Vostok.Data.registerSubquery("analytics", new String[]{
    "SELECT id FROM events WHERE type = ?"
});

字段透明加密

Data 模块通过 @VKColumn(encrypted=true) 注解对数据库列实现透明加密:写入时自动加密,读取时自动解密,业务代码操作实体字段如同操作明文。底层使用 Security 模块的 vkf3 格式(AES-256-GCM + DEK/KEK 双层密钥),解密为自描述模式,密文头携带密钥标识,无需调用方额外传参。

字段级密钥隔离
每个加密字段使用独立的 columnKeyId,默认规则为 表名-列名(如 users-phoneusers-id_card)。每个字段持有独立的 DEK,一个字段的密钥轮换或泄露不影响其他字段。如需覆盖,通过 @VKColumn(keyId="custom-key") 显式指定。
前置条件
字段加密依赖 Security 模块 KeyStore。需先调用 Vostok.Security.initKeyStore() 初始化密钥存储,再启用 Data 模块字段加密。
// 1. 初始化 KeyStore(字段加密所需)
Vostok.Security.initKeyStore(new VKKeyStoreConfig()
    .baseDir("./keys")
    .masterKey("my-master-secret")
);

// 2. 开启加密功能(配置阶段)
Vostok.Data.init(
    new VKDataConfig()
        .url("jdbc:mysql://...")
        .fieldEncryptionEnabled(true),
    "com.example.entity"
);

// 3. 实体字段标记加密(自动加密存储、解密读取)
@VKEntity(table = "users")
public class User {
    @VKId
    private Long id;

    // columnKeyId 自动推导为 "users-phone"(表名-列名)
    @VKColumn(name = "phone", encrypted = true)
    private String phone;

    // columnKeyId 自动推导为 "users-id_card"
    @VKColumn(name = "id_card", encrypted = true)
    private String idCard;

    // 显式指定 columnKeyId(覆盖自动推导)
    @VKColumn(name = "secret", encrypted = true, keyId = "custom-key")
    private String secret;
}

加密字段类型必须为 String,存储在数据库中的值为 vkf3 格式 Base64 字符串。解密采用自描述模式,密文头携带密钥哈希和 DEK 版本号,支持跨 DEK 版本解密(DEK 轮换后旧密文无需重加密)。

columnKeyId 推导规则

情况columnKeyId示例
未指定 keyId表名-列名users-phoneorders-address
显式 @VKColumn(keyId="k")k(原样使用)custom-key

存量数据迁移(加密 / 解密)

对已有明文数据执行批量加密,或将 vkf3 密文批量解密还原为明文。已是 vkf3 密文的行在加密时自动跳过(幂等),可分批、断点续跑。

迁移时的 columnKeyId
encryptKeyId 必须与字段在运行时使用的 columnKeyId 完全一致,否则加密结果无法被 Data 模块自动解密。
未指定 @VKColumn(keyId) 时,运行时 columnKeyId 为 表名-列名(如 users-phone),迁移时 encryptKeyId 应填写相同的值。
// 预览迁移计划(估算行数,不执行任何写入)
VKCryptoMigratePlan plan = Vostok.Data.previewEncrypt(
    new VKCryptoMigrateOptions()
        .table("users")
        .idColumn("id")
        .targetColumn("phone")
        .encryptKeyId("users-phone")  // 与字段运行时 columnKeyId 一致(表名-列名)
        .batchSize(500)
);
System.out.println("预计行数: " + plan.getEstimatedRows());

// 执行批量加密迁移(明文 → vkf3 密文)
VKCryptoMigrateResult result = Vostok.Data.encryptColumn(
    new VKCryptoMigrateOptions()
        .table("users")
        .idColumn("id")
        .targetColumn("phone")
        .encryptKeyId("users-phone")  // columnKeyId = 表名-列名
        .batchSize(500)
        .skipOnError(true)
        .useTransactionPerBatch(true)
);
System.out.printf("扫描=%d 成功=%d 失败=%d 耗时=%dms%n",
    result.getScannedRows(), result.getUpdatedRows(),
    result.getFailedRows(), result.getCostMs());

// 执行批量解密迁移(vkf3 密文 → 明文)
VKCryptoMigrateResult r2 = Vostok.Data.decryptColumn(
    new VKCryptoMigrateOptions()
        .table("users")
        .idColumn("id")
        .targetColumn("phone")
        .whereSql("status = ?")
        .whereParams("active")
        .allowPlaintextRead(true)  // 非 vkf3 行跳过,不报错
);

连接池监控

// 获取所有数据源的连接池指标
List<VKPoolMetrics> metricsList = Vostok.Data.poolMetrics();
for (VKPoolMetrics m : metricsList) {
    System.out.printf("[%s] total=%d active=%d idle=%d%n",
        m.getName(), m.getTotal(), m.getActive(), m.getIdle());
}

// 获取可读诊断报告
String report = Vostok.Data.report();
System.out.println(report);

元数据刷新

// 刷新所有已注册实体的元数据(不重新扫描包)
Vostok.Data.refreshMeta();

// 重新扫描指定包并刷新元数据
Vostok.Data.refreshMeta("com.example.entity", "com.example.model");

配置参数

参数类型默认值说明
数据库连接
urlStringJDBC 连接 URL(必填)
usernameString数据库用户名
passwordString数据库密码
driverString自动推断JDBC 驱动类名
dialectVKDialectType自动推断SQL 方言:MYSQL / POSTGRESQL / ORACLE / DB2 / SQLSERVER
连接池
minIdleint1最小空闲连接数
maxActiveint10最大活跃连接数
maxWaitMslong30000获取连接最大等待时间(ms)
testOnBorrowbooleanfalse借出时校验连接是否存活
testOnReturnbooleanfalse归还时校验连接是否存活
validationQueryString自动连接校验 SQL(优先于 isValid)
validationTimeoutSecint2连接校验超时(秒)
idleValidationIntervalMslong0空闲连接定期校验间隔(ms,<=0 不启用)
preheatEnabledbooleantrue启动时预热连接池至 minIdle
idleTimeoutMslong0空闲连接超时回收(ms,<=0 不回收)
leakDetectMslong0连接泄漏检测阈值(ms,0=关闭)
statementCacheSizeint50每连接预编译 SQL 缓存大小
sqlTemplateCacheSizeint200每数据源 SQL 模板缓存大小
externalDataSourceDataSource注入外部连接池(HikariCP 等)
closeExternalDataSourcebooleanfalse关闭模块时是否同时关闭外部连接池
重试
retryEnabledbooleanfalse操作失败后是否自动重试
maxRetriesint2最大重试次数
retryBackoffBaseMslong50指数退避基数(ms)
retryBackoffMaxMslong2000指数退避上限(ms)
retrySqlStatePrefixesString[]["08","40","57"]可重试 SQLState 前缀白名单
批操作
batchSizeint500批量操作每批大小
batchFailStrategyVKBatchFailStrategyFAIL_FAST批操作失败策略:FAIL_FAST(首失败抛异常)/ CONTINUE(跳过继续)
SQL 日志与指标
logSqlbooleanfalse打印执行的 SQL
logParamsbooleanfalse打印 SQL 参数(需配合 logSql)
slowSqlMslong0慢 SQL 阈值(ms,<=0 不记录)
sqlMetricsEnabledbooleantrue是否启用 SQL 耗时分布统计
slowSqlTopNint0慢 SQL TopN 数量(0 不保存)
事务
savepointEnabledbooleantrue是否启用 Savepoint 支持
txTimeoutMslong0事务超时(ms,0 不限制)
queryTimeoutMslong0非事务查询超时(ms,0 不限制)
DDL
autoCreateTablebooleanfalse启动时自动建表
validateDdlbooleanfalse启动时校验实体与表结构是否一致
ddlSchemaStringDDL 校验使用的 schema(可选)
字段加密(vkf3)
fieldEncryptionEnabledbooleanfalse是否开启字段透明加密(vkf3 格式)
allowPlaintextReadbooleanfalse是否允许读取非 vkf3 明文(true = 迁移期跳过,false = 格式异常时抛异常)

API 速查

初始化与生命周期

方法说明
init(VKDataConfig, String... packages)初始化数据模块并扫描实体
registerDataSource(name, VKDataConfig)注册命名数据源
refreshMeta()刷新所有实体元数据
refreshMeta(String... packages)重扫指定包并刷新元数据
started()是否已初始化
close()关闭数据模块,释放连接池

CRUD

方法返回值说明
insert(entity)int插入单条记录,自增主键写回实体
batchInsert(List)int批量插入,返回总成功行数
batchInsertDetail(List)VKBatchDetailResult批量插入,返回每条明细
update(entity)int按主键更新,返回影响行数
batchUpdate(List)int批量更新,返回总行数
batchUpdateDetail(List)VKBatchDetailResult批量更新,返回每条明细
delete(Class, id)int按主键删除,返回影响行数
batchDelete(Class, List ids)int批量删除,返回总行数
batchDeleteDetail(Class, List ids)VKBatchDetailResult批量删除,返回每条明细
findById(Class<T>, id)T按主键查询,未找到返回 null
findAll(Class<T>)List<T>查询全部记录
query(Class<T>, VKQuery)List<T>条件查询
queryColumns(Class<T>, VKQuery, fields...)List<T>查询指定列,其余字段为默认值
executeQuery(sql, params...)DataResult执行原生查询 SQL,返回游标结果
executeUpdate(sql, params...)int执行原生更新 SQL,返回影响行数
count(Class, VKQuery)long统计符合条件的记录数
aggregate(Class, VKQuery, VKAggregate...)List<Object[]>多维聚合查询,列顺序与参数一致

事务

方法说明
tx(Runnable)Lambda 事务(无返回值,默认传播和隔离)
tx(Runnable, propagation, isolation)Lambda 事务(指定传播和隔离)
tx(Runnable, propagation, isolation, readOnly)Lambda 事务(含只读标志)
tx(Supplier<T>)Lambda 事务(有返回值,默认传播和隔离)
tx(Supplier<T>, propagation, isolation)Lambda 事务(有返回值,指定传播和隔离)
tx(Supplier<T>, propagation, isolation, readOnly)Lambda 事务(有返回值,含只读标志)
beginTx()手动开启事务
beginTx(propagation, isolation)手动开启事务(指定传播和隔离)
beginTx(propagation, isolation, readOnly)手动开启事务(含只读标志)
commitTx()手动提交事务
rollbackTx()手动回滚事务

多数据源与异步

方法说明
withDataSource(name, Runnable)切换数据源执行(无返回值)
withDataSource(name, Supplier<T>)切换数据源执行(有返回值)
captureContext()捕获当前线程上下文(数据源等)
wrap(Runnable)包装 Runnable,自动绑定当前上下文
wrap(Supplier<T>)包装 Supplier,自动绑定当前上下文
wrap(VostokContext, Runnable)包装 Runnable,绑定指定上下文
wrap(VostokContext, Supplier<T>)包装 Supplier,绑定指定上下文

SQL 安全、拦截器与监控

方法说明
registerRawSql(String... sqls)注册原始 SQL 白名单(默认数据源)
registerRawSql(name, String[] sqls)注册原始 SQL 白名单(指定数据源)
registerSubquery(String... sqls)注册子查询白名单(默认数据源)
registerSubquery(name, String[] sqls)注册子查询白名单(指定数据源)
registerInterceptor(VKInterceptor)注册 SQL 拦截器
clearInterceptors()清除所有已注册拦截器
poolMetrics()获取所有数据源连接池指标列表
report()获取可读诊断报告字符串

字段加密迁移

方法说明
previewEncrypt(VKCryptoMigrateOptions)预览加密迁移计划(不执行)
previewDecrypt(VKCryptoMigrateOptions)预览解密迁移计划(不执行)
encryptColumn(VKCryptoMigrateOptions)执行存量数据批量加密迁移
decryptColumn(VKCryptoMigrateOptions)执行存量数据批量解密迁移

异常处理

try {
    Vostok.Data.insert(user);
} catch (VKSqlException e) {
    // SQL 执行失败:可获取失败 SQL、SQLState、数据库 vendor 错误码
    System.err.println(e.getCode() + " " + e.getMessage());
    System.err.println("SQL: " + e.getSql());
    System.err.println("SQLState: " + e.getSqlState());
} catch (VKOptimisticLockException e) {
    // 乐观锁冲突:@VKVersion 版本不匹配,UPDATE 影响行数为 0
    // 建议:重新查询最新实体,合并变更后重试
} catch (VKPoolException e) {
    // 连接池耗尽或连接获取超时
} catch (VKTxException e) {
    // 事务提交/回滚/传播违规
} catch (VKArgumentException e) {
    // 非法参数(如 null 实体)
} catch (VKConfigException e) {
    // 数据源配置错误
} catch (VKMetaException e) {
    // 实体元数据解析失败
}

异常类层次

异常类错误码触发场景
VKArgumentExceptionDK-400非法参数,如传入 null
VKStateExceptionDK-401模块未初始化
VKConfigExceptionDK-402配置错误
VKMetaExceptionDK-410实体元数据解析失败
VKSqlExceptionDK-500~DK-504SQL 执行失败、超时、约束违反、语法错误、连接断开
VKScanExceptionDK-510实体扫描失败
VKPoolExceptionDK-520连接池耗尽或超时
VKTxExceptionDK-530事务提交/回滚/传播违规
VKOptimisticLockExceptionDK-550乐观锁冲突(版本号不匹配,UPDATE 影响行数为 0)

完整错误码见 错误码参考