Home > Software engineering >  Is this a correct way to struct Java DAO?
Is this a correct way to struct Java DAO?

Time:12-06

I'm trying to develop an statistics / achievement system in my minecraft server.

I've done some research, but still couldn't make decision well, so decided to post my very first question in stack overflow.

There are multiple types of achievements, such as block broken, crop harvested, animal killed.. and so on. Table initializer looks like this. ( I intentionally set those values as 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();
        }
    }

and I save it back with this

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

}

I have question on general design of programming , not fixing code itself.

For now, this is how things go. For simplicity, let me pick two actions of statistics - break stone , and kill monster.

  1. player joins game, and read data , make a cache of player and put statistics information into the cache.

  2. player breaks stone, and it increments statistics in player cache.

  3. If player broke stone, it toggles a boolean flag to show he has broken stone, thus this information need to be flushed to database at some point.

  4. Server loops all players , and check if player has done anything. If player has done something, it calls sql save method, and toggle boolean flag back.

However, there are few problems I encountered this time.

  1. player can break stone, and kill monster in duration of writing to the database. or even more different actions. That will result multiple save functions to be called from a player. Is there better way to deal with this?

  2. Is my general apparoach to read and write data correct? I'm pretty much using same way to do database stuff to other functionalities, but unsure if this is the good way.

CodePudding user response:

There are very few situations where this is a single correct way to do anything; but you did mention a Data Access Object. DAO's follow a pattern, but that pattern is a general one. Depending on your needs the actual objects might contain more (or less) data, or be structured in to back to one (or many tables).

the DAO pattern is a class that performs all direct operations on the database. Minimally it includes add(Object), remove(Object), get(id), and maybe getAll() but it can also include getOldest(), remove(id), and so on.

What it should generally NOT do is expose the underlying table structure directly. So in a sense, your approach (by exposing UUID and the stat to be updated independently) is not following the pattern.

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

The advantage of a DAO is that if you later decide to change the underlying table (or set of joined tables), you have one location to bind the existing "Data Object" to the new table structures.

CodePudding user response:

That will result multiple save functions to be called from a player. Is there better way to deal with this?

I think you are exacerbating the severity of running multiple SQL insert statements for a player. On a Minecraft server, there won't be very much load on the database at all and the fact you are using Hikari ensures that the performance impact of having these extra few queries is negligible.

However, if you are very sure that the environment you are working in is incredibly performance sensitive (which for a Minecraft plugin it probably isn't) then consider running batch SQL statements or combining updates for the same player manually into a single statement and sending that to the SQL database.

CodePudding user response:

For me, you should use an object that will keep all informations, and save them only when you wants. For example: StatsPlayer.

You have a static map : HashMap<UUID, StatsPlayer> that contains all instance of players.

Each instance contains all informations like that :

private HashMap<StatisticsType, Double> stats;

Or:

private double blockBreak;

When creating new instance, don't forget to get informations from DB, for example :

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

Now, you have to do a static getter like that :

public static StatsPlayer getPlayer(Player p) {
    synchronized(PLAYERS) {
        return PLAYERS.computeIfAbsent(p, StatsPlayer::new);
    }
}

In your StatsPlayer object, you should add method to update values, and one to save everything :

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

Finally, you should save the object sometimes, for example only when they left server or when server stop

  • Related