本文档详细介绍 UltiTools-API 的 ORM 数据存储系统。
UltiTools 提供统一的数据访问抽象层,让开发者无需关心底层存储实现:
- 统一 API:
DataOperator<T>接口 - 多存储支持: MySQL、SQLite、JSON
- ORM 映射: 使用
@Table和@Column注解 - 用户可选: 服务器管理员在配置中选择存储方式
┌─────────────────────────────────────────┐
│ Your Plugin Code │
│ DataOperator<PlayerData> operator │
└───────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ DataStore Interface │
└───────────────┬─────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌───────┐ ┌────────┐ ┌────────┐
│ MySQL │ │ SQLite │ │ JSON │
└───────┘ └────────┘ └────────┘
适用场景: 多服务器共享数据、高并发、生产环境
配置 (config.yml):
mysql:
enable: true
host: localhost
port: 3306
database: ultitools
username: root
password: password
datasource:
type: mysql适用场景: 单服务器、中等数据量、默认推荐
配置:
datasource:
type: sqlite数据文件位于: plugins/UltiTools/data/database.db
适用场景: 开发测试、小数据量、人工可读
配置:
datasource:
type: json数据文件位于: plugins/UltiTools/pluginConfig/<插件名>/data/<表名>/
import com.ultikits.ultitools.abstracts.AbstractDataEntity;
import com.ultikits.ultitools.annotations.Table;
import com.ultikits.ultitools.annotations.Column;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Table("player_data") // 表名/文件夹名
public class PlayerData extends AbstractDataEntity {
@Column("uuid")
private String uuid;
@Column("name")
private String name;
@Column(value = "balance", type = "DOUBLE")
private double balance;
@Column(value = "last_login", type = "BIGINT")
private long lastLogin;
@Column("is_vip")
private boolean vip;
}指定数据表/文件夹名称:
@Table("economy_accounts") // MySQL/SQLite 表名,JSON 文件夹名
public class Account extends AbstractDataEntity {
// ...
}映射字段到数据列:
@Column("column_name") // 列名
private String field;
@Column(value = "amount", type = "DECIMAL(10,2)") // 指定 SQL 类型
private BigDecimal amount;
@Column(value = "data", type = "TEXT") // 长文本
private String jsonData;支持的 SQL 类型:
| type 值 | 描述 |
|---|---|
VARCHAR(255) |
默认,字符串 |
INT |
整数 |
BIGINT |
长整数 |
DOUBLE |
双精度浮点 |
FLOAT |
单精度浮点 |
DECIMAL(p,s) |
精确数值 |
TEXT |
长文本 |
BOOLEAN |
布尔值 |
TIMESTAMP |
时间戳 |
在 UltiToolsPlugin 子类中:
public class MyPlugin extends UltiToolsPlugin {
@Override
public boolean registerSelf() {
// 获取数据操作器
DataOperator<PlayerData> operator = getDataOperator(PlayerData.class);
// 使用操作器
// ...
return true;
}
}在 Service 中:
@Service
public class PlayerService {
private final DataOperator<PlayerData> dataOperator;
public PlayerService(UltiToolsPlugin plugin) {
this.dataOperator = plugin.getDataOperator(PlayerData.class);
}
}PlayerData player = new PlayerData();
player.setUuid(uuid.toString());
player.setName("Steve");
player.setBalance(100.0);
player.setLastLogin(System.currentTimeMillis());
dataOperator.insert(player);// 通过 ID 查询
PlayerData player = dataOperator.getById(1);
// 查询所有
List<PlayerData> allPlayers = dataOperator.getAll();
// 条件查询
List<PlayerData> vipPlayers = dataOperator.getAll(
WhereCondition.builder()
.column("is_vip")
.value(true)
.build()
);// 更新单个字段
dataOperator.update("balance", 200.0, playerId);
// 更新整个实体
player.setBalance(300.0);
player.setLastLogin(System.currentTimeMillis());
dataOperator.update(player);// 通过 ID 删除
dataOperator.delById(playerId);
// 条件删除
dataOperator.del(
WhereCondition.builder()
.column("balance")
.value(0)
.comparison(Comparison.LESS_THAN)
.build()
);import com.ultikits.ultitools.entities.WhereCondition;
import com.ultikits.ultitools.entities.Comparison;
// 等于
WhereCondition eq = WhereCondition.builder()
.column("name")
.value("Steve")
.build(); // name = 'Steve'
// 大于
WhereCondition gt = WhereCondition.builder()
.column("balance")
.value(100)
.comparison(Comparison.GREATER_THAN)
.build(); // balance > 100
// 小于等于
WhereCondition lte = WhereCondition.builder()
.column("level")
.value(50)
.comparison(Comparison.LESS_THAN_OR_EQUAL)
.build(); // level <= 50
// 不等于
WhereCondition neq = WhereCondition.builder()
.column("status")
.value("banned")
.comparison(Comparison.NOT_EQUAL)
.build(); // status != 'banned'// AND 条件(多个 WhereCondition)
List<PlayerData> results = dataOperator.getAll(
WhereCondition.builder().column("is_vip").value(true).build(),
WhereCondition.builder().column("balance").value(1000).comparison(Comparison.GREATER_THAN).build()
);
// WHERE is_vip = true AND balance > 1000import cn.hutool.db.sql.Condition;
// 包含
List<PlayerData> results = dataOperator.getLike("name", "Steve", Condition.LikeType.Contains);
// name LIKE '%Steve%'
// 以...开头
List<PlayerData> results = dataOperator.getLike("name", "Ste", Condition.LikeType.StartWith);
// name LIKE 'Ste%'
// 以...结尾
List<PlayerData> results = dataOperator.getLike("name", "eve", Condition.LikeType.EndWith);
// name LIKE '%eve'// 第 1 页,每页 10 条
List<PlayerData> page1 = dataOperator.page(1, 10);
// 带条件分页
List<PlayerData> vipPage = dataOperator.page(1, 10,
WhereCondition.builder().column("is_vip").value(true).build()
);// 通过实体检查
PlayerData player = new PlayerData();
player.setUuid(uuid.toString());
boolean exists = dataOperator.exist(player);
// 通过条件检查
boolean hasRich = dataOperator.exist(
WhereCondition.builder()
.column("balance")
.value(1000000)
.comparison(Comparison.GREATER_THAN)
.build()
);虽然 UltiTools ORM 不直接支持关联查询,但可以通过手动管理实现:
@Data
@Table("player_homes")
public class PlayerHome extends AbstractDataEntity {
@Column("player_uuid")
private String playerUuid;
@Column("home_name")
private String homeName;
@Column("world")
private String world;
@Column(value = "x", type = "DOUBLE")
private double x;
@Column(value = "y", type = "DOUBLE")
private double y;
@Column(value = "z", type = "DOUBLE")
private double z;
}
// 查询玩家所有家
List<PlayerHome> homes = homeOperator.getAll(
WhereCondition.builder()
.column("player_uuid")
.value(player.getUniqueId().toString())
.build()
);当使用 JSON 存储时,数据组织如下:
plugins/UltiTools/pluginConfig/<插件名>/data/
└── player_data/
├── 1.json
├── 2.json
└── 3.json
每个 JSON 文件内容:
{
"id": 1,
"uuid": "uuid-string",
"name": "Steve",
"balance": 100.0,
"lastLogin": 1704067200000,
"vip": false
}MySQL 和 SQLite 自动使用事务,JSON 存储不支持事务。
对于需要原子操作的场景:
// MySQL/SQLite - 操作自动在事务中
try {
dataOperator.update("balance", newBalance, player1Id);
dataOperator.update("balance", newBalance2, player2Id);
} catch (Exception e) {
// 发生错误时自动回滚
logger.error("转账失败", e);
}@Service
public class PlayerDataService {
private final DataOperator<PlayerData> dataOperator;
@Autowired
public PlayerDataService(MyPlugin plugin) {
this.dataOperator = plugin.getDataOperator(PlayerData.class);
}
/**
* 获取或创建玩家数据
*/
public PlayerData getOrCreate(Player player) {
List<PlayerData> results = dataOperator.getAll(
WhereCondition.builder()
.column("uuid")
.value(player.getUniqueId().toString())
.build()
);
if (!results.isEmpty()) {
PlayerData data = results.get(0);
// 更新最后登录时间
data.setLastLogin(System.currentTimeMillis());
data.setName(player.getName()); // 更新可能变化的名字
try {
dataOperator.update(data);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return data;
}
// 创建新数据
PlayerData newData = new PlayerData();
newData.setUuid(player.getUniqueId().toString());
newData.setName(player.getName());
newData.setBalance(0.0);
newData.setLastLogin(System.currentTimeMillis());
newData.setVip(false);
dataOperator.insert(newData);
return newData;
}
/**
* 增加余额
*/
public void addBalance(String uuid, double amount) {
List<PlayerData> results = dataOperator.getAll(
WhereCondition.builder().column("uuid").value(uuid).build()
);
if (!results.isEmpty()) {
PlayerData data = results.get(0);
dataOperator.update("balance", data.getBalance() + amount, data.getId());
}
}
/**
* 获取财富排行榜
*/
public List<PlayerData> getTopBalances(int limit) {
List<PlayerData> all = dataOperator.getAll();
return all.stream()
.sorted((a, b) -> Double.compare(b.getBalance(), a.getBalance()))
.limit(limit)
.collect(Collectors.toList());
}
/**
* 获取 VIP 玩家
*/
public List<PlayerData> getVipPlayers() {
return dataOperator.getAll(
WhereCondition.builder().column("is_vip").value(true).build()
);
}
/**
* 搜索玩家
*/
public List<PlayerData> searchByName(String keyword) {
return dataOperator.getLike("name", keyword, Condition.LikeType.Contains);
}
}-
使用 Lombok 简化实体类
@Data @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) @Table("my_table") public class MyEntity extends AbstractDataEntity { // ... }
-
为常用查询封装方法
public PlayerData findByUuid(String uuid) { List<PlayerData> results = dataOperator.getAll( WhereCondition.builder().column("uuid").value(uuid).build() ); return results.isEmpty() ? null : results.get(0); }
-
使用服务层封装数据操作
-
考虑数据迁移
- 添加新字段时提供默认值
- 避免删除或重命名字段
-
为大表使用分页查询
// 避免 List<PlayerData> all = dataOperator.getAll(); // 可能很慢 // 推荐 List<PlayerData> page = dataOperator.page(1, 100);
-
避免在主线程进行大量数据操作
// 在异步线程中操作 Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { List<PlayerData> data = dataOperator.getAll(); // 处理数据... });
-
避免存储过多冗余数据
-
避免频繁的单条更新
- 批量操作优于多次单条操作
-
避免在实体中存储非序列化对象
// 错误 - Location 无法直接序列化 @Column("location") private Location location; // 正确 - 分开存储 @Column("world") private String world; @Column(value = "x", type = "DOUBLE") private double x; // ...
下一步: 阅读 配置管理 了解配置系统