未发布版本 v6.3.0-SNAPSHOT。 本页内容来自 alpha 分支,随时可能变更,不属于任何已发布版本。

commit 49619c6 · 注入于 2026-09-13 12:23 UTC

Skip to content

数据储存

UltiTools 封装了一套数据储存 API,它支持 MySQL 数据库、SQLite 数据库(6.1.0起)与 JSON 文件储存。数据存储对于开发者来说是透明的,UltiTools将通过服主的配置判断使用哪种存储方式。

你需要的仅仅只是一个实体类。CRUD 操作将由 UltiTools 自动完成。

尽量不要嵌套对象

由于插件还处于开发状态,难免在处理复杂对象时出现问题,所以存储的对象尽量不要超过两层嵌套(尽量不要嵌套对象)。

创建实体类

BaseDataEntity

创建一个继承 BaseDataEntity<String> 的类,并使用 @Table@Column 注解来标记你的实体类。

java
package com.ultikits.docs.data;

import com.ultikits.ultitools.abstracts.data.BaseDataEntity;
import com.ultikits.ultitools.annotations.Column;
import com.ultikits.ultitools.annotations.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Table("user_data")
public class UserData extends BaseDataEntity<String> {
    @Column("player_name")
    private String playerName;

    @Column(value = "balance", type = "FLOAT")
    private double balance;
}

其中,@Table 注解用于标记该类对应的数据表(若使用 MySQL 数据库),@Column 注解用于标记该类的字段对应的数据表的列。

@Data@Builder@NoArgsConstructor@AllArgsConstructor@EqualsAndHashCode 则为 Lombok 注解,用于自动生成 gettersetterbuilderequalshashCode 方法。

从 AbstractDataEntity 迁移

从 v6.2.0 开始,DataOperatorQueryUltiToolsPlugin.getDataOperator() 要求实体继承 BaseDataEntity<String> 而非 AbstractDataEntity。如果你的实体仍然继承 AbstractDataEntity,请改为 BaseDataEntity<String>

BaseDataEntity<String> 提供了插入/更新/删除/加载事件的生命周期钩子:

方法说明
onCreate()在实体首次持久化之前调用
onUpdate()在实体更新之前调用
onDelete()在实体删除之前调用
onLoad()在从数据存储加载实体后调用
validate()实体有效返回 true
isNew()实体无 ID 时返回 true
copyWithoutId()创建不含 ID 的实体副本,前提是实体类自行实现 Cloneable

生命周期钩子由你的代码调用,而不是由操作器调用

onCreate()onUpdate()onDelete()onLoad() 声明在 BaseDataEntity 上,但 JSON、MySQL 与 SQLite 三个操作器的读写路径都不调用它们,因此重写这些方法的实体落库结果与不重写完全一致。 在操作前后自己调一次,写入前 entity.onCreate(); op.insert(entity);,读取则在返回的实体上调 entity.onLoad();:这四个方法都是 public。 让操作器调用这些钩子的修法跟踪于 issue #194

AuditableDataEntity

对于需要跟踪创建和修改的实体,可以使用 AuditableDataEntity

java
package com.ultikits.docs.data;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.abstracts.data.AuditableDataEntity;
import com.ultikits.ultitools.abstracts.data.BaseDataEntity;
import com.ultikits.ultitools.annotations.*;
import com.ultikits.ultitools.interfaces.DataOperator;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.bukkit.entity.Player;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Table("audit_log")
public class AuditEntry extends AuditableDataEntity<String> {
    @Column("action")
    private String action;
    @Column("details")
    private String details;
}

AuditableDataEntity<String> 继承了 BaseDataEntity<String>,自动管理以下字段:

字段类型说明
createdAtLocalDateTime实体创建时间(在 onCreate() 中自动设置)
updatedAtLocalDateTime上次修改时间(在 onUpdate() 中自动更新)
createdByUUID创建实体的用户 ID(从线程本地上下文获取)
updatedByUUID上次修改实体的用户 ID(从线程本地上下文获取)

所有这四个字段都已预配置 @Column 注解,子类中无需声明。

四个审计列在插入后仍为 NULL

由于操作器不调用生命周期钩子,onCreate()onUpdate() 不会执行,created_atupdated_atcreated_byupdated_by 四列因此不被写入,wasModified() 恒返回 falsegetAge()getTimeSinceUpdate() 恒返回 null。 先用 AuditableDataEntity.setCurrentUser(uuid) 设置线程上下文,在写入前调用 entity.onCreate()entity.onUpdate(),并在 finally 中清除上下文:不设置上下文时,即使钩子执行,两个 by 字段仍为 null。 让操作器调用这些钩子的修法跟踪于 issue #194

用户上下文管理

要跟踪执行操作的用户,需要在数据库操作前设置当前用户:

java
import com.ultikits.ultitools.abstracts.data.AuditableDataEntity;

UUID currentUserId = player.getUniqueId();
AuditableDataEntity.setCurrentUser(currentUserId);

try {
    DataOperator<AuditEntry> op = plugin.getDataOperator(AuditEntry.class);
    AuditEntry entry = AuditEntry.builder()
        .action("login")
        .details("玩家从 192.168.1.1 登录")
        .build();
    op.insert(entry);  // createdBy 和 updatedBy 自动设置
} finally {
    AuditableDataEntity.clearCurrentUser();
}

必须清除上下文

