Это правильный способ структурирования Java DAO?

#java #minecraft

#java #Minecraft

Вопрос:

Я пытаюсь разработать систему статистики / достижений на своем сервере minecraft.

Я провел некоторое исследование, но все еще не мог принять правильное решение, поэтому решил опубликовать свой самый первый вопрос в stack overflow .

Существует несколько типов достижений, таких как разбитый блок, собранный урожай, убитое животное … и так далее. Инициализатор таблицы выглядит следующим образом. ( Я намеренно установил эти значения как двойные )

     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 и переключает логический флаг обратно.

Однако на этот раз я столкнулся с несколькими проблемами.

  1. игрок может разбить камень и убить монстра во время записи в базу данных. или даже более разные действия. Это приведет к вызову нескольких функций сохранения из проигрывателя. Есть ли лучший способ справиться с этим?
  2. Правильно ли мое общее устройство для чтения и записи данных? Я в значительной степени использую тот же способ для обработки базы данных с другими функциями, но не уверен, что это хороший способ.

Комментарии:

1. 1) Вы также должны использовать try/resource/ catch в первом коде, как и во втором коде. 2) То, как вы это делаете, у вас много небольших записей (в данном случае это не проблема, все еще слишком мало запросов в секунду). НО вы могли бы сохранить статистику в оперативной памяти и флаг «изменено». Затем запустите таймер (цикл потока демона со сном или sth alik), который проверяет их каждую секунду или около того, и записывает все измененные наборы данных в БД в пакетном режиме (несколько строк данных одновременно, подготовленная инструкция). вызов. Теоретически, если бы все еще были проблемы с производительностью, время цикла записи просто увеличилось бы само по себе.

Ответ №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 insert для игрока. На сервере Minecraft нагрузка на базу данных вообще не будет большой, а тот факт, что вы используете Hikari, гарантирует, что влияние на производительность этих дополнительных нескольких запросов незначительно.

Однако, если вы уверены, что среда, в которой вы работаете, невероятно чувствительна к производительности (что для плагина Minecraft, вероятно, не так), тогда рассмотрите возможность запуска пакетных инструкций SQL или объединения обновлений для одного и того же проигрывателя вручную в одну инструкцию и отправки ее в базу данных SQL.

Ответ №3:

Для меня вы должны использовать объект, который будет хранить всю информацию и сохранять их только тогда, когда вы захотите. Например: 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();
   }
}
 

Теперь вам нужно сделать статический геттер, подобный этому :

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

Наконец, вы должны иногда сохранять объект, например, только тогда, когда они покинули сервер или когда сервер остановился