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.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; import org.javatuples.Pair; import fr.pandacube.util.Log; /** * ORM = Object-Relational Mapping * * @author Marc Baloup * */ public final class ORM { private static List>> tables = new ArrayList<>(); private static Map>, String> tableNames = new HashMap<>(); 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.getConstructor().newInstance(); String tableName = instance.tableName(); tableNames.put(elemClass, tableName); if (!tableExistInDB(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(); } } public static > String getTableName(Class elemClass) throws ORMException { initTable(elemClass); return tableNames.get(elemClass); } private static boolean tableExistInDB(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, Integer... ids) throws ORMException { return getByIds(elemClass, Arrays.asList(ids)); } public static > SQLElementList getByIds(Class elemClass, Collection ids) throws ORMException { return getAll(elemClass, getSQLIdField(elemClass).in(ids), SQLOrderBy.asc(getSQLIdField(elemClass)), 1, null); } public static > E getById(Class elemClass, int id) throws ORMException { return getFirst(elemClass, getSQLIdField(elemClass).eq(id)); } public static > E getFirst(Class elemClass, SQLWhere where) throws ORMException { return getFirst(elemClass, where, null, null); } public static > E getFirst(Class elemClass, SQLOrderBy orderBy) throws ORMException { return getFirst(elemClass, null, orderBy, 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 " + getTableName(elemClass); 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 (ResultSet set = customQueryStatement(sql, params)) { while (set.next()) { E elm = getElementInstance(set, elemClass); action.accept(elm); } } } catch (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 " + getTableName(elemClass); List params = new ArrayList<>(); if (where != null) { Pair> ret = where.toSQL(); sql += " WHERE " + ret.getValue0(); params.addAll(ret.getValue1()); } sql += ";"; try (ResultSet set = customQueryStatement(sql, params)) { if (set.next()) { return set.getLong(1); } } } catch (SQLException e) { throw new ORMException(e); } throw new ORMException("Can’t retrieve element count from database (The ResultSet may be empty)"); } public static ResultSet customQueryStatement(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); } } public static > SQLUpdate update(Class elemClass, SQLWhere where) throws ORMException { return new SQLUpdate<>(elemClass, where); } /* package */ static > int update(Class elemClass, SQLWhere where, Map, Object> values) throws ORMException { return new SQLUpdate<>(elemClass, where, values).execute(); } /** * 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}. * @throws ORMException */ public static > int delete(Class elemClass, SQLWhere where) throws ORMException { initTable(elemClass); if (where == null) { return truncateTable(elemClass); } Pair> whereData = where.toSQL(); String sql = "DELETE FROM " + getTableName(elemClass) + " WHERE " + whereData.getValue0() + ";"; List params = new ArrayList<>(whereData.getValue1()); return customUpdateStatement(sql, params); } public static int customUpdateStatement(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()); return ps.executeUpdate(); } catch (SQLException e) { throw new ORMException(e); } } public static > int truncateTable(Class elemClass) throws ORMException { try (Statement stmt = connection.getNativeConnection().createStatement()) { return stmt.executeUpdate("TRUNCATE `" + getTableName(elemClass) + "`"); } 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 }