package com.moparisthebest.jdbc; import com.moparisthebest.jdbc.util.ResultSetUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; import static com.moparisthebest.jdbc.TryClose.tryClose; public abstract class UpdateableDTO implements Finishable { private boolean finished = false; private Map changes = null; private String tableName; private String whereClause; public static final String YES = ResultSetUtil.TRUE; public static final String NO = ResultSetUtil.FALSE; /** * Will always return YES or NO from this class, so they CAN be compared with object equality '==' instead of '.equals()' * @param bool * @return */ public static String booleanToString(boolean bool){ return bool ? YES : NO; } public static boolean changed(Object oldValue, Object newValue){ return oldValue != newValue && ((oldValue != null && !oldValue.equals(newValue)) || !newValue.equals(oldValue)); } public static Object[] getUpdate(final StringBuilder sb, final Map changes, final String tableName, final String whereClause){ if(changes.isEmpty()) return new Object[0]; sb.append("UPDATE ").append(tableName).append(" SET "); int count = -1; Object[] objectsToBind = new Object[changes.size()]; boolean notFirst = false; for (Map.Entry column : changes.entrySet()) { if (column.getKey() == null) continue; if (notFirst) sb.append(","); else notFirst = true; sb.append(column.getKey()).append("=?"); objectsToBind[++count] = column.getValue(); } if(whereClause != null) sb.append(" ").append(whereClause); //System.out.println("updateRow: "+sb); return objectsToBind; } public static Object[] getInsert(final StringBuilder sb, final Map changes, final String tableName){ if(changes.isEmpty()) return new Object[0]; sb.append("INSERT INTO ").append(tableName).append(" ("); int count = -1; Object[] objectsToBind = new Object[changes.size()]; boolean notFirst = false; for (Map.Entry column : changes.entrySet()) { if (column.getKey() == null) continue; if (notFirst) sb.append(","); else notFirst = true; sb.append(column.getKey()); objectsToBind[++count] = column.getValue(); } sb.append(") VALUES (?"); for(int x = 1; x < objectsToBind.length; ++x) sb.append(",?"); sb.append(")"); //System.out.println("insertRow: "+sb); return objectsToBind; } public static int updateRow(final Connection conn, final Map changes, final String tableName, final String whereClause, final Object... additionalBindObjects) throws SQLException { StringBuilder sb = new StringBuilder(); Object[] objectsToBind = getUpdate(sb, changes, tableName, whereClause); PreparedStatement ps = null; try { ps = conn.prepareStatement(sb.toString()); QueryMapper.bindStatement(ps, objectsToBind, additionalBindObjects == null ? new Object[0] : additionalBindObjects); return ps.executeUpdate(); } finally { tryClose(ps); } } public static int updateRow(final QueryMapper qm, final Map changes, final String tableName, final String whereClause, final Object... additionalBindObjects) throws SQLException { StringBuilder sb = new StringBuilder(); Object[] objectsToBind = getUpdate(sb, changes, tableName, whereClause); return qm.executeUpdate(sb.toString(), objectsToBind, additionalBindObjects == null ? new Object[0] : additionalBindObjects); } public static int insertRow(final Connection conn, final Map changes, final String tableName) throws SQLException { StringBuilder sb = new StringBuilder(); Object[] objectsToBind = getInsert(sb, changes, tableName); PreparedStatement ps = null; try { ps = conn.prepareStatement(sb.toString()); QueryMapper.bindStatement(ps, objectsToBind); return ps.executeUpdate(); } finally { tryClose(ps); } } public static int insertRow(final QueryMapper qm, final Map changes, final String tableName) throws SQLException { StringBuilder sb = new StringBuilder(); Object[] objectsToBind = getInsert(sb, changes, tableName); return qm.executeUpdate(sb.toString(), objectsToBind); } public static int deleteRow(final Connection conn, final String tableName, final String whereClause, final Object... additionalBindObjects) throws SQLException { PreparedStatement ps = null; try { ps = conn.prepareStatement(String.format("DELETE FROM %s %s", tableName, whereClause == null ? "" : whereClause)); if(additionalBindObjects != null) QueryMapper.bindStatement(ps, additionalBindObjects); return ps.executeUpdate(); } finally { tryClose(ps); } } public static int deleteRow(final QueryMapper qm, final String tableName, final String whereClause, final Object... additionalBindObjects) throws SQLException { return qm.executeUpdate(String.format("DELETE FROM %s %s", tableName, whereClause == null ? "" : whereClause), additionalBindObjects == null ? new Object[0] : additionalBindObjects); } protected UpdateableDTO(String tableName, String whereClause) { setTableName(tableName); this.whereClause = whereClause; } protected String getTableName() { return tableName; } protected void setTableName(String tableName) { if(tableName == null) throw new NullPointerException("tableName can never be null! Insert, update, or delete would never work!"); this.tableName = tableName; } protected String getWhereClause() { return whereClause; } protected void setWhereClause(String whereClause) { this.whereClause = whereClause; } protected final void change(String columnName, boolean oldValue, boolean newValue) { change(columnName, booleanToString(oldValue), booleanToString(newValue)); } /** * Right now this is NOT thread-safe, if this class is to be used concurrently, it needs synchronized * * This never saves the ORIGINAL value, so if you change a certain column to something new, then change it back * to the original, it is still counted as a change. The way to fix this would be another Map * of ORIGINAL values, but I'm not sure this is desired. * * @param columnName * @param newValue */ protected final boolean change(String columnName, Object oldValue, Object newValue) { if(finished && changed(oldValue, newValue)){ /* System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++"); System.out.println("before: "+this); System.out.printf("changing column '%s' from '%s' to '%s'\n", columnName, oldValue, newValue); new Exception("Stack trace").printStackTrace(System.out); System.out.println("----------------------------------------------"); */ if (changes == null) changes = new HashMap(); changes.put(columnName, newValue); return true; } return false; } public boolean hasChanged() { return changes != null && !changes.isEmpty(); // not null would probably suffice? } public int updateRow(Connection conn) throws SQLException { if (!hasChanged()) return 0; return updateRow(conn, changes, this.tableName, this.whereClause, this.getAdditionalBindObjects()); } public int updateRow(QueryMapper qm) throws SQLException { if (!hasChanged()) return 0; return updateRow(qm, changes, this.tableName, this.whereClause, this.getAdditionalBindObjects()); } public int insertRow(Connection conn) throws SQLException { if (!hasChanged()) return 0; return insertRow(conn, changes, this.tableName); } public int insertRow(QueryMapper qm) throws SQLException { if (!hasChanged()) return 0; return insertRow(qm, changes, this.tableName); } public int deleteRow(Connection conn) throws SQLException { return deleteRow(conn, this.tableName, this.whereClause, this.getAdditionalBindObjects()); } public int deleteRow(QueryMapper qm) throws SQLException { return deleteRow(qm, this.tableName, this.whereClause, this.getAdditionalBindObjects()); } @Override public void finish(ResultSet rs) throws SQLException { this.finished = true; } /** * You will almost always want to return some objects here for your where clause * * if you need to bind a single null, return new Object[]{null}, otherwise nothing will be binded * if you only need to bind a single non-null object, return that object here * if you need to bind multiple objects, return an Object[] or a Collection */ public abstract Object getAdditionalBindObjects(); @Override public String toString() { return "UpdateableDTO{" + "finished=" + finished + ", changes=" + changes + '}'; } }