package fr.pandacube.util.orm; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Consumer; import org.javatuples.Pair; import fr.pandacube.util.Log; import fr.pandacube.util.orm.SQLWhereChain.SQLBoolOp; import fr.pandacube.util.orm.SQLWhereComp.SQLComparator; /** * ORM = Object-Relational Mapping * * @author Marc Baloup * */ public final class ORM { private static List>> tables = new ArrayList<>(); private static DBConnection connection; public static DBConnection getConnection() { return connection; } public synchronized static > void init(DBConnection conn) { connection = conn; } public static synchronized > void initTable(Class elemClass) throws ORMInitTableException { if (tables.contains(elemClass)) return; try { tables.add(elemClass); Log.debug("[ORM] Start Init SQL table "+elemClass.getSimpleName()); E instance = elemClass.newInstance(); String tableName = instance.tableName(); if (!tableExist(tableName)) createTable(instance); Log.debug("[ORM] End init SQL table "+elemClass.getSimpleName()); } catch (Exception|ExceptionInInitializerError e) { throw new ORMInitTableException(elemClass, e); } } private static > void createTable(E elem) throws SQLException { String sql = "CREATE TABLE IF NOT EXISTS " + elem.tableName() + " ("; List params = new ArrayList<>(); Collection> tableFields = elem.getFields().values(); boolean first = true; for (SQLField f : tableFields) { Pair> statementPart = f.forSQLPreparedStatement(); params.addAll(statementPart.getValue1()); if (!first) sql += ", "; first = false; sql += statementPart.getValue0(); } sql += ", PRIMARY KEY id(id))"; try (PreparedStatement ps = connection.getNativeConnection().prepareStatement(sql)) { int i = 1; for (Object val : params) ps.setObject(i++, val); Log.info("Creating table " + elem.tableName() + ":\n" + ps.toString()); ps.executeUpdate(); } } private static boolean tableExist(String tableName) throws SQLException { boolean exist = false; try (ResultSet set = connection.getNativeConnection().getMetaData().getTables(null, null, tableName, null)) { exist = set.next(); } return exist; } @SuppressWarnings("unchecked") public static > SQLField getSQLIdField(Class elemClass) throws ORMInitTableException { initTable(elemClass); return (SQLField) SQLElement.fieldsCache.get(elemClass).get("id"); } public static > SQLElementList getByIds(Class elemClass, Collection ids) throws ORMException { return getByIds(elemClass, ids.toArray(new Integer[ids.size()])); } public static > SQLElementList getByIds(Class elemClass, Integer... ids) throws ORMException { SQLField idField = getSQLIdField(elemClass); SQLWhereChain where = new SQLWhereChain(SQLBoolOp.OR); for (Integer id : ids) if (id != null) where.add(new SQLWhereComp(idField, SQLComparator.EQ, id)); return getAll(elemClass, where, new SQLOrderBy().add(idField), 1, null); } public static > E getById(Class elemClass, int id) throws ORMException { return getFirst(elemClass, new SQLWhereComp(getSQLIdField(elemClass), SQLComparator.EQ, id)); } public static > E getFirst(Class elemClass, SQLWhere where) throws ORMException { return getFirst(elemClass, where, null, null); } public static > E getFirst(Class elemClass, SQLWhere where, SQLOrderBy orderBy) throws ORMException { return getFirst(elemClass, where, orderBy, null); } public static > E getFirst(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer offset) throws ORMException { SQLElementList elts = getAll(elemClass, where, orderBy, 1, offset); return (elts.size() == 0) ? null : elts.get(0); } public static > SQLElementList getAll(Class elemClass) throws ORMException { return getAll(elemClass, null, null, null, null); } public static > SQLElementList getAll(Class elemClass, SQLWhere where) throws ORMException { return getAll(elemClass, where, null, null, null); } public static > SQLElementList getAll(Class elemClass, SQLWhere where, SQLOrderBy orderBy) throws ORMException { return getAll(elemClass, where, orderBy, null, null); } public static > SQLElementList getAll(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer limit) throws ORMException { return getAll(elemClass, where, orderBy, limit, null); } public static > SQLElementList getAll(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer limit, Integer offset) throws ORMException { SQLElementList elmts = new SQLElementList<>(); forEach(elemClass, where, orderBy, limit, offset, elmts::add); return elmts; } public static > void forEach(Class elemClass, Consumer action) throws ORMException { forEach(elemClass, null, null, null, null, action); } public static > void forEach(Class elemClass, SQLWhere where, Consumer action) throws ORMException { forEach(elemClass, where, null, null, null, action); } public static > void forEach(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Consumer action) throws ORMException { forEach(elemClass, where, orderBy, null, null, action); } public static > void forEach(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer limit, Consumer action) throws ORMException { forEach(elemClass, where, orderBy, limit, null, action); } public static > void forEach(Class elemClass, SQLWhere where, SQLOrderBy orderBy, Integer limit, Integer offset, Consumer action) throws ORMException { initTable(elemClass); try { String sql = "SELECT * FROM " + elemClass.newInstance().tableName(); List params = new ArrayList<>(); if (where != null) { Pair> ret = where.toSQL(); sql += " WHERE " + ret.getValue0(); params.addAll(ret.getValue1()); } if (orderBy != null) sql += " ORDER BY " + orderBy.toSQL(); if (limit != null) sql += " LIMIT " + limit; if (offset != null) sql += " OFFSET " + offset; sql += ";"; 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()); try (ResultSet set = ps.executeQuery()) { while (set.next()) { E elm = getElementInstance(set, elemClass); action.accept(elm); } } } } catch (ReflectiveOperationException | SQLException e) { throw new ORMException(e); } } public static > long count(Class elemClass) throws ORMException { return count(elemClass, null); } public static > long count(Class elemClass, SQLWhere where) throws ORMException { initTable(elemClass); try { String sql = "SELECT COUNT(*) as count FROM " + elemClass.newInstance().tableName(); List params = new ArrayList<>(); if (where != null) { Pair> ret = where.toSQL(); sql += " WHERE " + ret.getValue0(); params.addAll(ret.getValue1()); } sql += ";"; 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()); try (ResultSet set = ps.executeQuery()) { while (set.next()) { return set.getLong(1); } } } } catch (ReflectiveOperationException | SQLException e) { throw new ORMException(e); } throw new ORMException("Can’t retrieve element count from database (The ResultSet may be empty)"); } public static > boolean truncateTable(Class elemClass) throws ORMException { boolean success; try (Statement stmt = connection.getNativeConnection().createStatement()) { success = stmt.execute("TRUNCATE `" + elemClass.newInstance().tableName() + "`"); } catch(SQLException | ReflectiveOperationException e) { throw new ORMException(e); } return success; } public static ResultSet getCustomResult(String sql, List params) throws ORMException { 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 ORMException(e); } } @SuppressWarnings("unchecked") private static > E getElementInstance(ResultSet set, Class elemClass) throws ORMException { try { E instance = elemClass.getConstructor(int.class).newInstance(set.getInt("id")); 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 ORMException("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.toString()+"'", 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()); } } if (!instance.isValidForSave()) throw new ORMException( "This SQLElement representing a database entry is not valid for save : " + instance.toString()); return instance; } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | SQLException e) { throw new ORMException("Can't instanciate " + elemClass.getName(), e); } } private ORM() {} // rend la classe non instanciable /* * public static void main(String[] args) throws Throwable { * ORM.init(new DBConnection("localhost", 3306, "pandacube", "pandacube", * "pandacube")); * List players = ORM.getAll(SQLPlayer.class, * new SQLWhereChain(SQLBoolOp.AND) * .add(new SQLWhereNull(SQLPlayer.banTimeout, true)) * .add(new SQLWhereChain(SQLBoolOp.OR) * .add(new SQLWhereComp(SQLPlayer.bambou, SQLComparator.EQ, 0L)) * .add(new SQLWhereComp(SQLPlayer.grade, SQLComparator.EQ, "default")) * ), * new SQLOrderBy().addField(SQLPlayer.playerDisplayName), null, null); * for(SQLPlayer p : players) { * System.out.println(p.get(SQLPlayer.playerDisplayName)); * } * // TODO mise à jour relative d'un champ (incrément / décrément) * } */ }