【问题标题】:Is this a correct way to struct Java DAO?这是构建 Java DAO 的正确方法吗?
【发布时间】:2021-12-05 22:16:40
【问题描述】:

我正在尝试在我的 Minecraft 服务器中开发一个统计/成就系统。

我做了一些研究,但仍然不能很好地做出决定,所以决定在堆栈溢出中发布我的第一个问题。

有多种类型的成就,例如破坏方块,收获作物,杀死动物......等等。表初始化程序如下所示。 (我故意将这些值设置为双精度)

    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;
    }

}

我对编程的一般设计有疑问,而不是修复代码本身。

目前,事情就是这样发展的。 为简单起见,让我选择两个统计动作 - 碎石和杀怪。

  1. 玩家加入游戏,读取数据,对玩家进行缓存,并将统计信息放入缓存中。

  2. 玩家打破石头,它会增加玩家缓存中的统计数据。

  3. 如果玩家打碎了石头,它会切换一个布尔标志以显示他已经打碎了石头,因此需要在某个时候将此信息刷新到数据库中。

  4. 服务器循环所有玩家,并检查玩家是否做了任何事情。如果玩家做了什么,它会调用 sql save 方法,并切换回布尔标志。

不过,我这次遇到的问题很少。

  1. 玩家可以在写入数据库期间打破石头,杀死怪物。甚至更多不同的动作。这将导致从播放器调用多个保存函数。有没有更好的方法来解决这个问题?

  2. 我读取和写入数据的一般方法是否正确?我几乎使用相同的方法来处理其他功能的数据库内容,但不确定这是否是好方法。

【问题讨论】:

  • 1) 您也应该在第一个代码中使用 try/resource/catch,就像在第二个代码中一样。 2)你这样做的方式,你有很多小写(在这种情况下不是问题,每秒查询仍然太少)。但是您可以将统计信息保存在 RAM 中,并使用“已更改”标志。然后运行一个计时器(带有 sleep 或 sth alik 的守护线程循环),每隔一秒左右检查一次,并将所有更改的数据集批量写入数据库(一次多个数据行,准备好的语句)调用。理论上,如果性能仍然存在问题,写入周期时间会自行增加。

标签: java minecraft


【解决方案1】:

在极少数情况下,这是做任何事情的唯一正确方法;但你确实提到了一个数据访问对象。 DAO 遵循一种模式,但这种模式是通用的。根据您的需要,实际对象可能包含更多(或更少)数据,或者被结构化为一个(或多个表)。

DAO 模式是一个对数据库执行所有直接操作的类。最少包括add(Object)remove(Object)get(id),可能还有getAll(),但也可以包括getOldest()remove(id)等。

它通常不应该做的是直接暴露底层表结构。所以从某种意义上说,您的方法(通过公开 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 的优势在于,如果您稍后决定更改基础表(或连接表集),您可以在一个位置将现有“数据对象”绑定到新表结构。

【讨论】:

    【解决方案2】:

    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 数据库。

    【讨论】:

      【解决方案3】:

      对我来说,你应该使用一个能保存所有信息的对象,并且只在你需要的时候保存它们。例如:StatsPlayer

      您有一个静态地图:HashMap&lt;UUID, StatsPlayer&gt;,其中包含所有玩家实例。

      每个实例都包含这样的所有信息:

      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();
         }
      }
      

      现在,你必须做一个这样的静态吸气剂:

      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();
         }
      }
      

      最后,有时你应该保存对象,例如仅当他们离开服务器或服务器停止时

      【讨论】:

        猜你喜欢
        • 2010-09-16
        • 1970-01-01
        • 1970-01-01
        • 2017-01-29
        • 2013-09-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-01-31
        相关资源
        最近更新 更多