数据储存
UltiTools 封装了一套数据储存 API,它支持 MySQL 数据库、SQLite 数据库(6.1.0起)与 JSON 文件储存。数据存储对于开发者来说是透明的,UltiTools将通过服主的配置判断使用哪种存储方式。
你需要的仅仅只是一个实体类。CRUD 操作将由 UltiTools 自动完成。
尽量不要嵌套对象
由于插件还处于开发状态,难免在处理复杂对象时出现问题,所以存储的对象尽量不要超过两层嵌套(尽量不要嵌套对象)。
创建实体类
BaseDataEntity
创建一个继承 BaseDataEntity<String> 的类,并使用 @Table 和 @Column 注解来标记你的实体类。
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 注解,用于自动生成 getter、setter、builder、equals、hashCode 方法。
从 AbstractDataEntity 迁移
从 v6.2.0 开始,DataOperator、Query 和 UltiToolsPlugin.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:
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>,自动管理以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
createdAt | LocalDateTime | 实体创建时间(在 onCreate() 中自动设置) |
updatedAt | LocalDateTime | 上次修改时间(在 onUpdate() 中自动更新) |
createdBy | UUID | 创建实体的用户 ID(从线程本地上下文获取) |
updatedBy | UUID | 上次修改实体的用户 ID(从线程本地上下文获取) |
所有这四个字段都已预配置 @Column 注解,子类中无需声明。
四个审计列在插入后仍为 NULL
由于操作器不调用生命周期钩子,onCreate() 与 onUpdate() 不会执行,created_at、updated_at、created_by、updated_by 四列因此不被写入,wasModified() 恒返回 false,getAge() 与 getTimeSinceUpdate() 恒返回 null。 先用 AuditableDataEntity.setCurrentUser(uuid) 设置线程上下文,在写入前调用 entity.onCreate() 或 entity.onUpdate(),并在 finally 中清除上下文:不设置上下文时,即使钩子执行,两个 by 字段仍为 null。 让操作器调用这些钩子的修法跟踪于 issue #194。
用户上下文管理
要跟踪执行操作的用户,需要在数据库操作前设置当前用户:
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() | Duration 或 null | 实体自创建以来经过的时间 |
getTimeSinceUpdate() | Duration 或 null | 实体自上次修改以来经过的时间 |
wasModified() | boolean | 实体是否在创建后被修改过 |
使用示例:
AuditEntry entry = op.getById("some-id");
if (entry.wasModified()) {
System.out.println("修改于 " + entry.getTimeSinceUpdate().getSeconds() + " 秒前");
}空值安全
如果实体尚未持久化(缺少 createdAt 或 updatedAt),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 方法。
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 对象。
插入
SomeEntity entity = SomeEntity.builder()
.name("test")
.something(42.0)
.build();
dataOperator.insert(entity);查询
使用 WhereCondition:
List<SomeEntity> list = dataOperator.getAll(
WhereCondition.builder()
.column("name")
.value("test")
.build()
);或按 ID 获取单个实体:
SomeEntity entity = dataOperator.getById("some-id");获取所有实体:
List<SomeEntity> all = dataOperator.getAll();分页查询:
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 = ?。 让 page 与 exist 在空条件上与 getAll 对齐的修法跟踪于 issue #193。
查询 DSL
从 v6.2.0 开始,你可以使用流式查询 DSL 来编写更可读的查询:
SomeEntity entity = dataOperator.query()
.where("name").eq("test")
.first();更新
更新单个字段:
dataOperator.update("name", "newName", entityId);使用实体对象更新:
try {
entity.setName("newName");
dataOperator.update(entity);
} catch (IllegalAccessException e) {
// 处理异常或继续向上抛出
}该重载声明了受检异常 IllegalAccessException,调用方需要声明或捕获它。
删除
按 ID 删除:
dataOperator.delById(entityId);按条件删除:
dataOperator.del(
WhereCondition.builder()
.column("name")
.value("test")
.build()
);WhereCondition
WhereCondition 用于指定查询条件。
WhereCondition.builder().column("somecol").value(someval).build();其中,column 属性用于指定查询的列,value 属性用于指定查询的值。
事务
对于需要同时成功或同时失败的操作,请参阅事务指南。
只有 JSON 后端会回滚这段代码
MySQL 与 SQLite 的操作器在构造时不注入事务管理器,而 transaction(...) 在管理器为 null 时直接执行回调,连接始终处于 autocommit 状态,下面每一条 insert 各自独立提交。 需要这段代码原子时,改用 JSON 后端,或自取 JDBC 连接、关闭 autocommit 并自行提交或回滚:事务指南对两种做法都有说明。 把事务管理器接进关系型操作器的修法跟踪于 issue #307。
dataOperator.transaction(() -> {
dataOperator.insert(entity1);
dataOperator.insert(entity2);
// 全部插入或全部不插入
});