From 660414424ea2c7b91fac59c88b0950b3d160fb01 Mon Sep 17 00:00:00 2001 From: Marc Baloup Date: Mon, 1 Aug 2022 22:21:04 +0200 Subject: [PATCH] Javadoc and refactor pandalib-db --- pandalib-db/pom.xml | 70 +- .../src/main/java/fr/pandacube/lib/db/DB.java | 877 +++++++----- .../fr/pandacube/lib/db/DBConnection.java | 113 +- .../java/fr/pandacube/lib/db/DBException.java | 21 +- .../lib/db/DBInitTableException.java | 15 +- .../lib/db/ParameterizedSQLString.java | 2 +- .../fr/pandacube/lib/db/SQLCustomType.java | 54 +- .../java/fr/pandacube/lib/db/SQLElement.java | 1248 ++++++++++------- .../fr/pandacube/lib/db/SQLElementList.java | 333 ++--- .../java/fr/pandacube/lib/db/SQLFKField.java | 102 +- .../java/fr/pandacube/lib/db/SQLField.java | 316 +++-- .../java/fr/pandacube/lib/db/SQLOrderBy.java | 146 +- .../java/fr/pandacube/lib/db/SQLType.java | 72 +- .../java/fr/pandacube/lib/db/SQLUpdate.java | 68 - .../fr/pandacube/lib/db/SQLUpdateBuilder.java | 92 ++ .../java/fr/pandacube/lib/db/SQLWhere.java | 563 ++++---- 16 files changed, 2457 insertions(+), 1635 deletions(-) delete mode 100644 pandalib-db/src/main/java/fr/pandacube/lib/db/SQLUpdate.java create mode 100644 pandalib-db/src/main/java/fr/pandacube/lib/db/SQLUpdateBuilder.java diff --git a/pandalib-db/pom.xml b/pandalib-db/pom.xml index ef86f6e..df38184 100644 --- a/pandalib-db/pom.xml +++ b/pandalib-db/pom.xml @@ -19,7 +19,75 @@ pandalib-util ${project.version} - + + org.apache.commons + commons-dbcp2 + 2.9.0 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.3.0 + + + package + + shade + + + + + org.apache.commons:commons-dbcp2 + org.apache.commons:commons-pool2 + commons-logging:commons-logging + + + + + org.apache.commons:commons-dbcp2 + + META-INF/MANIFEST.MF + + + + org.apache.commons:commons-pool2 + + META-INF/MANIFEST.MF + + + + commons-logging:commons-logging + + META-INF/MANIFEST.MF + + + + + + org.apache.commons + fr.pandacube.lib.db.shaded.commons + + + org.apache.commons + fr.pandacube.lib.db.shaded.commons + + + + + + + false + + + + + + + + + \ No newline at end of file diff --git a/pandalib-db/src/main/java/fr/pandacube/lib/db/DB.java b/pandalib-db/src/main/java/fr/pandacube/lib/db/DB.java index 1d3b13c..6293ba0 100644 --- a/pandalib-db/src/main/java/fr/pandacube/lib/db/DB.java +++ b/pandalib-db/src/main/java/fr/pandacube/lib/db/DB.java @@ -1,5 +1,6 @@ package fr.pandacube.lib.db; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -17,7 +18,7 @@ import fr.pandacube.lib.util.Log; /** * Static class to handle most of the database operations. - * + *

* To use this database library, first call {@link #init(DBConnection, String)} with an appropriate {@link DBConnection}, * then you can initialize every table you need for your application, using {@link #initTable(Class)}. * @@ -25,370 +26,606 @@ import fr.pandacube.lib.util.Log; */ public final class DB { - private static final List>> tables = new ArrayList<>(); - private static final Map>, String> tableNames = new HashMap<>(); + private static final List>> tables = new ArrayList<>(); + private static final Map>, String> tableNames = new HashMap<>(); - private static DBConnection connection; - /* package */ static String tablePrefix = ""; + private static DBConnection connection; + /* package */ static String tablePrefix = ""; - public static DBConnection getConnection() { - return connection; - } + /** + * Gets the {@link DBConnection}. + * @return the {@link DBConnection}. + */ + public static DBConnection getConnection() { + return connection; + } - public synchronized static void init(DBConnection conn, String tablePrefix) { - connection = conn; - DB.tablePrefix = Objects.requireNonNull(tablePrefix); - } + /** + * Initialize with the provided connection. + * @param conn the database connection. + * @param tablePrefix determine a prefix for the table that will be initialized. + */ + public synchronized static void init(DBConnection conn, String tablePrefix) { + connection = conn; + DB.tablePrefix = Objects.requireNonNull(tablePrefix); + } - public static synchronized > void initTable(Class elemClass) throws DBInitTableException { - if (connection == null) { - throw new DBInitTableException(elemClass, "Database connection is not yet initialized."); - } - if (tables.contains(elemClass)) return; - try { - tables.add(elemClass); - Log.debug("[DB] Start Init SQL table "+elemClass.getSimpleName()); - E instance = elemClass.getConstructor().newInstance(); - String tableName = tablePrefix + instance.tableName(); - tableNames.put(elemClass, tableName); - if (!tableExistInDB(tableName)) createTable(instance); - Log.debug("[DB] End init SQL table "+elemClass.getSimpleName()); - } catch (Exception|ExceptionInInitializerError e) { - throw new DBInitTableException(elemClass, e); - } - } + /** + * Initialialize the table represented by the provided class. + * @param elemClass the class representing a table. + * @param the type representing the table. + * @throws DBInitTableException if the table failed to initialized. + */ + public static synchronized > void initTable(Class elemClass) throws DBInitTableException { + if (connection == null) { + throw new DBInitTableException(elemClass, "Database connection is not yet initialized."); + } + if (tables.contains(elemClass)) return; + try { + tables.add(elemClass); + Log.debug("[DB] Start Init SQL table "+elemClass.getSimpleName()); + E instance = elemClass.getConstructor().newInstance(); + String tableName = tablePrefix + instance.tableName(); + tableNames.put(elemClass, tableName); + if (!tableExistInDB(tableName)) createTable(instance); + Log.debug("[DB] End init SQL table "+elemClass.getSimpleName()); + } catch (Exception|ExceptionInInitializerError e) { + throw new DBInitTableException(elemClass, e); + } + } - private static > void createTable(E elem) throws SQLException { - - String tableName = tablePrefix + elem.tableName(); + private static > void createTable(E elem) throws SQLException { - StringBuilder sql = new StringBuilder("CREATE TABLE IF NOT EXISTS " + tableName + " ("); - List params = new ArrayList<>(); + String tableName = tablePrefix + elem.tableName(); - Collection> tableFields = elem.getFields().values(); - boolean first = true; - for (SQLField f : tableFields) { - ParameterizedSQLString statementPart = f.forSQLPreparedStatement(); - params.addAll(statementPart.parameters()); + StringBuilder sql = new StringBuilder("CREATE TABLE IF NOT EXISTS " + tableName + " ("); + List params = new ArrayList<>(); - if (!first) - sql.append(", "); - first = false; - sql.append(statementPart.sqlString()); - } + Collection> tableFields = elem.getFields().values(); + boolean first = true; + for (SQLField f : tableFields) { + ParameterizedSQLString statementPart = f.forSQLPreparedStatement(); + params.addAll(statementPart.parameters()); - sql.append(", PRIMARY KEY id(id))"); - - try (PreparedStatement ps = connection.getNativeConnection().prepareStatement(sql.toString())) { - int i = 1; - for (Object val : params) - ps.setObject(i++, val); - Log.info("Creating table " + elem.tableName() + ":\n" + ps.toString()); - ps.executeUpdate(); - } - } - - public static > String getTableName(Class elemClass) throws DBException { - initTable(elemClass); - return tableNames.get(elemClass); - } + if (!first) + sql.append(", "); + first = false; + sql.append(statementPart.sqlString()); + } - private static boolean tableExistInDB(String tableName) throws SQLException { - try (ResultSet set = connection.getNativeConnection().getMetaData().getTables(null, null, tableName, null)) { - return set.next(); - } - } + sql.append(", PRIMARY KEY id(id))"); - @SuppressWarnings("unchecked") - public static > SQLField getSQLIdField(Class elemClass) - throws DBInitTableException { - initTable(elemClass); - return (SQLField) SQLElement.fieldsCache.get(elemClass).get("id"); - } + try (Connection c = connection.getConnection(); + PreparedStatement ps = c.prepareStatement(sql.toString())) { + int i = 1; + for (Object val : params) + ps.setObject(i++, val); + Log.info("Creating table " + elem.tableName() + ":\n" + ps.toString()); + ps.executeUpdate(); + } + } - public static > SQLElementList getByIds(Class elemClass, Integer... ids) throws DBException { - return getByIds(elemClass, Arrays.asList(ids)); - } + /** + * Gets the name of the table in the database. + * @param elemClass the class representing a table. + * @return a table name. + * @param the type representing the table. + * @throws DBInitTableException if the provided table had to be initialized and it failed to do so. + */ + public static > String getTableName(Class elemClass) throws DBInitTableException { + initTable(elemClass); + return tableNames.get(elemClass); + } - public static > SQLElementList getByIds(Class elemClass, Collection ids) - throws DBException { - return getAll(elemClass, getSQLIdField(elemClass).in(ids), SQLOrderBy.asc(getSQLIdField(elemClass)), 1, null); - } + private static boolean tableExistInDB(String tableName) throws SQLException { + try (Connection c = connection.getConnection(); + ResultSet set = c.getMetaData().getTables(null, null, tableName, null)) { + return set.next(); + } + } - public static > E getById(Class elemClass, int id) throws DBException { - return getFirst(elemClass, getSQLIdField(elemClass).eq(id)); - } + /** + * Gets the {@code id} field of the provided table. + * @param elemClass the class representing a table. + * @return the {@code id} field of the provided table. + * @param the type representing the table. + * @throws DBInitTableException if the provided table had to be initialized and it failed to do so. + */ + @SuppressWarnings("unchecked") + public static > SQLField getSQLIdField(Class elemClass) throws DBInitTableException { + initTable(elemClass); + return (SQLField) SQLElement.fieldsCache.get(elemClass).get("id"); + } - public static > E getFirst(Class elemClass, SQLWhere where) - throws DBException { - return getFirst(elemClass, where, null, null); - } + /** + * Fetch the entry from the provided table, that has the specified ids. + * @param elemClass the class representing a table. + * @param ids the ids of the element entries. + * @return the entry from the provided table, that has the specified ids. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > SQLElementList getByIds(Class elemClass, Integer... ids) throws DBException { + return getByIds(elemClass, Arrays.asList(ids)); + } - public static > E getFirst(Class elemClass, SQLOrderBy orderBy) - throws DBException { - return getFirst(elemClass, null, orderBy, null); - } + /** + * Fetch the entry from the provided table, that has the specified ids. + * @param elemClass the class representing a table. + * @param ids the ids of the element entries. + * @return the entry from the provided table, that has the specified ids. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > SQLElementList getByIds(Class elemClass, Collection ids) throws DBException { + return getAll(elemClass, getSQLIdField(elemClass).in(ids), SQLOrderBy.asc(getSQLIdField(elemClass)), 1, null); + } - public static > E getFirst(Class elemClass, SQLWhere where, SQLOrderBy orderBy) - throws DBException { - return getFirst(elemClass, where, orderBy, null); - } + /** + * Fetch the entry from the provided table, that has the specified id. + * @param elemClass the class representing a table. + * @param id the id of the element entry. + * @return the entry from the provided table, that has the specified id, or null if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > E getById(Class elemClass, int id) throws DBException { + return getFirst(elemClass, getSQLIdField(elemClass).eq(id)); + } - public static > E getFirst(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer offset) - throws DBException { - SQLElementList elts = getAll(elemClass, where, orderBy, 1, offset); - return (elts.size() == 0) ? null : elts.get(0); - } + /** + * Fetch the entry from the provided table, using the provided {@code WHERE} clause, + * and a {@code LIMIT} of 1. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @return the entry from the provided table, or null if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > E getFirst(Class elemClass, SQLWhere where) throws DBException { + return getFirst(elemClass, where, null, null); + } - public static > SQLElementList getAll(Class elemClass) throws DBException { - return getAll(elemClass, null, null, null, null); - } + /** + * Fetch the entry from the provided table, using the provided {@code ORDER BY} clause, + * and a {@code LIMIT} of 1. + * @param elemClass the class representing a table. + * @param orderBy the {@code ORDER BY} clause of the query. + * @return the entry from the provided table, or null if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > E getFirst(Class elemClass, SQLOrderBy orderBy) throws DBException { + return getFirst(elemClass, null, orderBy, null); + } - public static > SQLElementList getAll(Class elemClass, SQLWhere where) throws DBException { - return getAll(elemClass, where, null, null, null); - } + /** + * Fetch the entry from the provided table, using the provided {@code WHERE} and {@code ORDER BY} clauses, + * and a {@code LIMIT} of 1. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param orderBy the {@code ORDER BY} clause of the query. + * @return the entry from the provided table, or null if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > E getFirst(Class elemClass, SQLWhere where, SQLOrderBy orderBy) throws DBException { + return getFirst(elemClass, where, orderBy, null); + } - public static > SQLElementList getAll(Class elemClass, SQLWhere where, - SQLOrderBy orderBy) throws DBException { - return getAll(elemClass, where, orderBy, null, null); - } + /** + * Fetch the entry from the provided table, using the provided {@code WHERE}, {@code ORDER BY} and {@code OFFSET} + * clauses, and a {@code LIMIT} of 1. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param orderBy the {@code ORDER BY} clause of the query. + * @param offset the {@code OFFSET} clause of the query. + * @return the entry from the provided table, or null if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > E getFirst(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer offset) throws DBException { + SQLElementList elts = getAll(elemClass, where, orderBy, 1, offset); + return (elts.size() == 0) ? null : elts.get(0); + } - public static > SQLElementList getAll(Class elemClass, SQLWhere where, - SQLOrderBy orderBy, Integer limit) throws DBException { - return getAll(elemClass, where, orderBy, limit, null); - } + /** + * Fetch all the entries from the provided table. + * @param elemClass the class representing a table. + * @return the entries from the provided table, or empty if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > SQLElementList getAll(Class elemClass) throws DBException { + return getAll(elemClass, null, null, null, null); + } - public static > SQLElementList getAll(Class elemClass, SQLWhere where, - SQLOrderBy orderBy, Integer limit, Integer offset) throws DBException { - SQLElementList elmts = new SQLElementList<>(); - forEach(elemClass, where, orderBy, limit, offset, elmts::add); - return elmts; - } - - public static > void forEach(Class elemClass, Consumer action) throws DBException { - forEach(elemClass, null, null, null, null, action); - } - - public static > void forEach(Class elemClass, SQLWhere where, - Consumer action) throws DBException { - forEach(elemClass, where, null, null, null, action); - } - - public static > void forEach(Class elemClass, SQLWhere where, - SQLOrderBy orderBy, Consumer action) throws DBException { - forEach(elemClass, where, orderBy, null, null, action); - } - - public static > void forEach(Class elemClass, SQLWhere where, - SQLOrderBy orderBy, Integer limit, Consumer action) throws DBException { - forEach(elemClass, where, orderBy, limit, null, action); - } - - public static > void forEach(Class elemClass, SQLWhere where, - SQLOrderBy orderBy, Integer limit, Integer offset, Consumer action) throws DBException { - initTable(elemClass); + /** + * Fetch the entries from the provided table, using the provided {@code WHERE} clause. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @return the entries from the provided table, or empty if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > SQLElementList getAll(Class elemClass, SQLWhere where) throws DBException { + return getAll(elemClass, where, null, null, null); + } - try { - String sql = "SELECT * FROM " + getTableName(elemClass); + /** + * Fetch the entries from the provided table, using the provided {@code WHERE} and {@code ORDER BY} clauses. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param orderBy the {@code ORDER BY} clause of the query. + * @return the entries from the provided table, or empty if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > SQLElementList getAll(Class elemClass, SQLWhere where, SQLOrderBy orderBy) throws DBException { + return getAll(elemClass, where, orderBy, null, null); + } - List params = new ArrayList<>(); + /** + * Fetch the entries from the provided table, using the provided {@code WHERE}, {@code ORDER BY} and {@code LIMIT} + * clauses. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param orderBy the {@code ORDER BY} clause of the query. + * @param limit the {@code LIMIT} clause of the query. + * @return the entries from the provided table, or empty if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > SQLElementList getAll(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer limit) throws DBException { + return getAll(elemClass, where, orderBy, limit, null); + } - if (where != null) { - ParameterizedSQLString ret = where.toSQL(); - sql += " WHERE " + ret.sqlString(); - params.addAll(ret.parameters()); - } - if (orderBy != null) sql += " ORDER BY " + orderBy.toSQL(); - if (limit != null) sql += " LIMIT " + limit; - if (offset != null) sql += " OFFSET " + offset; - sql += ";"; - - try (ResultSet set = customQueryStatement(sql, params)) { - while (set.next()) { - E elm = getElementInstance(set, elemClass); - action.accept(elm); - } - } - } catch (SQLException e) { - throw new DBException(e); - } + /** + * Fetch the entries from the provided table, using the provided {@code WHERE}, {@code ORDER BY}, {@code LIMIT} and + * {@code OFFSET} clauses. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param orderBy the {@code ORDER BY} clause of the query. + * @param limit the {@code LIMIT} clause of the query. + * @param offset the {@code OFFSET} clause of the query. + * @return the entries from the provided table, or empty if none was found. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > SQLElementList getAll(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer limit, Integer offset) throws DBException { + SQLElementList elmts = new SQLElementList<>(); + forEach(elemClass, where, orderBy, limit, offset, elmts::add); + return elmts; + } - } - - - - public static > long count(Class elemClass) throws DBException { - return count(elemClass, null); - } - - public static > long count(Class elemClass, SQLWhere where) throws DBException { - initTable(elemClass); + /** + * Iterate through all the entries from the provided table. + * @param elemClass the class representing a table. + * @param action the action to perform on each entries. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > void forEach(Class elemClass, Consumer action) throws DBException { + forEach(elemClass, null, null, null, null, action); + } - try { - String sql = "SELECT COUNT(*) as count FROM " + getTableName(elemClass); + /** + * Iterate through the entries from the provided table, using the provided {@code WHERE} clause. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param action the action to perform on each entries. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > void forEach(Class elemClass, SQLWhere where, Consumer action) throws DBException { + forEach(elemClass, where, null, null, null, action); + } - List params = new ArrayList<>(); + /** + * Iterate through the entries from the provided table, using the provided {@code WHERE} and {@code ORDER BY} + * clauses. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param orderBy the {@code ORDER BY} clause of the query. + * @param action the action to perform on each entries. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > void forEach(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Consumer action) throws DBException { + forEach(elemClass, where, orderBy, null, null, action); + } - if (where != null) { - ParameterizedSQLString ret = where.toSQL(); - sql += " WHERE " + ret.sqlString(); - params.addAll(ret.parameters()); - } - sql += ";"; - - try (ResultSet set = customQueryStatement(sql, params)) { - if (set.next()) { - return set.getLong(1); - } - } - } catch (SQLException e) { - throw new DBException(e); - } - - throw new DBException("Can’t retrieve element count from database (The ResultSet may be empty)"); + /** + * Iterate through the entries from the provided table, using the provided {@code WHERE}, {@code ORDER BY} and + * {@code LIMIT} clauses. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param orderBy the {@code ORDER BY} clause of the query. + * @param limit the {@code LIMIT} clause of the query. + * @param action the action to perform on each entries. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > void forEach(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer limit, Consumer action) throws DBException { + forEach(elemClass, where, orderBy, limit, null, action); + } - } - - - - - public static ResultSet customQueryStatement(String sql, List params) throws DBException { - try { - PreparedStatement ps = connection.getNativeConnection().prepareStatement(sql); - int i = 1; - for (Object val : params) { - if (val instanceof Enum) val = ((Enum) val).name(); - ps.setObject(i++, val); - } - Log.debug(ps.toString()); - - ResultSet rs = ps.executeQuery(); - - ps.closeOnCompletion(); - - return rs; - } catch (SQLException e) { - throw new DBException(e); - } + /** + * Iterate through the entries from the provided table, using the provided {@code WHERE}, {@code ORDER BY}, + * {@code LIMIT} and {@code OFFSET} clauses. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param orderBy the {@code ORDER BY} clause of the query. + * @param limit the {@code LIMIT} clause of the query. + * @param offset the {@code OFFSET} clause of the query. + * @param action the action to perform on each entries. + * @param the type representing the table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > void forEach(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer limit, Integer offset, Consumer action) throws DBException { + initTable(elemClass); - } + String sql = "SELECT * FROM " + getTableName(elemClass); - + List params = new ArrayList<>(); - - public static > SQLUpdate update(Class elemClass, SQLWhere where) { - return new SQLUpdate<>(elemClass, where); - } - - /* package */ static > int update(Class elemClass, SQLWhere where, Map, Object> values) throws DBException { - return new SQLUpdate<>(elemClass, where, values).execute(); - } + if (where != null) { + ParameterizedSQLString ret = where.toSQL(); + sql += " WHERE " + ret.sqlString(); + params.addAll(ret.parameters()); + } + if (orderBy != null) + sql += " ORDER BY " + orderBy.toSQL(); + if (limit != null) + sql += " LIMIT " + limit; + if (offset != null) + sql += " OFFSET " + offset; + sql += ";"; - - /** - * Delete the elements of the table represented by {@code elemClass} which meet the condition {@code where}. - * @param elemClass the SQLElement representing the table. - * @param where the condition to meet for an element to be deleted from the table. If null, the table is truncated using {@link #truncateTable(Class)}. - * @return The return value of {@link PreparedStatement#executeUpdate()}, for an SQL query {@code DELETE}. - */ - public static > int delete(Class elemClass, SQLWhere where) throws DBException { - initTable(elemClass); - - if (where == null) { - return truncateTable(elemClass); - } - - ParameterizedSQLString whereData = where.toSQL(); - - String sql = "DELETE FROM " + getTableName(elemClass) - + " WHERE " + whereData.sqlString() - + ";"; - List params = new ArrayList<>(whereData.parameters()); - - return customUpdateStatement(sql, params); + customQueryStatement(sql, params, set -> { + while (set.next()) { + E elm = getElementInstance(set, elemClass); + action.accept(elm); + } + return null; + }); + } - } - - - - public static int customUpdateStatement(String sql, List params) throws DBException { - try (PreparedStatement ps = connection.getNativeConnection().prepareStatement(sql)) { - int i = 1; - for (Object val : params) { - if (val instanceof Enum) val = ((Enum) val).name(); - ps.setObject(i++, val); - } - Log.debug(ps.toString()); - - return ps.executeUpdate(); - } catch (SQLException e) { - throw new DBException(e); - } - } - - - - public static > int truncateTable(Class elemClass) throws DBException { - try (Statement stmt = connection.getNativeConnection().createStatement()) { + + /** + * Counts the number of entries in the provided table. + * @param elemClass the class representing a table. + * @param the type representing the table. + * @return the number of entries in the provided table. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > long count(Class elemClass) throws DBException { + return count(elemClass, null); + } + + /** + * Counts the number of entries from the provided table, that meet the {@code WHERE} clause conditions. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param the type representing the table. + * @return the number of entries from the provided table, that meet the {@code WHERE} clause conditions. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > long count(Class elemClass, SQLWhere where) throws DBException { + initTable(elemClass); + + String sql = "SELECT COUNT(*) AS count FROM " + getTableName(elemClass); + + List params = new ArrayList<>(); + + if (where != null) { + ParameterizedSQLString ret = where.toSQL(); + sql += " WHERE " + ret.sqlString(); + params.addAll(ret.parameters()); + } + sql += ";"; + + return customQueryStatement(sql, params, rs -> { + if (rs.next()) { + return rs.getLong(1); + } + throw new DBException("Can’t retrieve element count from database (the ResultSet is empty)."); + }); + } + + + /** + * Execute a custom SQL query statement with the provided parameters, and passes the produced {@link ResultSet} + * to the {@code rsFunction}. + * @param sql the query in SQL language, passed to {@link Connection#prepareStatement(String)}. + * @param params the parameters to put in the query. Uses {@link PreparedStatement#setObject(int, Object)}. + * @param rsFunction the function executed with the result set as the parameter. Its return value will then be + * returned to the caller of this method. + * @param the return type of {@code rsFunction}. + * @return the value returned by {@code rsFunction}. + * @throws DBException if an error occurs when interacting with the database. + */ + public static R customQueryStatement(String sql, List params, ResultSetFunction rsFunction) throws DBException { + try (Connection c = connection.getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + + int i = 1; + for (Object val : params) { + if (val instanceof Enum) val = ((Enum) val).name(); + ps.setObject(i++, val); + } + Log.debug(ps.toString()); + + try (ResultSet set = ps.executeQuery()) { + return rsFunction.apply(set); + } + } catch (SQLException e) { + throw new DBException(e); + } + + } + + + /** + * A function that takes a {@link ResultSet} as an input and output any value. + * @param the return type. + */ + @FunctionalInterface + public interface ResultSetFunction { + /** + * Reads data into the result set. + * @param resultSet the result set to read. + * @throws SQLException if an error occurs while reading the result set. + * @throws DBException if an error occurs while reading the result set. + * @return data computed from the resultSet. + */ + R apply(ResultSet resultSet) throws SQLException, DBException; + } + + + /** + * Prepares an UPDATE query to the database. + * Call the {@link SQLUpdateBuilder#set(SQLField, Object)} for any field you want to change the value, then call + * {@link SQLUpdateBuilder#execute()} to send the query. + * @param elemClass the class representing a table. + * @param where the {@code WHERE} clause of the query. + * @param the type representing the table. + * @return an {@link SQLUpdateBuilder} instance. + */ + public static > SQLUpdateBuilder update(Class elemClass, SQLWhere where) { + return new SQLUpdateBuilder<>(elemClass, where); + } + + /* package */ static > int update(Class elemClass, SQLWhere where, Map, Object> values) throws DBException { + return new SQLUpdateBuilder<>(elemClass, where, values).execute(); + } + + + /** + * Delete the entries from the provided table, using the provided {@code WHERE} clause. + * @param elemClass the class representing a table. + * @param where the condition to meet for an element to be deleted from the table. If null, the table is truncated + * using {@link #truncateTable(Class)}. + * @param the type representing the table. + * @return The return value of {@link PreparedStatement#executeUpdate()}, for an SQL query {@code DELETE}. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > int delete(Class elemClass, SQLWhere where) throws DBException { + initTable(elemClass); + + if (where == null) { + return truncateTable(elemClass); + } + + ParameterizedSQLString whereData = where.toSQL(); + + String sql = "DELETE FROM " + getTableName(elemClass) + + " WHERE " + whereData.sqlString() + + ";"; + List params = new ArrayList<>(whereData.parameters()); + + return customUpdateStatement(sql, params); + + } + + + + /** + * Execute a custom SQL update statement with the provided parameters. + * @param sql the query in SQL language, passed to {@link Connection#prepareStatement(String)}. + * @param params the parameters to put in the query. Uses {@link PreparedStatement#setObject(int, Object)}. + * @return the value returned by {@link PreparedStatement#executeUpdate()}. + * @throws DBException if an error occurs when interacting with the database. + */ + public static int customUpdateStatement(String sql, List params) throws DBException { + try (Connection c = connection.getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + + int i = 1; + for (Object val : params) { + if (val instanceof Enum) val = ((Enum) val).name(); + ps.setObject(i++, val); + } + Log.debug(ps.toString()); + + return ps.executeUpdate(); + } catch (SQLException e) { + throw new DBException(e); + } + } + + + + /** + * Truncate provided table. + * @param elemClass the class representing a table. + * @param the type representing the table. + * @return The return value of {@link PreparedStatement#executeUpdate()}, for an SQL query {@code DELETE}. + * @throws DBException if an error occurs when interacting with the database. + */ + public static > int truncateTable(Class elemClass) throws DBException { + try (Connection c = connection.getConnection(); + Statement stmt = c.createStatement()) { return stmt.executeUpdate("TRUNCATE `" + getTableName(elemClass) + "`"); } catch(SQLException e) { - throw new DBException(e); + throw new DBException(e); } - } + } - @SuppressWarnings("unchecked") - private static > E getElementInstance(ResultSet set, Class elemClass) throws DBException { - try { - E instance = elemClass.getConstructor(int.class).newInstance(set.getInt("id")); + @SuppressWarnings("unchecked") + private static > E getElementInstance(ResultSet set, Class elemClass) throws DBException { + try { + E instance = elemClass.getConstructor(int.class).newInstance(set.getInt("id")); - int fieldCount = set.getMetaData().getColumnCount(); + int fieldCount = set.getMetaData().getColumnCount(); - for (int c = 1; c <= fieldCount; c++) { - String fieldName = set.getMetaData().getColumnLabel(c); - - // ignore when field is present in database but not handled by SQLElement instance - if (!instance.getFields().containsKey(fieldName)) continue; - - SQLField sqlField = (SQLField) instance.getFields().get(fieldName); - - boolean customType = sqlField.type instanceof SQLCustomType; - - Object val = set.getObject(c, - (Class)(customType ? ((SQLCustomType)sqlField.type).intermediateJavaType - : sqlField.type.getJavaType())); - - if (val == null || set.wasNull()) { - instance.set(sqlField, null, false); - } - else { - if (customType) { - try { - val = ((SQLCustomType)sqlField.type).dbToJavaConv.apply(val); - } catch (Exception e) { - throw new DBException("Error while converting value of field '"+sqlField.getName()+"' with SQLCustomType from "+((SQLCustomType)sqlField.type).intermediateJavaType - +"(jdbc source) to "+sqlField.type.getJavaType()+"(java destination). The original value is '"+ val +"'", e); - } - } - - instance.set(sqlField, val, false); - // la valeur venant de la BDD est marqué comme "non modifié" - // dans l'instance car le constructeur de l'instance met - // tout les champs comme modifiés - instance.modifiedSinceLastSave.remove(sqlField.getName()); - - } - } + for (int c = 1; c <= fieldCount; c++) { + String fieldName = set.getMetaData().getColumnLabel(c); - if (!instance.isValidForSave()) throw new DBException( - "This SQLElement representing a database entry is not valid for save : " + instance); + // ignore when field is present in database but not handled by SQLElement instance + if (!instance.getFields().containsKey(fieldName)) continue; - return instance; - } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | SQLException e) { - throw new DBException("Can't instanciate " + elemClass.getName(), e); - } - } + SQLField sqlField = (SQLField) instance.getFields().get(fieldName); - private DB() {} + boolean customType = sqlField.type instanceof SQLCustomType; + + Object val = set.getObject(c, + (Class)(customType ? ((SQLCustomType)sqlField.type).intermediateJavaType + : sqlField.type.getJavaType())); + + if (val == null || set.wasNull()) { + instance.set(sqlField, null, false); + } + else { + if (customType) { + try { + val = ((SQLCustomType)sqlField.type).dbToJavaConv.apply(val); + } catch (Exception e) { + throw new DBException("Error while converting value of field '"+sqlField.getName()+"' with SQLCustomType from "+((SQLCustomType)sqlField.type).intermediateJavaType + +"(jdbc source) to "+sqlField.type.getJavaType()+"(java destination). The original value is '"+ val +"'", e); + } + } + + /* + * The value from the DB is marked as not-modified in the entry instance since this boolean is set + * only when the value differs from the DB. + */ + instance.set(sqlField, val, false); + instance.modifiedSinceLastSave.remove(sqlField.getName()); + + } + } + + if (!instance.isValidForSave()) throw new DBException( + "This SQLElement representing a database entry is not valid for save : " + instance); + + return instance; + } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | SQLException e) { + throw new DBException("Can't instanciate " + elemClass.getName(), e); + } + } + + private DB() {} } diff --git a/pandalib-db/src/main/java/fr/pandacube/lib/db/DBConnection.java b/pandalib-db/src/main/java/fr/pandacube/lib/db/DBConnection.java index 029c134..4a24aae 100644 --- a/pandalib-db/src/main/java/fr/pandacube/lib/db/DBConnection.java +++ b/pandalib-db/src/main/java/fr/pandacube/lib/db/DBConnection.java @@ -1,83 +1,60 @@ package fr.pandacube.lib.db; import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; import java.sql.SQLException; -import fr.pandacube.lib.util.Log; +import org.apache.commons.dbcp2.BasicDataSource; +/** + * A class holding the connection to the database. + */ public class DBConnection { - private static final long CONNECTION_CHECK_TIMEOUT = 30000; // in ms - - private Connection conn; - private final String url; - private final String login; - private final String pass; - - private long timeOfLastCheck = 0; - public DBConnection(String host, int port, String dbname, String l, String p) - throws SQLException { - url = "jdbc:mysql://" + host + ":" + port + "/" + dbname - + "?autoReconnect=true" - + "&useUnicode=true" - + "&useSSL=false" - + "&characterEncoding=utf8" - + "&characterSetResults=utf8" - + "&character_set_server=utf8mb4" - + "&character_set_connection=utf8mb4"; - login = l; - pass = p; - connect(); - } + private final BasicDataSource connSource; - private void checkConnection() throws SQLException { - if (!isConnected()) { - Log.info("Connection to the database lost. Trying to reconnect..."); - close(); - connect(); - } - } - - private boolean isConnected() - { - try { - if (conn.isClosed()) - return false; - - // avoid checking the connection everytime we want to do a db request - long now = System.currentTimeMillis(); - if (timeOfLastCheck + CONNECTION_CHECK_TIMEOUT > now) - return true; - - timeOfLastCheck = now; - - if (conn.isValid(1)) - return true; - - try (ResultSet rs = conn.createStatement().executeQuery("SELECT 1;")) { - return rs != null && rs.next(); - } - } catch (Exception e) { - return false; - } + /** + * Create a new connection with the provided settings. + * @param host the MySQL DB host. + * @param port the MySQL DB port. + * @param dbname the MySQL DB name. + * @param login the login/username. + * @param password the password. + */ + public DBConnection(String host, int port, String dbname, String login, String password) { + this("jdbc:mysql://" + host + ":" + port + "/" + dbname + + "?useUnicode=true" + + "&useSSL=false" + + "&characterEncoding=utf8" + + "&characterSetResults=utf8" + + "&character_set_server=utf8mb4" + + "&character_set_connection=utf8mb4", + login, password); } - public Connection getNativeConnection() throws SQLException { - checkConnection(); - return conn; - } + /** + * Create a new connection with the provided settings. + * @param url the JDBC URL. + * @param login the login/username. + * @param password the password. + */ + public DBConnection(String url, String login, String password) { + connSource = new BasicDataSource(); + connSource.setUrl(url); + connSource.setUsername(login); + connSource.setPassword(password); + } - private void connect() throws SQLException { - conn = DriverManager.getConnection(url, login, pass); - timeOfLastCheck = System.currentTimeMillis(); - } + /* package */ Connection getConnection() throws SQLException { + return connSource.getConnection(); + } - public void close() { - try { - conn.close(); - } catch (Exception ignored) {} - } + /** + * Closes the connection. + */ + public void close() { + try { + connSource.close(); + } catch (SQLException ignored) {} + } } diff --git a/pandalib-db/src/main/java/fr/pandacube/lib/db/DBException.java b/pandalib-db/src/main/java/fr/pandacube/lib/db/DBException.java index 585ac72..2bbbe70 100644 --- a/pandalib-db/src/main/java/fr/pandacube/lib/db/DBException.java +++ b/pandalib-db/src/main/java/fr/pandacube/lib/db/DBException.java @@ -1,17 +1,20 @@ package fr.pandacube.lib.db; +/** + * Exception thrown when something bad happends when using the {@link DB} API. + */ public class DBException extends Exception { - public DBException(Throwable initCause) { - super(initCause); - } + /* package */ DBException(Throwable initCause) { + super(initCause); + } - public DBException(String message, Throwable initCause) { - super(message, initCause); - } + /* package */ DBException(String message, Throwable initCause) { + super(message, initCause); + } - public DBException(String message) { - super(message); - } + /* package */ DBException(String message) { + super(message); + } } diff --git a/pandalib-db/src/main/java/fr/pandacube/lib/db/DBInitTableException.java b/pandalib-db/src/main/java/fr/pandacube/lib/db/DBInitTableException.java index 45d592d..03c6052 100644 --- a/pandalib-db/src/main/java/fr/pandacube/lib/db/DBInitTableException.java +++ b/pandalib-db/src/main/java/fr/pandacube/lib/db/DBInitTableException.java @@ -1,13 +1,16 @@ package fr.pandacube.lib.db; +/** + * Exception thrown when something bad happends when initializing a new table using {@link DB#initTable(Class)}. + */ public class DBInitTableException extends DBException { - /* package */ > DBInitTableException(Class tableElem, Throwable t) { - super("Error while initializing table " + ((tableElem != null) ? tableElem.getName() : "null"), t); - } + /* package */ > DBInitTableException(Class tableElem, Throwable t) { + super("Error while initializing table " + ((tableElem != null) ? tableElem.getName() : "null"), t); + } - /* package */ > DBInitTableException(Class tableElem, String message) { - super("Error while initializing table " + ((tableElem != null) ? tableElem.getName() : "null") + ": " + message); - } + /* package */ > DBInitTableException(Class tableElem, String message) { + super("Error while initializing table " + ((tableElem != null) ? tableElem.getName() : "null") + ": " + message); + } } diff --git a/pandalib-db/src/main/java/fr/pandacube/lib/db/ParameterizedSQLString.java b/pandalib-db/src/main/java/fr/pandacube/lib/db/ParameterizedSQLString.java index a6b9455..b98b21d 100644 --- a/pandalib-db/src/main/java/fr/pandacube/lib/db/ParameterizedSQLString.java +++ b/pandalib-db/src/main/java/fr/pandacube/lib/db/ParameterizedSQLString.java @@ -2,5 +2,5 @@ package fr.pandacube.lib.db; import java.util.List; -public record ParameterizedSQLString(String sqlString, List parameters) { +/* package */ record ParameterizedSQLString(String sqlString, List parameters) { } diff --git a/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLCustomType.java b/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLCustomType.java index b00879b..c0c507f 100644 --- a/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLCustomType.java +++ b/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLCustomType.java @@ -3,23 +3,45 @@ package fr.pandacube.lib.db; import java.util.function.Function; /** - * @param intermediate type, the type of the value transmitted to the JDBC - * @param Java type + * Represents a SQL type that needs conversion from and to the JDBC values. + *

+ * For instance, if there is a UUID field in a table, it’s possible to create a new type as follows: + *

{@code
+ * SQLType UUID = new SQLCustomType<>(SQLElement.CHAR(36), UUID.class, UUID::fromString, UUID::toString);
+ * }
+ * @param intermediate Java type, the type of the value transmitted to the JDBC. + * @param the final Java type. */ public class SQLCustomType extends SQLType { - - public final Class intermediateJavaType; - public final Function dbToJavaConv; - public final Function javaToDbConv; - /* package */ SQLCustomType(SQLType type, Class javaT, Function dbToJava, Function javaToDb) { - this(type.sqlDeclaration, type.getJavaType(), javaT, dbToJava, javaToDb); - } - - /* package */ SQLCustomType(String sqlD, Class intermediateJavaT, Class javaT, Function dbToJava, Function javaToDb) { - super(sqlD, javaT); - intermediateJavaType = intermediateJavaT; - dbToJavaConv = dbToJava; - javaToDbConv = javaToDb; - } + /* package */ final Class intermediateJavaType; + /* package */ final Function dbToJavaConv; + /* package */ final Function javaToDbConv; + + /** + * Creates a new custom type, using a type that is already managed by JDBC and already has a {@link SQLType} + * instance, like {@link SQLElement#VARCHAR(int)}. + * @param type the raw {@link SQLType} instance. + * @param javaT the class of the Java type. + * @param dbToJava a function that converts from the JDBC value to Java value. + * @param javaToDb a function that converts from Java value to JDBC value. + */ + public SQLCustomType(SQLType type, Class javaT, Function dbToJava, Function javaToDb) { + this(type.sqlDeclaration, type.getJavaType(), javaT, dbToJava, javaToDb); + } + + /** + * Creates a new custom type. + * @param sqlD the name of the type in SQL (like {@code BLOB} or {@code BIGINT}). + * @param intermediateJavaT the class of the JDBC value type. + * @param javaT the class of the Java value type. + * @param dbToJava a function that converts from the JDBC value to Java value. + * @param javaToDb a function that converts from Java value to JDBC value. + */ + public SQLCustomType(String sqlD, Class intermediateJavaT, Class javaT, Function dbToJava, Function javaToDb) { + super(sqlD, javaT); + intermediateJavaType = intermediateJavaT; + dbToJavaConv = dbToJava; + javaToDbConv = javaToDb; + } } diff --git a/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLElement.java b/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLElement.java index 270b306..5109f4b 100644 --- a/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLElement.java +++ b/pandalib-db/src/main/java/fr/pandacube/lib/db/SQLElement.java @@ -1,13 +1,20 @@ package fr.pandacube.lib.db; import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.sql.Connection; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -21,475 +28,776 @@ import java.util.stream.Collectors; import fr.pandacube.lib.util.EnumUtil; import fr.pandacube.lib.util.Log; +/** + * Represents an entry in a SQL table. Each subclass is for a specific table. + * @param the type of the subclass. + */// TODO exemple subclass public abstract class SQLElement> { - /** cache for fields for each subclass of SQLElement */ - /* package */ static final Map>, SQLFieldMap>> fieldsCache = new HashMap<>(); - - private final DBConnection db = DB.getConnection(); - - private boolean stored = false; - private int id; - - private final SQLFieldMap fields; - - private final Map, Object> values; - /* package */ final Set modifiedSinceLastSave; - - @SuppressWarnings("unchecked") - public SQLElement() { - - try { - DB.initTable((Class)getClass()); - } catch (DBInitTableException e) { - throw new RuntimeException(e); - } - - if (fieldsCache.get(getClass()) == null) { - fields = new SQLFieldMap<>((Class)getClass()); - - // le champ id commun à toutes les tables - SQLField idF = new SQLField<>(INT, false, true, 0); - idF.setName("id"); - fields.addField(idF); - - generateFields(fields); - fieldsCache.put((Class)getClass(), fields); - } - else - fields = (SQLFieldMap) fieldsCache.get(getClass()); - - values = new LinkedHashMap<>(fields.size()); - modifiedSinceLastSave = new HashSet<>(fields.size()); - - initDefaultValues(); - - } - - protected SQLElement(int id) { - this(); - @SuppressWarnings("unchecked") - SQLField idField = (SQLField) fields.get("id"); - set(idField, id, false); - this.id = id; - stored = true; - } - - /** - * @return The name of the table in the database, without the prefix defined by {@link DB#init(DBConnection, String)}. - */ - protected abstract String tableName(); - - @SuppressWarnings("unchecked") - private void initDefaultValues() { - // remplissage des données par défaut (si peut être null ou si valeur - // par défaut existe) - for (@SuppressWarnings("rawtypes") - SQLField f : fields.values()) - if (f.defaultValue != null) set(f, f.defaultValue); - else if (f.canBeNull || (f.autoIncrement && !stored)) set(f, null); - } - - @SuppressWarnings("unchecked") - protected void generateFields(SQLFieldMap listToFill) { - - java.lang.reflect.Field[] declaredFields = getClass().getDeclaredFields(); - for (java.lang.reflect.Field field : declaredFields) { - if (!SQLField.class.isAssignableFrom(field.getType())) { - Log.debug("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " is of type " + field.getType().getName() + " so it will be ignored."); - continue; - } - if (!Modifier.isStatic(field.getModifiers())) { - Log.severe("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " can't be initialized because it is not static."); - continue; - } - field.setAccessible(true); - try { - Object val = field.get(null); - if (!(val instanceof SQLField)) { - Log.severe("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " can't be initialized because its value is null."); - continue; - } - SQLField checkedF = (SQLField) val; - checkedF.setName(field.getName()); - if (!Modifier.isPublic(field.getModifiers())) - Log.warning("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " should be public !"); - if (listToFill.containsKey(checkedF.getName())) throw new IllegalArgumentException( - "SQLField " + checkedF.getName() + " already exist in " + getClass().getName()); - checkedF.setSQLElementType((Class) getClass()); - listToFill.addField((SQLField) val); - } catch (IllegalArgumentException | IllegalAccessException e) { - Log.severe("Can't get value of static field " + field, e); - } - } - - } - - /* package */ Map> getFields() { - return Collections.unmodifiableMap(fields); - } - - public Map, Object> getValues() { - return Collections.unmodifiableMap(values); - } - - @SuppressWarnings("unchecked") - public E set(SQLField field, T value) { - set(field, value, true); - return (E) this; - } - - /* package */ void set(SQLField sqlField, T value, boolean setModified) { - if (sqlField == null) throw new IllegalArgumentException("sqlField can't be null"); - if (!fields.containsValue(sqlField)) // should not append at runtime because of generic type check at compilation - throw new IllegalStateException("In the table "+getClass().getName()+ ": the field asked for modification is not initialized properly."); - - if (value == null) { - if (!sqlField.canBeNull && (!sqlField.autoIncrement || stored)) - throw new IllegalArgumentException( - "SQLField '" + sqlField.getName() + "' of " + getClass().getName() + " is a NOT NULL field"); - } - else if (!sqlField.type.isInstance(value)) { - throw new IllegalArgumentException("SQLField '" + sqlField.getName() + "' of " + getClass().getName() - + " type is '" + sqlField.type + "' and can't accept values of type " - + value.getClass().getName()); - } - - if (!values.containsKey(sqlField)) { - values.put(sqlField, value); - if (setModified) modifiedSinceLastSave.add(sqlField.getName()); - } - else { - Object oldVal = values.get(sqlField); - if (!Objects.equals(oldVal, value)) { - values.put(sqlField, value); - if (setModified) modifiedSinceLastSave.add(sqlField.getName()); - } - // sinon, rien n'est modifié - } - - } - - public T get(SQLField field) { - if (field == null) throw new IllegalArgumentException("field can't be null"); - if (values.containsKey(field)) { - @SuppressWarnings("unchecked") - T val = (T) values.get(field); - return val; - } - throw new IllegalArgumentException("The field '" + field.getName() + "' in this instance of " + getClass().getName() - + " does not exist or is not set"); - } - - /** - * @param the type of the specified field - * @param

the table class of the primary key targeted by the specified foreign key field - * @return the element in the table P that his primary key correspond to the foreign key value of this element. - */ - public > P getReferencedEntry(SQLFKField field) throws DBException { - T fkValue = get(field); - if (fkValue == null) return null; - return DB.getFirst(field.getForeignElementClass(), field.getPrimaryField().eq(fkValue), null); - } - - /** - * @param the type of the specified field - * @param the table class of the foreign key that reference a primary key of this element. - * @return all elements in the table F for which the specified foreign key value correspond to the primary key of this element. - */ - public > SQLElementList getReferencingForeignEntries(SQLFKField field, SQLOrderBy orderBy, Integer limit, Integer offset) throws DBException { - T value = get(field.getPrimaryField()); - if (value == null) return new SQLElementList<>(); - return DB.getAll(field.getSQLElementType(), field.eq(value), orderBy, limit, offset); - } - - public boolean isValidForSave() { - return values.keySet().containsAll(fields.values()); - } - - private Map, Object> getOnlyModifiedValues() { - Map, Object> modifiedValues = new LinkedHashMap<>(); - values.forEach((k, v) -> { - if (modifiedSinceLastSave.contains(k.getName())) modifiedValues.put(k, v); - }); - return modifiedValues; - } - - public boolean isModified(SQLField field) { - return modifiedSinceLastSave.contains(field.getName()); - } - - @SuppressWarnings("unchecked") - public E save() throws DBException { - if (!isValidForSave()) - throw new IllegalStateException(this + " has at least one undefined value and can't be saved."); - - DB.initTable((Class)getClass()); - try { - - if (stored) { // mettre à jour les valeurs dans la base - - // restaurer l'ID au cas il aurait été changé à la main dans - // values - SQLField idField = (SQLField) fields.get("id"); - values.put(idField, id); - modifiedSinceLastSave.remove("id"); - Map, Object> modifiedValues = getOnlyModifiedValues(); - - if (modifiedValues.isEmpty()) return (E) this; - - DB.update((Class)getClass(), getFieldId().eq(getId()), modifiedValues); - } - else { // ajouter dans la base - - // restaurer l'ID au cas il aurait été changé à la main dans - // values - values.put(fields.get("id"), null); - - StringBuilder concatValues = new StringBuilder(); - StringBuilder concatFields = new StringBuilder(); - List psValues = new ArrayList<>(); - - boolean first = true; - for (Map.Entry, Object> entry : values.entrySet()) { - if (!first) { - concatValues.append(","); - concatFields.append(","); - } - first = false; - concatValues.append(" ? "); - concatFields.append("`").append(entry.getKey().getName()).append("`"); - addValueToSQLObjectList(psValues, entry.getKey(), entry.getValue()); - } - - try (PreparedStatement ps = db.getNativeConnection().prepareStatement( - "INSERT INTO " + DB.tablePrefix + tableName() + " (" + concatFields + ") VALUES (" + concatValues + ")", - Statement.RETURN_GENERATED_KEYS)) { - - int i = 1; - for (Object val : psValues) - ps.setObject(i++, val); - - ps.executeUpdate(); - - try (ResultSet rs = ps.getGeneratedKeys()) { - if (rs.next()) id = rs.getInt(1); - stored = true; - } - } - - } - - modifiedSinceLastSave.clear(); - } catch (SQLException e) { - throw new DBException("Error while saving data", e); - } - return (E) this; - } - - - @SuppressWarnings({ "rawtypes", "unchecked" }) - protected static > void addValueToSQLObjectList(List list, SQLField field, Object jValue) throws DBException { - if (jValue != null && field.type instanceof SQLCustomType) { - try { - jValue = ((SQLCustomType)field.type).javaToDbConv.apply(jValue); - } catch (Exception e) { - throw new DBException("Error while converting value of field '"+field.getName()+"' with SQLCustomType from "+field.type.getJavaType() - +"(java source) to "+((SQLCustomType)field.type).intermediateJavaType+"(jdbc destination). The original value is '"+jValue+"'", e); - } - } - list.add(jValue); - } - - public boolean isStored() { - return stored; - } - - public Integer getId() { - return (stored) ? id : null; - } - - @SuppressWarnings("unchecked") - public SQLField getFieldId() { - return (SQLField) fields.get("id"); - } - - public void delete() throws DBException { - - if (stored) { // supprimer la ligne de la base - try (PreparedStatement st = db.getNativeConnection() - .prepareStatement("DELETE FROM " + DB.tablePrefix + tableName() + " WHERE id=" + id)) { - Log.debug(st.toString()); - st.executeUpdate(); - markAsNotStored(); - } catch (SQLException e) { - throw new DBException(e); - } - } - - } - - /** - * Méthode appelée quand l'élément courant est retirée de la base de données - * via une requête externe - */ - /* package */ void markAsNotStored() { - stored = false; - id = 0; - modifiedSinceLastSave.clear(); - values.forEach((k, v) -> modifiedSinceLastSave.add(k.getName())); - } - - protected static class SQLFieldMap> extends LinkedHashMap> { - private final Class sqlElemClass; - - private SQLFieldMap(Class elemClass) { - sqlElemClass = elemClass; - } - - private void addField(SQLField f) { - if (f == null) return; - if (containsKey(f.getName())) throw new IllegalArgumentException( - "SQLField " + f.getName() + " already exist in " + sqlElemClass.getName()); - @SuppressWarnings("unchecked") - SQLField checkedF = (SQLField) f; - checkedF.setSQLElementType(sqlElemClass); - put(checkedF.getName(), checkedF); - } - - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(this.getClass().getName()); - sb.append('{'); - sb.append(fields.values().stream() - .map(f -> { - try { - return f.getName() + "=" + get(f); - } catch (IllegalArgumentException e) { - return f.getName() + "=(Undefined)"; - } - }) - .collect(Collectors.joining(", "))); - sb.append('}'); - return sb.toString(); - } - - @Override - public boolean equals(Object o) { - if (!(getClass().isInstance(o))) return false; - SQLElement oEl = (SQLElement) o; - if (oEl.getId() == null) return false; - return oEl.getId().equals(getId()); - } - - @Override - public int hashCode() { - return getClass().hashCode() ^ Objects.hashCode(getId()); - } - - - - - - - - - - - protected static , T> SQLField field(SQLType t, boolean nul, boolean autoIncr, T deflt) { - return new SQLField<>(t, nul, autoIncr, deflt); - } - - protected static , T> SQLField field(SQLType t, boolean nul) { - return new SQLField<>(t, nul); - } - - protected static , T> SQLField field(SQLType t, boolean nul, boolean autoIncr) { - return new SQLField<>(t, nul, autoIncr); - } - - protected static , T> SQLField field(SQLType t, boolean nul, T deflt) { - return new SQLField<>(t, nul, deflt); - } - - - protected static , F extends SQLElement> SQLFKField foreignKeyId(boolean nul, Class fkEl) { - return SQLFKField.idFK(nul, fkEl); - } - - protected static , F extends SQLElement> SQLFKField foreignKeyId(boolean nul, Integer deflt, Class fkEl) { - return SQLFKField.idFK(nul, deflt, fkEl); - } - - protected static , T, F extends SQLElement> SQLFKField foreignKey(boolean nul, Class fkEl, SQLField fkF) { - return SQLFKField.customFK(nul, fkEl, fkF); - } - - protected static , T, F extends SQLElement> SQLFKField foreignKey(boolean nul, T deflt, Class fkEl, SQLField fkF) { - return SQLFKField.customFK(nul, deflt, fkEl, fkF); - } - - - public static final SQLType BOOLEAN = new SQLType<>("BOOLEAN", Boolean.class); - - public static final SQLType TINYINT = new SQLType<>("TINYINT", Integer.class); // can’t be Byte due to MYSQL JDBC Connector limitations - public static final SQLType BYTE = TINYINT; - - public static final SQLType SMALLINT = new SQLType<>("SMALLINT", Integer.class); // can’t be Short due to MYSQL JDBC Connector limitations - public static final SQLType SHORT = SMALLINT; - - public static final SQLType INT = new SQLType<>("INT", Integer.class); - public static final SQLType INTEGER = INT; - - public static final SQLType BIGINT = new SQLType<>("BIGINT", Long.class); - public static final SQLType LONG = BIGINT; - - public static final SQLType DATE = new SQLType<>("DATE", Date.class); - - public static final SQLType FLOAT = new SQLType<>("FLOAT", Float.class); - - public static final SQLType DOUBLE = new SQLType<>("DOUBLE", Double.class); - - public static SQLType CHAR(int charCount) { - if (charCount <= 0) throw new IllegalArgumentException("charCount must be positive."); - return new SQLType<>("CHAR(" + charCount + ")", String.class); - } - - public static SQLType VARCHAR(int charCount) { - if (charCount <= 0) throw new IllegalArgumentException("charCount must be positive."); - return new SQLType<>("VARCHAR(" + charCount + ")", String.class); - } - - public static final SQLType TEXT = new SQLType<>("TEXT", String.class); - public static final SQLType STRING = TEXT; - - public static SQLType BINARY(int byteCount) { - if (byteCount <= 0) throw new IllegalArgumentException("byteCount must be positive."); - return new SQLType<>("BINARY(" + byteCount + ")", byte[].class); - } - - public static SQLType VARBINARY(int byteCount) { - if (byteCount <= 0) throw new IllegalArgumentException("byteCount must be positive."); - return new SQLType<>("VARBINARY(" + byteCount + ")", byte[].class); - } - - public static final SQLType BLOB = new SQLType<>("BLOB", byte[].class); - - public static > SQLType ENUM(Class enumType) { - if (enumType == null) throw new IllegalArgumentException("enumType can't be null."); - StringBuilder enumStr = new StringBuilder("'"); - boolean first = true; - for (T el : enumType.getEnumConstants()) { - if (!first) enumStr.append("', '"); - first = false; - enumStr.append(el.name()); - - } - enumStr.append("'"); - - return new SQLCustomType<>("VARCHAR(" + enumStr + ")", String.class, enumType, s -> EnumUtil.searchEnum(enumType, s), Enum::name); - } - - public static final SQLType CHAR36_UUID = new SQLCustomType<>(CHAR(36), UUID.class, UUID::fromString, UUID::toString); + + // cache for fields for each subclass of SQLElement + /* package */ static final Map>, SQLFieldMap>> fieldsCache = new HashMap<>(); + + /* package */ static class SQLFieldMap> extends LinkedHashMap> { + private final Class sqlElemClass; + + private SQLFieldMap(Class elemClass) { + sqlElemClass = elemClass; + } + + private void addField(SQLField f) { + if (f == null) return; + if (containsKey(f.getName())) throw new IllegalArgumentException( + "SQLField " + f.getName() + " already exist in " + sqlElemClass.getName()); + @SuppressWarnings("unchecked") + SQLField checkedF = (SQLField) f; + checkedF.setSQLElementType(sqlElemClass); + put(checkedF.getName(), checkedF); + } + + } + + private final DBConnection db = DB.getConnection(); + + private boolean stored = false; + private int id; + + private final SQLFieldMap fields; + + private final Map, Object> values; + /* package */ final Set modifiedSinceLastSave; + + /** + * Create a new instance of a table entry, not yet saved in the database. + * All the required values has to be set before saving the entry in the database. + */ + @SuppressWarnings("unchecked") + protected SQLElement() { + + try { + DB.initTable((Class)getClass()); + } catch (DBInitTableException e) { + throw new RuntimeException(e); + } + + if (fieldsCache.get(getClass()) == null) { + fields = new SQLFieldMap<>(getCheckedClass()); + + // le champ id commun à toutes les tables + SQLField idF = new SQLField<>(INT, false, true, 0); + idF.setName("id"); + fields.addField(idF); + + generateFields(fields); + fieldsCache.put(getCheckedClass(), fields); + } + else + fields = (SQLFieldMap) fieldsCache.get(getClass()); + + values = new LinkedHashMap<>(fields.size()); + modifiedSinceLastSave = new HashSet<>(fields.size()); + + initDefaultValues(); + + } + + /** + * Create a new instance of a table entry, representing an already present one in the database. + *

+ * Subclasses must implement a constructor with the same signature, that calls this parent constructor, and may be + * private to avoid accidental instanciation. This constructor will be called by the DB API when fetching entries + * from the database. + * @param id the id of the entry in the database. + */ + protected SQLElement(int id) { + this(); + @SuppressWarnings("unchecked") + SQLField idField = (SQLField) fields.get("id"); + set(idField, id, false); + this.id = id; + stored = true; + } + + /** + * Gets the name of the table in the database, without the prefix defined by {@link DB#init(DBConnection, String)}. + * @return The unprefixed name of the table in the database. + */ + protected abstract String tableName(); + + /** + * Gets a checked version of the {@link Class} instance of this {@link SQLElement} object. + * @return {@code (Class) getClass()}; + */ + @SuppressWarnings("unchecked") + public Class getCheckedClass() { + return (Class) getClass(); + } + + /** + * Fills the values of this entry that are known to be nullable or have a default value. + */ + @SuppressWarnings("unchecked") + private void initDefaultValues() { + for (@SuppressWarnings("rawtypes") + SQLField f : fields.values()) + if (f.defaultValue != null) set(f, f.defaultValue); + else if (f.nullable || (f.autoIncrement && !stored)) set(f, null); + } + + @SuppressWarnings("unchecked") + private void generateFields(SQLFieldMap listToFill) { + + java.lang.reflect.Field[] declaredFields = getClass().getDeclaredFields(); + for (java.lang.reflect.Field field : declaredFields) { + if (!SQLField.class.isAssignableFrom(field.getType())) { + Log.debug("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " is of type " + field.getType().getName() + " so it will be ignored."); + continue; + } + if (!Modifier.isStatic(field.getModifiers())) { + Log.severe("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " can't be initialized because it is not static."); + continue; + } + field.setAccessible(true); + try { + Object val = field.get(null); + if (!(val instanceof SQLField)) { + Log.severe("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " can't be initialized because its value is null."); + continue; + } + SQLField checkedF = (SQLField) val; + checkedF.setName(field.getName()); + if (!Modifier.isPublic(field.getModifiers())) + Log.warning("[ORM] The field " + field.getDeclaringClass().getName() + "." + field.getName() + " should be public !"); + if (listToFill.containsKey(checkedF.getName())) throw new IllegalArgumentException( + "SQLField " + checkedF.getName() + " already exist in " + getClass().getName()); + checkedF.setSQLElementType((Class) getClass()); + listToFill.addField((SQLField) val); + } catch (IllegalArgumentException | IllegalAccessException e) { + Log.severe("Can't get value of static field " + field, e); + } + } + + } + + /* package */ Map> getFields() { + return Collections.unmodifiableMap(fields); + } + + /** + * Gets the fiels of this entry’s table, mapped to the values of this entry. + * @return the fiels of this entry’s table, mapped to the values of this entry. + */ + public Map, Object> getValues() { + return Collections.unmodifiableMap(values); + } + + /** + * Sets a value in this entry. + *

+ * This is not good practice to set the {@code id} field of any entry, because it’s an unique auto-incremented + * value. Use {@link #save()} and {@link #delete()} to set or unset the {@code id} instead, in consistence with the + * database. + * @param field the field to set. + * @param value the new value for this field. + * @return this. + * @param the Java type of the field. + */ + @SuppressWarnings("unchecked") + public E set(SQLField field, T value) { + set(field, value, true); + return (E) this; + } + + /* package */ void set(SQLField sqlField, T value, boolean setModified) { + if (sqlField == null) throw new IllegalArgumentException("sqlField can't be null"); + if (!fields.containsValue(sqlField)) // should not append at runtime because of generic type check at compilation + throw new IllegalStateException("In the table "+getClass().getName()+ ": the field asked for modification is not initialized properly."); + + if (value == null) { + if (!sqlField.nullable && (!sqlField.autoIncrement || stored)) + throw new IllegalArgumentException( + "SQLField '" + sqlField.getName() + "' of " + getClass().getName() + " is a NOT NULL field"); + } + else if (!sqlField.type.isInstance(value)) { + throw new IllegalArgumentException("SQLField '" + sqlField.getName() + "' of " + getClass().getName() + + " type is '" + sqlField.type + "' and can't accept values of type " + + value.getClass().getName()); + } + + if (!values.containsKey(sqlField)) { + values.put(sqlField, value); + if (setModified) modifiedSinceLastSave.add(sqlField.getName()); + } + else { + Object oldVal = values.get(sqlField); + if (!Objects.equals(oldVal, value)) { + values.put(sqlField, value); + if (setModified) modifiedSinceLastSave.add(sqlField.getName()); + } + // sinon, rien n'est modifié + } + } + + /** + * Gets the value of the provided field in this entry. + * @param field the field to get the value from. + * @return the value of the provided field in this entry. + * @param the Java type of the field. + */ + public T get(SQLField field) { + if (field == null) throw new IllegalArgumentException("field can't be null"); + if (values.containsKey(field)) { + @SuppressWarnings("unchecked") + T val = (T) values.get(field); + return val; + } + throw new IllegalArgumentException("The field '" + field.getName() + "' in this instance of " + getClass().getName() + + " does not exist or is not set"); + } + + /** + * Gets the foreign table entry targeted by the provided foreignkey of this table. + * @param field a foreignkey of this table. + * @param the type of the foreignkey field. + * @param

the targeted foreign table type. + * @return the foreign table entry targeted by the provided foreignkey of this table. + * @throws DBException if an error occurs when interacting with the database. + */ + public > P getReferencedEntry(SQLFKField field) throws DBException { + T fkValue = get(field); + if (fkValue == null) return null; + return DB.getFirst(field.getForeignElementClass(), field.getPrimaryField().eq(fkValue), null); + } + + /** + * Gets the original table entry which the provided foreign key is targeting this entry, and following the provided + * {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses. + * @param field a foreignkey in the original table. + * @param orderBy the {@code ORDER BY} clause of the query. + * @param limit the {@code LIMIT} clause of the query. + * @param offset the {@code OFFSET} clause of the query. + * @param the type of the foreignkey field. + * @param the table class of the foreign key that reference a field of this entry. + * @return the original table entry which the provided foreign key is targeting this entry. + * @throws DBException if an error occurs when interacting with the database. + */ + public > SQLElementList getReferencingForeignEntries(SQLFKField field, SQLOrderBy orderBy, Integer limit, Integer offset) throws DBException { + T value = get(field.getPrimaryField()); + if (value == null) return new SQLElementList<>(); + return DB.getAll(field.getSQLElementType(), field.eq(value), orderBy, limit, offset); + } + + /** + * Determine if this entry is valid for save, that is when all the values are either set, or are nullable or have a + * default value. + * @return true if this entry is valid for save, false otherwise. + */ + public boolean isValidForSave() { + return values.keySet().containsAll(fields.values()); + } + + private Map, Object> getOnlyModifiedValues() { + Map, Object> modifiedValues = new LinkedHashMap<>(); + values.forEach((k, v) -> { + if (modifiedSinceLastSave.contains(k.getName())) modifiedValues.put(k, v); + }); + return modifiedValues; + } + + /** + * Determine if the provided entry has been modified since this entry was created or saved. + * @param field the field to check in this entry. + * @return true if the field has been modified, false otherwise. + */ + public boolean isModified(SQLField field) { + return modifiedSinceLastSave.contains(field.getName()); + } + + /** + * Saves this entry into the database, either by updating the already existing entry in it, or by creating a new + * entry if it doesn’t exist yet. + * @return this. + * @throws DBException if an error occurs when interacting with the database. + */ + @SuppressWarnings("unchecked") + public E save() throws DBException { + if (!isValidForSave()) + throw new IllegalStateException(this + " has at least one undefined value and can't be saved."); + + DB.initTable((Class)getClass()); + try { + + if (stored) { // update in database + // restore the id field to its real value in case it was modified using #set(...) + values.put(fields.get("id"), id); + modifiedSinceLastSave.remove("id"); + + Map, Object> modifiedValues = getOnlyModifiedValues(); + + if (modifiedValues.isEmpty()) return (E) this; + + DB.update((Class)getClass(), getIdField().eq(getId()), modifiedValues); + } + else { // add entry in the database + // restore the id field to its real value in case it was modified using #set(...) + values.put(fields.get("id"), null); + + StringBuilder concatValues = new StringBuilder(); + StringBuilder concatFields = new StringBuilder(); + List psValues = new ArrayList<>(); + + boolean first = true; + for (Map.Entry, Object> entry : values.entrySet()) { + if (!first) { + concatValues.append(","); + concatFields.append(","); + } + first = false; + concatValues.append(" ? "); + concatFields.append("`").append(entry.getKey().getName()).append("`"); + addValueToSQLObjectList(psValues, entry.getKey(), entry.getValue()); + } + + try (Connection c = db.getConnection(); + PreparedStatement ps = c.prepareStatement("INSERT INTO " + DB.tablePrefix + tableName() + " (" + concatFields + ") VALUES (" + concatValues + ")", + Statement.RETURN_GENERATED_KEYS)) { + + int i = 1; + for (Object val : psValues) + ps.setObject(i++, val); + + ps.executeUpdate(); + + try (ResultSet rs = ps.getGeneratedKeys()) { + if (rs.next()) id = rs.getInt(1); + stored = true; + } + } + + } + + modifiedSinceLastSave.clear(); + } catch (SQLException e) { + throw new DBException("Error while saving data", e); + } + return (E) this; + } + + + @SuppressWarnings({ "rawtypes", "unchecked" }) + /* package */ static > void addValueToSQLObjectList(List list, SQLField field, Object jValue) throws DBException { + if (jValue != null && field.type instanceof SQLCustomType) { + try { + jValue = ((SQLCustomType)field.type).javaToDbConv.apply(jValue); + } catch (Exception e) { + throw new DBException("Error while converting value of field '"+field.getName()+"' with SQLCustomType from "+field.type.getJavaType() + +"(java source) to "+((SQLCustomType)field.type).intermediateJavaType+"(jdbc destination). The original value is '"+jValue+"'", e); + } + } + list.add(jValue); + } + + /** + * Tells if this entry is currently stored in DB or not. + * @return true if this entry is currently stored in DB, or false otherwise. + */ + public boolean isStored() { + return stored; + } + + /** + * Gets the id of the entry in the DB. + * @return the id of the entry in the DB, or null if it’s not saved. + */ + public Integer getId() { + return (stored) ? id : null; + } + + /** + * Gets the {@link SQLField} instance corresponding to the {@code id} field of this table. + * @return the {@link SQLField} instance corresponding to the {@code id} field of this table. + */ + @SuppressWarnings("unchecked") + public SQLField getIdField() { + return (SQLField) fields.get("id"); + } + + /** + * Deletes this entry from the database. + * @throws DBException if an error occurs when interacting with the database. + */ + public void delete() throws DBException { + if (stored) { + DB.delete(getCheckedClass(), getIdField().eq(id)); + markAsNotStored(); + } + } + + /* package */ void markAsNotStored() { + stored = false; + id = 0; + modifiedSinceLastSave.clear(); + values.forEach((k, v) -> modifiedSinceLastSave.add(k.getName())); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(this.getClass().getName()); + sb.append('{'); + sb.append(fields.values().stream() + .map(f -> { + try { + return f.getName() + "=" + get(f); + } catch (IllegalArgumentException e) { + return f.getName() + "=(Undefined)"; + } + }) + .collect(Collectors.joining(", "))); + sb.append('}'); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (!(getClass().isInstance(o))) return false; + SQLElement oEl = (SQLElement) o; + if (oEl.getId() == null) return false; + return oEl.getId().equals(getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode() ^ Objects.hashCode(getId()); + } + + + /** + * Creates a new SQL field. + * @param type the type of the field. + * @param nullable true if nullable, false if {@code NOT NULL}. + * @param autoIncr if {@code AUTO_INCREMENT}. + * @param deflt a default value for this field. A null value indicate that this has no default value. + * @return the new SQL field. + * @param the table type. + * @param the Java type of this field. + */ + protected static , T> SQLField field(SQLType type, boolean nullable, boolean autoIncr, T deflt) { + return new SQLField<>(type, nullable, autoIncr, deflt); + } + + /** + * Creates a new SQL field. + * @param type the type of the field. + * @param nullable true if nullable, false if {@code NOT NULL}. + * @return the new SQL field. + * @param the table type. + * @param the Java type of this field. + */ + protected static , T> SQLField field(SQLType type, boolean nullable) { + return new SQLField<>(type, nullable); + } + + /** + * Creates a new SQL field. + * @param type the type of the field. + * @param nullable true if nullable, false if {@code NOT NULL}. + * @param autoIncr if {@code AUTO_INCREMENT}. + * @return the new SQL field. + * @param the table type. + * @param the Java type of this field. + */ + protected static , T> SQLField field(SQLType type, boolean nullable, boolean autoIncr) { + return new SQLField<>(type, nullable, autoIncr); + } + + /** + * Creates a new SQL field. + * @param type the type of the field. + * @param nullable true if nullable, false if {@code NOT NULL}. + * @param deflt a default value for this field. A null value indicate that this has no default value. + * @return the new SQL field. + * @param the table type. + * @param the Java type of this field. + */ + protected static , T> SQLField field(SQLType type, boolean nullable, T deflt) { + return new SQLField<>(type, nullable, deflt); + } + + + + + + + /** + * Creates a new SQL foreign key field pointing to the {@code id} field of the provided table. + * @param nul true if this foreign key is nullable, false if {@code NOT NULL}. + * @param deflt a default value for this field. A null value indicate that this has no default value. + * @param fkEl the target table. + * @return the new SQL foreign key field. + * @param the table type. + * @param the target table type. + */ + protected static , F extends SQLElement> SQLFKField foreignKeyId(boolean nul, Integer deflt, Class fkEl) { + return SQLFKField.idFK(nul, deflt, fkEl); + } + + /** + * Creates a new SQL foreign key field pointing to the {@code id} field of the provided table. + * @param nul true if this foreign key is nullable, false if {@code NOT NULL}. + * @param fkEl the target table. + * @return the new SQL foreign key field. + * @param the table type. + * @param the target table type. + */ + protected static , F extends SQLElement> SQLFKField foreignKeyId(boolean nul, Class fkEl) { + return SQLFKField.idFK(nul, fkEl); + } + + /** + * Creates a new SQL foreign key field pointing to the provided field. + * @param nul true if this foreign key is nullable, false if {@code NOT NULL}. + * @param deflt a default value for this field. A null value indicate that this has no default value. + * @param fkEl the target table. + * @param fkF the field in the targeted table. + * @return the new SQL foreign key field. + * @param the table type. + * @param the Java type of this field. + * @param the target table type. + */ + protected static , T, F extends SQLElement> SQLFKField foreignKey(boolean nul, T deflt, Class fkEl, SQLField fkF) { + return SQLFKField.customFK(nul, deflt, fkEl, fkF); + } + + /** + * Creates a new SQL foreign key field pointing to the provided field. + * @param nul true if this foreign key is nullable, false if {@code NOT NULL}. + * @param fkEl the target table. + * @param fkF the field in the targeted table. + * @return the new SQL foreign key field. + * @param the table type. + * @param the Java type of this field. + * @param the target table type. + */ + protected static , T, F extends SQLElement> SQLFKField foreignKey(boolean nul, Class fkEl, SQLField fkF) { + return SQLFKField.customFK(nul, fkEl, fkF); + } + + + + + + + + + + + + // List of type from https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-type-conversions.html + + + /** SQL type {@code BIT(1)} represented in Java as a {@link Boolean}. */ + public static final SQLType BIT_1 = new SQLType<>("BIT(1)", Boolean.class); + + /** + * SQL type {@code BIT(bitCount)} represented in Java as a {@code byte[]}. + * @param bitCount the number of bits. At least 2, or use {@link #BIT_1} if 1 bit is needed. + * @return the SQL type {@code BIT} with the specified bitCount. + */ + public static SQLType BIT(int bitCount) { + if (bitCount <= 1) + throw new IllegalArgumentException("charCount must be greater than 1. If 1 is desired, use BIT_1 instead."); + return new SQLType<>("BIT(" + bitCount + ")", byte[].class); + } + + /** SQL type {@code BOOLEAN} represented in Java as a {@link Boolean}. */ + public static final SQLType BOOLEAN = new SQLType<>("BOOLEAN", Boolean.class); + + /** SQL type {@code TINYINT} represented in Java as an {@link Integer}. */ + public static final SQLType TINYINT = new SQLType<>("TINYINT", Integer.class); // can’t be Byte due to MYSQL JDBC Connector limitations + + /** + * Alias for the SQL type {@code TINYINT} represented in Java as an {@link Integer}. + * @deprecated use {@link #TINYINT} instead. + */ + @Deprecated + public static final SQLType BYTE = TINYINT; + + /** SQL type {@code SMALLINT} represented in Java as an {@link Integer}. */ + public static final SQLType SMALLINT = new SQLType<>("SMALLINT", Integer.class); // can’t be Short due to MYSQL JDBC Connector limitations + + /** + * Alias for the SQL type {@code SMALLINT} represented in Java as an {@link Integer}. + * @deprecated use {@link #SMALLINT} instead. + */ + @Deprecated + public static final SQLType SHORT = SMALLINT; + + /** SQL type {@code MEDIUMINT} represented in Java as an {@link Integer}. */ + public static final SQLType MEDIUMINT = new SQLType<>("MEDIUMINT", Integer.class); + + /** SQL type {@code INT} represented in Java as an {@link Integer}. */ + public static final SQLType INT = new SQLType<>("INT", Integer.class); + + /** + * Alias for the SQL type {@code INT} represented in Java as an {@link Integer}. + * @deprecated use {@link #INT} instead. + */ + @Deprecated + public static final SQLType INTEGER = INT; + + /** SQL type {@code BIGINT} represented in Java as a {@link Long}. */ + public static final SQLType BIGINT = new SQLType<>("BIGINT", Long.class); + + /** + * Alias for the SQL type {@code BIGINT} represented in Java as a {@link Long}. + * @deprecated use {@link #BIGINT} instead. + */ + @Deprecated + public static final SQLType LONG = BIGINT; + + /** SQL type {@code FLOAT} represented in Java as a {@link Float}. */ + public static final SQLType FLOAT = new SQLType<>("FLOAT", Float.class); + + /** SQL type {@code DOUBLE} represented in Java as a {@link Double}. */ + public static final SQLType DOUBLE = new SQLType<>("DOUBLE", Double.class); + + /** SQL type {@code DECIMAL} represented in Java as a {@link BigDecimal}. */ + public static final SQLType DECIMAL = new SQLType<>("DECIMAL", BigDecimal.class); + + /** SQL type {@code DATE} represented in Java as a {@link Date}. */ + public static final SQLType DATE = new SQLType<>("DATE", Date.class); + + /** SQL type {@code DATETIME} represented in Java as a {@link LocalDateTime}. */ + public static final SQLType DATETIME = new SQLType<>("DATETIME", LocalDateTime.class); + + /** SQL type {@code TIMESTAMP} represented in Java as a {@link Timestamp}. */ + public static final SQLType TIMESTAMP = new SQLType<>("TIMESTAMP", Timestamp.class); + + /** SQL type {@code TIME} represented in Java as a {@link Time}. */ + public static final SQLType