使用 try-finally 块确保调用 clearCurrentUser(),否则 ThreadLocal 上下文会持续存在于后续请求中,可能导致用户身份泄露。

工具方法

AuditableDataEntity 提供了便利的时间相关查询方法:

方法返回值说明
getAge()Durationnull实体自创建以来经过的时间
getTimeSinceUpdate()Durationnull实体自上次修改以来经过的时间
wasModified()boolean实体是否在创建后被修改过

使用示例:

java
AuditEntry entry = op.getById("some-id");
if (entry.wasModified()) {
    System.out.println("修改于 " + entry.getTimeSinceUpdate().getSeconds() + " 秒前");
}

空值安全

如果实体尚未持久化(缺少 createdAtupdatedAt),getAge()getTimeSinceUpdate() 会返回 null。在调用返回的 Duration 上的方法前,务必检查 null。

@Table 注解

@Table 注解有一个 value 属性,用于指定该类对应的数据表或文件夹的名称。

@Column 注解

@Column 注解有两个属性,value 属性用于指定该字段对应的数据表的列,type 属性用于指定该字段对应的数据表的列的类型。

type 属性的默认值为 VARCHAR(255)

可用的类型可参见 MySQL 数据类型

CRUD 操作

UltiTools 封装了一套语义化的 CRUD 操作 API,你只需要调用相应的方法,即可完成对数据的增删改查。

DataOperator

DataOperator 用于数据操作。

在继承了 UltiToolsPlugin 的主类中,有一个 getDataOperator 方法,用于获取数据操作器。

你需要获取插件主类的实例,然后调用 getDataOperator 方法。

java
package com.ultikits.docs.data;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.entities.WhereCondition;
import com.ultikits.ultitools.interfaces.DataOperator;

import java.util.List;

public class UserDataService {

    public void save(UltiToolsPlugin plugin, UserData data) {
        DataOperator<UserData> operator = plugin.getDataOperator(UserData.class);
        operator.insert(data);
    }

    public List<UserData> findByName(UltiToolsPlugin plugin, String name) {
        DataOperator<UserData> operator = plugin.getDataOperator(UserData.class);
        return operator.getAll(
                WhereCondition.builder().column("player_name").value(name).build()
        );
    }
}

请即取即用

DataOperator 不是线程安全的,请在需要的时候获取 DataOperator,不要试图保存 DataOperator 对象。

插入

java
SomeEntity entity = SomeEntity.builder()
    .name("test")
    .something(42.0)
    .build();
dataOperator.insert(entity);

查询

使用 WhereCondition

java
List<SomeEntity> list = dataOperator.getAll(
    WhereCondition.builder()
        .column("name")
        .value("test")
        .build()
);

或按 ID 获取单个实体:

java
SomeEntity entity = dataOperator.getById("some-id");

获取所有实体:

java
List<SomeEntity> all = dataOperator.getAll();

分页查询:

java
List<SomeEntity> page = dataOperator.page(1, 10); // 第 1 页,每页 10 条

page() 在 JSON 后端返回空列表

在 JSON 后端上,page(int, int) 转交 getAll(WhereCondition...),零长度参数走的分支返回空列表,而同一次调用在 MySQL 或 SQLite 上会拼出普通的 LIMIT ? OFFSET ? 并返回该页数据,同一份模块代码换后端结果不同。 需要两端一致的分页时,改用 getAll() 取全量再用 subList 切片:page(1, 10, WhereCondition.empty()) 不能替代,关系型操作器不过滤空条件,会拼出 WHERE null = ?。 让 pageexist 在空条件上与 getAll 对齐的修法跟踪于 issue #193

查询 DSL

从 v6.2.0 开始,你可以使用流式查询 DSL 来编写更可读的查询:

java
SomeEntity entity = dataOperator.query()
    .where("name").eq("test")
    .first();

更新

更新单个字段:

java
dataOperator.update("name", "newName", entityId);

使用实体对象更新:

java
try {
    entity.setName("newName");
    dataOperator.update(entity);
} catch (IllegalAccessException e) {
    // 处理异常或继续向上抛出
}

该重载声明了受检异常 IllegalAccessException,调用方需要声明或捕获它。

删除

按 ID 删除:

java
dataOperator.delById(entityId);

按条件删除:

java
dataOperator.del(
    WhereCondition.builder()
        .column("name")
        .value("test")
        .build()
);

WhereCondition

WhereCondition 用于指定查询条件。

java
WhereCondition.builder().column("somecol").value(someval).build();

其中,column 属性用于指定查询的列,value 属性用于指定查询的值。

事务

对于需要同时成功或同时失败的操作,请参阅事务指南。

只有 JSON 后端会回滚这段代码

MySQL 与 SQLite 的操作器在构造时不注入事务管理器,而 transaction(...) 在管理器为 null 时直接执行回调,连接始终处于 autocommit 状态,下面每一条 insert 各自独立提交。 需要这段代码原子时,改用 JSON 后端,或自取 JDBC 连接、关闭 autocommit 并自行提交或回滚:事务指南对两种做法都有说明。 把事务管理器接进关系型操作器的修法跟踪于 issue #307

java
dataOperator.transaction(() -> {
    dataOperator.insert(entity1);
    dataOperator.insert(entity2);
    // 全部插入或全部不插入
});

贡献者

暂无相关贡献者

基于 MIT 许可发布