server: database improves:

- fixed broken database in some use cases (example: AI and choose name dialog, related to #11285);
- added docs and debug tools for sql queries, caches and memory analyse (see DebugUtil);
- refactor code to use shared settings;
- deleted outdated and un-used code (db logs, stats, etc);
This commit is contained in:
Oleg Agafonov 2024-07-18 21:22:10 +04:00
parent c448612c97
commit bf3f26ccc1
18 changed files with 376 additions and 394 deletions

View file

@ -2,6 +2,7 @@ package mage.cards.repository;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.dao.GenericRawResults;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.stmt.SelectArg;
@ -19,6 +20,7 @@ import java.io.File;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* @author North, JayDi85
@ -31,16 +33,15 @@ public enum CardRepository {
// fixes limit for out of memory problems
private static final AtomicInteger databaseFixes = new AtomicInteger();
private static final int MAX_DATABASE_FIXES = 3;
private static final String JDBC_URL = "jdbc:h2:file:./db/cards.h2;AUTO_SERVER=TRUE;IGNORECASE=TRUE";
private static final int MAX_DATABASE_FIXES = 10;
// TODO: delete db version from cards and expansions due un-used (cause dbs re-created on each update now)
private static final String VERSION_ENTITY_NAME = "card";
// raise this if db structure was changed
private static final long CARD_DB_VERSION = 54;
// raise this if new cards were added to the server
private static final long CARD_CONTENT_VERSION = 241;
private Dao<CardInfo, Object> cardDao;
private Set<String> classNames;
private static final long CARD_DB_VERSION = 54; // raise this if db structure was changed
private static final long CARD_CONTENT_VERSION = 241; // raise this if new cards were added to the server
private Dao<CardInfo, Object> cardsDao;
// sets with exclusively snow basics
public static final Set<String> snowLandSetCodes = new HashSet<>(Arrays.asList(
@ -55,7 +56,7 @@ public enum CardRepository {
file.mkdirs();
}
try {
ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL);
ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, true));
boolean isObsolete = RepositoryUtil.isDatabaseObsolete(connectionSource, VERSION_ENTITY_NAME, CARD_DB_VERSION);
boolean isNewBuild = RepositoryUtil.isNewBuildRun(connectionSource, VERSION_ENTITY_NAME, CardRepository.class); // recreate db on new build
@ -65,7 +66,7 @@ public enum CardRepository {
}
TableUtils.createTableIfNotExists(connectionSource, CardInfo.class);
cardDao = DaoManager.createDao(connectionSource, CardInfo.class);
cardsDao = DaoManager.createDao(connectionSource, CardInfo.class);
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error creating card repository - " + e, e);
processMemoryErrors(e);
@ -95,26 +96,22 @@ public enum CardRepository {
}
public void saveCards(final List<CardInfo> newCards, long newContentVersion) {
if (newCards == null || newCards.isEmpty()) {
return;
}
try {
cardDao.callBatchTasks(() -> {
// add
if (newCards != null && !newCards.isEmpty()) {
logger.info("DB: need to add " + newCards.size() + " new cards");
try {
for (CardInfo card : newCards) {
cardDao.create(card);
if (classNames != null) {
classNames.add(card.getClassName());
}
}
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error adding cards to DB - " + e, e);
processMemoryErrors(e);
cardsDao.callBatchTasks(() -> {
// only add new cards (no updates)
logger.info("DB: need to add " + newCards.size() + " new cards");
try {
for (CardInfo card : newCards) {
cardsDao.create(card);
}
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error adding cards to DB - " + e, e);
processMemoryErrors(e);
}
// no card updates
return null;
});
@ -158,9 +155,9 @@ public enum CardRepository {
public Set<String> getNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -174,10 +171,10 @@ public enum CardRepository {
public Set<String> getNonLandNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%'));
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -191,14 +188,14 @@ public enum CardRepository {
public Set<String> getNonbasicLandNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'),
where.like("types", '%' + CardType.LAND.name() + '%')
);
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -212,10 +209,10 @@ public enum CardRepository {
public Set<String> getNotBasicLandNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%'));
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -229,10 +226,10 @@ public enum CardRepository {
public Set<String> getCreatureNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%'));
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -246,10 +243,10 @@ public enum CardRepository {
public Set<String> getArtifactNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%'));
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -263,14 +260,14 @@ public enum CardRepository {
public Set<String> getNonLandAndNonCreatureNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("types", '%' + CardType.CREATURE.name() + '%'),
where.not().like("types", '%' + CardType.LAND.name() + '%')
);
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -284,14 +281,14 @@ public enum CardRepository {
public Set<String> getNonArtifactAndNonLandNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'),
where.not().like("types", '%' + CardType.LAND.name() + '%')
);
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -308,7 +305,7 @@ public enum CardRepository {
public CardInfo findCard(String setCode, String cardNumber, boolean ignoreNightCards) {
try {
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> queryBuilder = cardsDao.queryBuilder();
if (ignoreNightCards) {
queryBuilder.limit(1L).where()
.eq("setCode", new SelectArg(setCode))
@ -323,7 +320,7 @@ public enum CardRepository {
// (example: vow - 65 - Jacob Hauken, Inspector), so make priority for main side first
queryBuilder.orderBy("nightCard", true);
}
List<CardInfo> result = cardDao.query(queryBuilder.prepare());
List<CardInfo> result = cardsDao.query(queryBuilder.prepare());
if (!result.isEmpty()) {
return result.get(0);
}
@ -337,7 +334,7 @@ public enum CardRepository {
public List<String> getClassNames() {
List<String> names = new ArrayList<>();
try {
List<CardInfo> results = cardDao.queryForAll();
List<CardInfo> results = cardsDao.queryForAll();
for (CardInfo card : results) {
names.add(card.getClassName());
}
@ -350,10 +347,10 @@ public enum CardRepository {
public List<CardInfo> getMissingCards(List<String> classNames) {
try {
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> queryBuilder = cardsDao.queryBuilder();
queryBuilder.where().not().in("className", classNames);
return cardDao.query(queryBuilder.prepare());
return cardsDao.query(queryBuilder.prepare());
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error getting missing cards from DB: " + e, e);
processMemoryErrors(e);
@ -424,12 +421,12 @@ public enum CardRepository {
* Used for building cubes, packs, and for ensuring that dual faces and split cards have sides/halves from
* the same set and variant art.
*
* @param name name of the card, or side of the card, to find
* @param expansion the set name from which to find the card
* @param cardNumber the card number for variant arts in one set
* @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card.
* Want this `false` when user is searching by either names in a split card so that
* the full card can be found by either name.
* @param name name of the card, or side of the card, to find
* @param expansion the set name from which to find the card
* @param cardNumber the card number for variant arts in one set
* @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card.
* Want this `false` when user is searching by either names in a split card so that
* the full card can be found by either name.
* @return
*/
public CardInfo findCardWithPreferredSetAndNumber(String name, String expansion, String cardNumber, boolean returnSplitCardHalf) {
@ -461,27 +458,27 @@ public enum CardRepository {
* Find a card's reprints from all sets.
* It allows for cards to be searched by their full name, or in the case of multi-name cards of the type "A // B"
* To search for them using "A", "B", or "A // B".
*
* <p>
* Note of how the function works:
* Out of all card types (Split, MDFC, Adventure, Flip, Transform)
* ONLY Split cards (Fire // Ice) MUST be queried in the DB by the full name when querying by "name".
* Searching for it by either half will return an incorrect result.
* ALL the others MUST be queried for by the first half of their full name (i.e. "A" from "A // B")
* when querying by "name".
* Out of all card types (Split, MDFC, Adventure, Flip, Transform)
* ONLY Split cards (Fire // Ice) MUST be queried in the DB by the full name when querying by "name".
* Searching for it by either half will return an incorrect result.
* ALL the others MUST be queried for by the first half of their full name (i.e. "A" from "A // B")
* when querying by "name".
*
* @param name the name of the card to search for
* @param limitByMaxAmount return max amount of different cards (if 0 then return card from all sets)
* @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card.
* Want this `false` when user is searching by either names in a split card so that
* the full card can be found by either name.
* Want this `true` when the client is searching for info on both halves to display it.
* @canCheckDatabaseHealth try to fix database on any errors (use true anytime except fix methods itself)
* @return a list of the reprints of the card if it was found (up to limitByMaxAmount number),
* or an empty list if the card was not found.
* @param name the name of the card to search for
* @param limitByMaxAmount return max amount of different cards (if 0 then return card from all sets)
* @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card.
* Want this `false` when user is searching by either names in a split card so that
* the full card can be found by either name.
* Want this `true` when the client is searching for info on both halves to display it.
* @return a list of the reprints of the card if it was found (up to limitByMaxAmount number),
* or an empty list if the card was not found.
* @canCheckDatabaseHealth try to fix database on any errors (use true anytime except fix methods itself)
*/
public List<CardInfo> findCards(String name, long limitByMaxAmount, boolean returnSplitCardHalf, boolean canCheckDatabaseHealth) {
List<CardInfo> results;
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> queryBuilder = cardsDao.queryBuilder();
if (limitByMaxAmount > 0) {
queryBuilder.limit(limitByMaxAmount);
}
@ -492,27 +489,27 @@ public enum CardRepository {
// Could be made faster by searching assuming it's NOT a split card and first searching by the first
// half of the name, but this is easier to understand.
queryBuilder.where().eq("name", new SelectArg(name));
results = cardDao.query(queryBuilder.prepare());
results = cardsDao.query(queryBuilder.prepare());
// Result comes back empty, try to search using the first half (could be Adventure, MDFC, etc.)
if (results.isEmpty()) {
String mainCardName = name.split(" // ", 2)[0];
queryBuilder.where().eq("name", new SelectArg(mainCardName));
results = cardDao.query(queryBuilder.prepare()); // If still empty, then card can't be found
results = cardsDao.query(queryBuilder.prepare()); // If still empty, then card can't be found
}
} else { // Cannot tell if string represents the full name of a card or only part of it.
// Assume it is the full card name
queryBuilder.where().eq("name", new SelectArg(name));
results = cardDao.query(queryBuilder.prepare());
results = cardsDao.query(queryBuilder.prepare());
if (results.isEmpty()) {
// Nothing found when looking for main name, try looking under the other names
queryBuilder.where()
.eq("flipCardName", new SelectArg(name)).or()
.eq("secondSideName", new SelectArg(name)).or()
.eq("adventureSpellName", new SelectArg(name)).or()
.eq("modalDoubleFacedSecondSideName", new SelectArg(name));
results = cardDao.query(queryBuilder.prepare());
.eq("flipCardName", new SelectArg(name)).or()
.eq("secondSideName", new SelectArg(name)).or()
.eq("adventureSpellName", new SelectArg(name)).or()
.eq("modalDoubleFacedSecondSideName", new SelectArg(name));
results = cardsDao.query(queryBuilder.prepare());
} else {
// Check that a full card was found and not a SplitCardHalf
// Can be caused by searching for "Fire" instead of "Fire // Ice"
@ -522,7 +519,7 @@ public enum CardRepository {
queryBuilder.where()
.eq("setCode", new SelectArg(firstCardInfo.setCode)).and()
.eq("cardNumber", new SelectArg(firstCardInfo.cardNumber));
List<CardInfo> tmpResults = cardDao.query(queryBuilder.prepare());
List<CardInfo> tmpResults = cardsDao.query(queryBuilder.prepare());
String fullSplitCardName = null;
for (CardInfo cardInfo : tmpResults) {
@ -536,7 +533,7 @@ public enum CardRepository {
}
queryBuilder.where().eq("name", new SelectArg(fullSplitCardName));
results = cardDao.query(queryBuilder.prepare());
results = cardsDao.query(queryBuilder.prepare());
}
}
}
@ -557,9 +554,9 @@ public enum CardRepository {
public List<CardInfo> findCardsByClass(String canonicalClassName) {
try {
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> queryBuilder = cardsDao.queryBuilder();
queryBuilder.where().eq("className", new SelectArg(canonicalClassName));
return cardDao.query(queryBuilder.prepare());
return cardsDao.query(queryBuilder.prepare());
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error during execution of raw sql statement" + e, e);
processMemoryErrors(e);
@ -570,7 +567,7 @@ public enum CardRepository {
/**
* Warning, don't use db functions in card's code - it generates heavy db loading in AI simulations. If you
* need that feature then check for simulation mode. See https://github.com/magefree/mage/issues/7014
*
* <p>
* Ignoring night cards by default
*
* @param criteria
@ -578,10 +575,10 @@ public enum CardRepository {
*/
public List<CardInfo> findCards(CardCriteria criteria) {
try {
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> queryBuilder = cardsDao.queryBuilder();
criteria.buildQuery(queryBuilder);
return cardDao.query(queryBuilder.prepare());
return cardsDao.query(queryBuilder.prepare());
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error during execution of card repository query statement: " + e, e);
processMemoryErrors(e);
@ -622,7 +619,7 @@ public enum CardRepository {
public long getContentVersionFromDB() {
try {
ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL);
ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, false));
return RepositoryUtil.getDatabaseVersion(connectionSource, VERSION_ENTITY_NAME + "Content");
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error getting content version from DB - " + e, e);
@ -633,7 +630,7 @@ public enum CardRepository {
public void setContentVersion(long version) {
try {
ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL);
ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, false));
RepositoryUtil.updateVersion(connectionSource, VERSION_ENTITY_NAME + "Content", version);
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error setting content version - " + e, e);
@ -645,25 +642,84 @@ public enum CardRepository {
return CARD_CONTENT_VERSION;
}
public void closeDB() {
public void closeDB(boolean writeCompact) {
try {
if (cardDao != null && cardDao.getConnectionSource() != null) {
DatabaseConnection conn = cardDao.getConnectionSource().getReadWriteConnection(cardDao.getTableName());
conn.executeStatement("shutdown compact", 0);
if (cardsDao != null && cardsDao.getConnectionSource() != null) {
DatabaseConnection conn = cardsDao.getConnectionSource().getReadWriteConnection(cardsDao.getTableName());
if (writeCompact) {
conn.executeStatement("SHUTDOWN COMPACT", 0); // compact data and rewrite whole db
} else {
conn.executeStatement("SHUTDOWN IMMEDIATELY", 0); // close without any writes
}
cardsDao.getConnectionSource().releaseConnection(conn);
}
} catch (SQLException ignore) {
}
}
private void openDB() {
public void openDB() {
try {
ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL);
cardDao = DaoManager.createDao(connectionSource, CardInfo.class);
ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, true));
cardsDao = DaoManager.createDao(connectionSource, CardInfo.class);
} catch (SQLException e) {
Logger.getLogger(CardRepository.class).error("Error opening card repository - " + e, e);
}
}
public void printDatabaseStats(String info) {
List<List<String>> allSettings = querySQL("SELECT NAME, VALUE FROM INFORMATION_SCHEMA.SETTINGS");
if (allSettings == null) {
return;
}
// cache
logger.info("Database cache settings (" + info + "):");
allSettings.stream().filter(values -> values.get(0).equals("CACHE_SIZE")).forEach(values -> {
logger.info(" - cache size, setup: " + values.get(1) + " kb");
});
allSettings.stream().filter(values -> values.get(0).equals("info.CACHE_MAX_SIZE")).forEach(values -> {
logger.info(" - cache size, max: " + values.get(1) + " mb");
});
allSettings.stream().filter(values -> values.get(0).equals("info.CACHE_SIZE")).forEach(values -> {
logger.info(" - cache size, current: " + values.get(1) + " mb");
});
// memory
allSettings = querySQL("SELECT MEMORY_FREE(), MEMORY_USED()");
if (allSettings == null) {
return;
}
logger.info("Database memory stats (" + info + "):");
logger.info(" - free: " + allSettings.get(0).get(0) + " kb");
logger.info(" - used: " + allSettings.get(0).get(1) + " kb");
}
/**
* Exec any SQL query and return result table as string values
*/
public List<List<String>> querySQL(String sql) {
try {
GenericRawResults<String[]> query = cardsDao.queryRaw(sql);
return query.getResults().stream()
.map(Arrays::asList)
.collect(Collectors.toList());
} catch (SQLException e) {
logger.error("Can't query sql due error: " + sql + " - " + e, e);
return null;
}
}
/**
* Exec any SQL code without result. Can be used to change db settings like SET xxx = YYY
*/
public void execSQL(String sql) {
try {
cardsDao.executeRaw(sql);
} catch (SQLException e) {
logger.error("Can't exec sql due error: " + sql + " - " + e, e);
}
}
private static CardInfo safeFindKnownCard() {
// safe find of known card with memory/db fixes
return instance.findCards("Silvercoat Lion", 1, false, false)
@ -690,7 +746,7 @@ public enum CardRepository {
}
// DB seems to have a problem - try to restart the DB (useless in 99% due out of memory problems)
instance.closeDB();
instance.closeDB(false);
instance.openDB();
cardInfo = safeFindKnownCard();
if (cardInfo != null) {

View file

@ -0,0 +1,69 @@
package mage.cards.repository;
import mage.util.DebugUtil;
/**
* Helper class for database
*
* @author JayDi85
*/
public class DatabaseUtils {
// warning, do not change names or db format
// h2
public static final String DB_NAME_FEEDBACK = "feedback.h2";
public static final String DB_NAME_USERS = "authorized_user.h2";
public static final String DB_NAME_CARDS = "cards.h2";
// sqlite (usage reason: h2 database works bad with 1GB+ files and can break it)
public static final String DB_NAME_RECORDS = "table_record.db";
public static final String DB_NAME_STATS = "user_stats.db";
/**
* Prepare JDBC connection string and setup additional params for H2 databases
*
* @param dbName database name like "cards.h2"
* @param improveCaches use memory optimizations for cards database (no needs for other dbs)
*/
public static String prepareH2Connection(String dbName, boolean improveCaches) {
// example: jdbc:h2:file:./db/cards.h2;AUTO_SERVER=TRUE;IGNORECASE=TRUE
String res = String.format("jdbc:h2:file:./db/%s", dbName);
// shared params
res += ";AUTO_SERVER=TRUE"; // open database in mix mode (first open by new thread, second open by new jvm-process)
res += ";IGNORECASE=TRUE"; // ignore char case for text searching
// additional params
// can be defined by connection string, by exec sql like "SET xxx = yyy", by settings from existing db-file
if (improveCaches) {
// CACHE_SIZE
// max query cache size in kb (default: 65 Mb per 1 GB of java's max memory)
// warning, xmage require 150Mb cache for big queries in AI games like all card names (db can be broken on lower cache)
//res += ";CACHE_SIZE=150000";
res += ";CACHE_SIZE=" + Math.round(Math.max(150000, Runtime.getRuntime().maxMemory() * 0.1 / 1024));
// QUERY_CACHE_SIZE
// queries amount per session to cache (default: 8)
res += ";QUERY_CACHE_SIZE=32";
}
// add debug stats (see DebugUtil for usage instruction)
if (DebugUtil.DATABASE_PROFILE_SQL_QUERIES_TO_FILE) {
res += ";TRACE_LEVEL_FILE=2";
res += ";QUERY_STATISTICS=TRUE";
}
return res;
}
/**
* Prepare JDBC connection string and setup additional params for SQLite databases
*
* @param dbName database name like "cards"
*/
public static String prepareSqliteConnection(String dbName) {
// example: jdbc:sqlite:./db/table_record.db
return String.format("jdbc:sqlite:./db/%s", dbName);
}
}

View file

@ -27,13 +27,13 @@ public enum ExpansionRepository {
private static final Logger logger = Logger.getLogger(ExpansionRepository.class);
private static final String JDBC_URL = "jdbc:h2:file:./db/cards.h2;AUTO_SERVER=TRUE;IGNORECASE=TRUE";
// TODO: delete db version from cards and expansions due un-used (that's dbs re-created on each update)
private static final String VERSION_ENTITY_NAME = "expansion";
private static final long EXPANSION_DB_VERSION = 5;
private static final long EXPANSION_CONTENT_VERSION = 18;
private Dao<ExpansionInfo, Object> expansionDao;
private RepositoryEventSource eventSource = new RepositoryEventSource();
private final RepositoryEventSource eventSource = new RepositoryEventSource();
public boolean instanceInitialized = false;
ExpansionRepository() {
@ -42,7 +42,7 @@ public enum ExpansionRepository {
file.mkdirs();
}
try {
ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL);
ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, true));
boolean isObsolete = RepositoryUtil.isDatabaseObsolete(connectionSource, VERSION_ENTITY_NAME, EXPANSION_DB_VERSION);
boolean isNewBuild = RepositoryUtil.isNewBuildRun(connectionSource, VERSION_ENTITY_NAME, ExpansionRepository.class); // recreate db on new build
@ -218,7 +218,7 @@ public enum ExpansionRepository {
public long getContentVersionFromDB() {
try {
ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL);
ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, false));
return RepositoryUtil.getDatabaseVersion(connectionSource, VERSION_ENTITY_NAME + "Content");
} catch (SQLException ex) {
ex.printStackTrace();
@ -228,7 +228,7 @@ public enum ExpansionRepository {
public void setContentVersion(long version) {
try {
ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL);
ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, false));
RepositoryUtil.updateVersion(connectionSource, VERSION_ENTITY_NAME + "Content", version);
} catch (SQLException e) {
logger.error("Error setting content version - " + e, e);

View file

@ -10,6 +10,7 @@ import java.util.EventObject;
*/
public class RepositoryEvent extends EventObject implements ExternalEvent, Serializable {
// TODO: db changed on update only, events can be deleted
public enum RepositoryEventType {
DB_LOADED, DB_UPDATED
}

View file

@ -7,6 +7,7 @@ import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.stmt.SelectArg;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import mage.util.DebugUtil;
import mage.util.JarVersion;
import org.apache.log4j.Logger;
@ -39,22 +40,26 @@ public final class RepositoryUtil {
logger.info(" - emblems: " + TokenRepository.instance.getByType(TokenType.EMBLEM).size());
logger.info(" - planes: " + TokenRepository.instance.getByType(TokenType.PLANE).size());
logger.info(" - dungeons: " + TokenRepository.instance.getByType(TokenType.DUNGEON).size());
if (DebugUtil.DATABASE_SHOW_CACHE_AND_MEMORY_STATS_ON_STARTUP) {
CardRepository.instance.printDatabaseStats("on startup");
}
}
public static boolean isDatabaseObsolete(ConnectionSource connectionSource, String entityName, long version) throws SQLException {
TableUtils.createTableIfNotExists(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
QueryBuilder<DatabaseVersion, Object> queryBuilder = dbVersionDao.queryBuilder();
QueryBuilder<DatabaseVersion, Object> queryBuilder = versionDao.queryBuilder();
queryBuilder.where().eq("entity", new SelectArg(entityName))
.and().eq("version", new SelectArg(version));
List<DatabaseVersion> dbVersions = dbVersionDao.query(queryBuilder.prepare());
List<DatabaseVersion> dbVersions = versionDao.query(queryBuilder.prepare());
if (dbVersions.isEmpty()) {
DatabaseVersion dbVersion = new DatabaseVersion();
dbVersion.setEntity(entityName);
dbVersion.setVersion(version);
dbVersionDao.create(dbVersion);
versionDao.create(dbVersion);
}
return dbVersions.isEmpty();
}
@ -68,48 +73,48 @@ public final class RepositoryUtil {
}
TableUtils.createTableIfNotExists(connectionSource, DatabaseBuild.class);
Dao<DatabaseBuild, Object> dbBuildDao = DaoManager.createDao(connectionSource, DatabaseBuild.class);
Dao<DatabaseBuild, Object> buildDao = DaoManager.createDao(connectionSource, DatabaseBuild.class);
QueryBuilder<DatabaseBuild, Object> queryBuilder = dbBuildDao.queryBuilder();
QueryBuilder<DatabaseBuild, Object> queryBuilder = buildDao.queryBuilder();
queryBuilder.where().eq("entity", new SelectArg(entityName))
.and().eq("last_build", new SelectArg(currentBuild));
List<DatabaseBuild> dbBuilds = dbBuildDao.query(queryBuilder.prepare());
List<DatabaseBuild> dbBuilds = buildDao.query(queryBuilder.prepare());
if (dbBuilds.isEmpty()) {
DatabaseBuild dbBuild = new DatabaseBuild();
dbBuild.setEntity(entityName);
dbBuild.setLastBuild(currentBuild);
dbBuildDao.create(dbBuild);
buildDao.create(dbBuild);
}
return dbBuilds.isEmpty();
}
public static void updateVersion(ConnectionSource connectionSource, String entityName, long version) throws SQLException {
TableUtils.createTableIfNotExists(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
QueryBuilder<DatabaseVersion, Object> queryBuilder = dbVersionDao.queryBuilder();
QueryBuilder<DatabaseVersion, Object> queryBuilder = versionDao.queryBuilder();
queryBuilder.where().eq("entity", new SelectArg(entityName));
List<DatabaseVersion> dbVersions = dbVersionDao.query(queryBuilder.prepare());
List<DatabaseVersion> dbVersions = versionDao.query(queryBuilder.prepare());
if (!dbVersions.isEmpty()) {
DeleteBuilder<DatabaseVersion, Object> deleteBuilder = dbVersionDao.deleteBuilder();
DeleteBuilder<DatabaseVersion, Object> deleteBuilder = versionDao.deleteBuilder();
deleteBuilder.where().eq("entity", new SelectArg(entityName));
deleteBuilder.delete();
}
DatabaseVersion databaseVersion = new DatabaseVersion();
databaseVersion.setEntity(entityName);
databaseVersion.setVersion(version);
dbVersionDao.create(databaseVersion);
versionDao.create(databaseVersion);
}
public static long getDatabaseVersion(ConnectionSource connectionSource, String entityName) throws SQLException {
TableUtils.createTableIfNotExists(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
QueryBuilder<DatabaseVersion, Object> queryBuilder = dbVersionDao.queryBuilder();
QueryBuilder<DatabaseVersion, Object> queryBuilder = versionDao.queryBuilder();
queryBuilder.where().eq("entity", new SelectArg(entityName));
List<DatabaseVersion> dbVersions = dbVersionDao.query(queryBuilder.prepare());
List<DatabaseVersion> dbVersions = versionDao.query(queryBuilder.prepare());
if (dbVersions.isEmpty()) {
return 0;
} else {

View file

@ -39,6 +39,18 @@ public class DebugUtil {
// game dialogs
public static boolean GUI_GAME_DIALOGS_DRAW_CARDS_AREA_BORDER = false;
// database - show additional info about cache and memory settings
public static boolean DATABASE_SHOW_CACHE_AND_MEMORY_STATS_ON_STARTUP = true;
// database - collect sql queries and stats
// how-to use:
// - clean db folders or delete all *.trace.db files
// - run tests or real server to collect some stats
// - download h2 files for ver 1.4.197 from https://h2database.com/h2-2018-03-18.zip and open tools folder like xxx\H2\bin
// - execute command: java -cp "h2-1.4.196.jar;%H2DRIVERS%;%CLASSPATH%" org.h2.tools.ConvertTraceFile -traceFile "xxx\Mage.Tests\db\cards.h2.trace.db" -script "xxx\Mage.Tests\db\cards.h2.trace.sql"
// - open *.sql file for all sql-queries and exec stats
public static boolean DATABASE_PROFILE_SQL_QUERIES_TO_FILE = false;
public static String getMethodNameWithSource(final int depth) {
return TraceHelper.getMethodNameWithSource(depth);
}