这是构建 Java DAO 的正确方法吗?
Is this a correct way to struct Java DAO?
我正在尝试在我的 Minecraft 服务器中开发一个统计/成就系统。
我做了一些研究,但仍然不能很好地做出决定,所以决定 post 我在堆栈溢出中的第一个问题。
有多种类型的成就,比如破坏方块、收割庄稼、杀死动物..等等。 Table 初始值设定项如下所示。 (我故意将这些值设置为 double )
public static void init() {
String query = "CREATE TABLE IF NOT EXISTS statistic ("
+ " uuid VARCHAR(255) PRIMARY KEY,"
+ " block_break double, crop_break double, ore_break double,"
+ " wood_break double, animal_kill double, monster_kill double, boss_kill double,"
+ " fish_natural double, fish_auto double "
+ ")";
try {
Connection conn = HikariPoolManager.getInstance().getConnection();
PreparedStatement ps = conn.prepareStatement(query);
ps.execute();
} catch (SQLException e) {
e.printStackTrace();
}
}
我用这个
把它存回去
public static void save(String uuidString, StatisticsType stat, double val) {
String query= "INSERT INTO statistics (uuid, {stat}) "
+" VALUE (?,?) ON DUPLICATE KEY UPDATE "
+" uuid=VALUES( uuid ), {stat}=VALUES( {stat} )"
.replace("{stat}", stat.name());
try (Connection conn = HikariPoolManager.getInstance().getConnection();
PreparedStatement ps = conn.prepareStatement(query)
){
ps.setString(1, uuidString);
ps.setDouble(2, val);
ps.execute();
} catch (SQLException e) {
e.printStackTrace();
}
}
PlayerCache.java
public class PlayerCache {
@Getter
private static final Map<UUID, PlayerCache> cacheMap = new HashMap<>();
private final UUID uuid;
@Getter
private HashMap<StatisticsType, Double> statistics;
@Getter
private HashSet<StatisticsType> changed;
public PlayerCache(UUID uuid) {
this.uuid= uuid;
}
public void init(HashMap<StatisticsType,Double> achievements) {
this.statistics = new HashMap<>();
this.changed = new HashSet<>();
this.statistics.putAll(achievements);
}
public void addValue(StatisticsType type, double addition) {
statistics.put(type, statistics.get(type) + addition);
changed.add(type);
}
public double getStatistic(StatisticsType type) {
return statistics.getOrDefault(type, 0.0D);
}
public void save() {
for (StatisticsType statisticsType : changed) {
StatisticDAO.save(uuid.toString(),statisticsType, getStatistic(statisticsType));
}
changed.clear();
}
public static PlayerCache get(final UUID uuid) {
PlayerCache playerCache = cacheMap.get(uuid);
if (playerCache == null) {
playerCache = new PlayerCache(uuid);
cacheMap.put(uuid, playerCache);
}
return playerCache;
}
}
我对编程的一般设计有疑问,而不是修复代码本身。
目前,事情是这样的。
为简单起见,让我选择两个统计动作-打破石头和杀死怪物。
玩家加入游戏,读取数据,对玩家进行缓存,并将统计信息放入缓存。
玩家打碎石头,并增加玩家缓存中的统计数据。
如果玩家打碎了石头,它会切换一个布尔标志以表明他打碎了石头,因此此信息需要在某个时候刷新到数据库。
服务器循环所有玩家,并检查玩家是否做了任何事情。如果玩家做了什么,它会调用 sql 保存方法,并切换布尔标志。
不过,我这次遇到的问题很少。
玩家可以在写入数据库的过程中打碎石头,杀死怪物。甚至更多不同的动作。这将导致从玩家调用多个保存功能。有没有更好的方法来处理这个问题?
我读写数据的一般方法是否正确?我几乎使用相同的方式对其他功能执行数据库操作,但不确定这是否是好方法。
That will result multiple save functions to be called from a player. Is there better way to deal with this?
我认为您正在加剧玩家 运行 多个 SQL 插入语句的严重性。在 Minecraft 服务器上,数据库根本不会有太多负载,而且您使用 Hikari 的事实确保了这些额外的少数查询对性能的影响可以忽略不计。
但是,如果您非常确定您工作的环境对性能非常敏感(对于 Minecraft 插件来说可能不是),那么请考虑 运行 batch SQL statements 或手动将同一玩家的更新合并到一个语句中并将其发送到 SQL 数据库。
对我来说,你应该使用一个对象来保存所有的信息,并且只在你需要的时候保存它们。例如:StatsPlayer
.
您有一张静态地图:HashMap<UUID, StatsPlayer>
包含所有玩家实例。
每个实例都包含这样的所有信息:
private HashMap<StatisticsType, Double> stats;
或者:
private double blockBreak;
创建新实例时,不要忘记从数据库获取信息,例如:
public StatsPlayer(Player p) {
try {
Connection conn = HikariPoolManager.getInstance().getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM statistics WHERE uuid = ?");
ps.setString(1, p.getUniqueId().toString());
ResultSet rs = ps.executeQuery();
if(rs.next()) {
// get informations from ResultSet instance
} else {
// Insert line into database
}
} catch(Exception e) {
e.printStackTrace();
}
}
现在,你必须像那样做静态 getter :
public static StatsPlayer getPlayer(Player p) {
synchronized(PLAYERS) {
return PLAYERS.computeIfAbsent(p, StatsPlayer::new);
}
}
在您的 StatsPlayer
对象中,您应该添加方法来更新值,并添加一个方法来保存所有内容:
public void save() {
try {
Connection conn = HikariPoolManager.getInstance().getConnection();
PreparedStatement ps = conn.prepareStatement("UPDATE statistics SET block_break = ? WHERE uuid = ?");
ps.setDouble(1, getBlockBreak());
ps.setString(2, p.getUniqueId().toString());
ps.executeUpdate(); // make the update
} catch(Exception e) {
e.printStackTrace();
}
}
最后,有时您应该保存对象,例如仅当他们离开服务器或服务器停止时
在极少数情况下,这是做任何事情的唯一正确方法;但是您确实提到了数据访问对象。 DAO 遵循一种模式,但这种模式是通用的。根据您的需要,实际对象可能包含更多(或更少)数据,或者结构化为一个(或多个 tables)。
DAO 模式是一种 class,可对数据库执行所有直接操作。它至少包括 add(Object)
、remove(Object)
、get(id)
,也许 getAll()
,但它也可以包括 getOldest()
、remove(id)
,等等。
它通常不应该做的是直接公开底层 table 结构。因此,从某种意义上说,您的方法(通过公开 UUID 和要独立更新的统计信息)没有遵循该模式。
public class PlayerStats {
// contains a UUID field, as well as other stat fields
}
public class PlayerStatsDAO {
public PlayerStatsDAO(DatabaseConnection connection) {
// store the connection and check the connection
}
public void update(PlayerStats value) {
}
public void add(PlayerStats value) {
}
public void addOrUpdate(PlayerStats value) {
}
public PlayerStats newEmptyStats() {
}
public void remove(PlayerStats value) {
}
// as well as searching methods
public PlayerStats statsForUUID(UUID uuid) {
}
public PlayerStats statsForPlayerName(String name) {
}
public PlayerStats mostBockBreaks() {
}
... etc ...
}
DAO 的优势在于,如果您稍后决定更改基础 table(或一组连接的 table),您可以在一个位置绑定现有的“数据对象”到新的 table 结构。
我正在尝试在我的 Minecraft 服务器中开发一个统计/成就系统。
我做了一些研究,但仍然不能很好地做出决定,所以决定 post 我在堆栈溢出中的第一个问题。
有多种类型的成就,比如破坏方块、收割庄稼、杀死动物..等等。 Table 初始值设定项如下所示。 (我故意将这些值设置为 double )
public static void init() {
String query = "CREATE TABLE IF NOT EXISTS statistic ("
+ " uuid VARCHAR(255) PRIMARY KEY,"
+ " block_break double, crop_break double, ore_break double,"
+ " wood_break double, animal_kill double, monster_kill double, boss_kill double,"
+ " fish_natural double, fish_auto double "
+ ")";
try {
Connection conn = HikariPoolManager.getInstance().getConnection();
PreparedStatement ps = conn.prepareStatement(query);
ps.execute();
} catch (SQLException e) {
e.printStackTrace();
}
}
我用这个
把它存回去 public static void save(String uuidString, StatisticsType stat, double val) {
String query= "INSERT INTO statistics (uuid, {stat}) "
+" VALUE (?,?) ON DUPLICATE KEY UPDATE "
+" uuid=VALUES( uuid ), {stat}=VALUES( {stat} )"
.replace("{stat}", stat.name());
try (Connection conn = HikariPoolManager.getInstance().getConnection();
PreparedStatement ps = conn.prepareStatement(query)
){
ps.setString(1, uuidString);
ps.setDouble(2, val);
ps.execute();
} catch (SQLException e) {
e.printStackTrace();
}
}
PlayerCache.java
public class PlayerCache {
@Getter
private static final Map<UUID, PlayerCache> cacheMap = new HashMap<>();
private final UUID uuid;
@Getter
private HashMap<StatisticsType, Double> statistics;
@Getter
private HashSet<StatisticsType> changed;
public PlayerCache(UUID uuid) {
this.uuid= uuid;
}
public void init(HashMap<StatisticsType,Double> achievements) {
this.statistics = new HashMap<>();
this.changed = new HashSet<>();
this.statistics.putAll(achievements);
}
public void addValue(StatisticsType type, double addition) {
statistics.put(type, statistics.get(type) + addition);
changed.add(type);
}
public double getStatistic(StatisticsType type) {
return statistics.getOrDefault(type, 0.0D);
}
public void save() {
for (StatisticsType statisticsType : changed) {
StatisticDAO.save(uuid.toString(),statisticsType, getStatistic(statisticsType));
}
changed.clear();
}
public static PlayerCache get(final UUID uuid) {
PlayerCache playerCache = cacheMap.get(uuid);
if (playerCache == null) {
playerCache = new PlayerCache(uuid);
cacheMap.put(uuid, playerCache);
}
return playerCache;
}
}
我对编程的一般设计有疑问,而不是修复代码本身。
目前,事情是这样的。 为简单起见,让我选择两个统计动作-打破石头和杀死怪物。
玩家加入游戏,读取数据,对玩家进行缓存,并将统计信息放入缓存。
玩家打碎石头,并增加玩家缓存中的统计数据。
如果玩家打碎了石头,它会切换一个布尔标志以表明他打碎了石头,因此此信息需要在某个时候刷新到数据库。
服务器循环所有玩家,并检查玩家是否做了任何事情。如果玩家做了什么,它会调用 sql 保存方法,并切换布尔标志。
不过,我这次遇到的问题很少。
玩家可以在写入数据库的过程中打碎石头,杀死怪物。甚至更多不同的动作。这将导致从玩家调用多个保存功能。有没有更好的方法来处理这个问题?
我读写数据的一般方法是否正确?我几乎使用相同的方式对其他功能执行数据库操作,但不确定这是否是好方法。
That will result multiple save functions to be called from a player. Is there better way to deal with this?
我认为您正在加剧玩家 运行 多个 SQL 插入语句的严重性。在 Minecraft 服务器上,数据库根本不会有太多负载,而且您使用 Hikari 的事实确保了这些额外的少数查询对性能的影响可以忽略不计。
但是,如果您非常确定您工作的环境对性能非常敏感(对于 Minecraft 插件来说可能不是),那么请考虑 运行 batch SQL statements 或手动将同一玩家的更新合并到一个语句中并将其发送到 SQL 数据库。
对我来说,你应该使用一个对象来保存所有的信息,并且只在你需要的时候保存它们。例如:StatsPlayer
.
您有一张静态地图:HashMap<UUID, StatsPlayer>
包含所有玩家实例。
每个实例都包含这样的所有信息:
private HashMap<StatisticsType, Double> stats;
或者:
private double blockBreak;
创建新实例时,不要忘记从数据库获取信息,例如:
public StatsPlayer(Player p) {
try {
Connection conn = HikariPoolManager.getInstance().getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM statistics WHERE uuid = ?");
ps.setString(1, p.getUniqueId().toString());
ResultSet rs = ps.executeQuery();
if(rs.next()) {
// get informations from ResultSet instance
} else {
// Insert line into database
}
} catch(Exception e) {
e.printStackTrace();
}
}
现在,你必须像那样做静态 getter :
public static StatsPlayer getPlayer(Player p) {
synchronized(PLAYERS) {
return PLAYERS.computeIfAbsent(p, StatsPlayer::new);
}
}
在您的 StatsPlayer
对象中,您应该添加方法来更新值,并添加一个方法来保存所有内容:
public void save() {
try {
Connection conn = HikariPoolManager.getInstance().getConnection();
PreparedStatement ps = conn.prepareStatement("UPDATE statistics SET block_break = ? WHERE uuid = ?");
ps.setDouble(1, getBlockBreak());
ps.setString(2, p.getUniqueId().toString());
ps.executeUpdate(); // make the update
} catch(Exception e) {
e.printStackTrace();
}
}
最后,有时您应该保存对象,例如仅当他们离开服务器或服务器停止时
在极少数情况下,这是做任何事情的唯一正确方法;但是您确实提到了数据访问对象。 DAO 遵循一种模式,但这种模式是通用的。根据您的需要,实际对象可能包含更多(或更少)数据,或者结构化为一个(或多个 tables)。
DAO 模式是一种 class,可对数据库执行所有直接操作。它至少包括 add(Object)
、remove(Object)
、get(id)
,也许 getAll()
,但它也可以包括 getOldest()
、remove(id)
,等等。
它通常不应该做的是直接公开底层 table 结构。因此,从某种意义上说,您的方法(通过公开 UUID 和要独立更新的统计信息)没有遵循该模式。
public class PlayerStats {
// contains a UUID field, as well as other stat fields
}
public class PlayerStatsDAO {
public PlayerStatsDAO(DatabaseConnection connection) {
// store the connection and check the connection
}
public void update(PlayerStats value) {
}
public void add(PlayerStats value) {
}
public void addOrUpdate(PlayerStats value) {
}
public PlayerStats newEmptyStats() {
}
public void remove(PlayerStats value) {
}
// as well as searching methods
public PlayerStats statsForUUID(UUID uuid) {
}
public PlayerStats statsForPlayerName(String name) {
}
public PlayerStats mostBockBreaks() {
}
... etc ...
}
DAO 的优势在于,如果您稍后决定更改基础 table(或一组连接的 table),您可以在一个位置绑定现有的“数据对象”到新的 table 结构。