* From Manfred's last patch for bug 468:

- Factor out repetitive SQL query building tasks into DbMapping.appendCondition()
  - Implement automatic extended prototype filter for collections
  - add prototype ids, but with simplified implementation (be agnostic
    about numeric ids vs. prototype names)
* Rewrite relational node insertion code
* Make better use of DbColumn class wherever possible
* Minor code improvements throughout the place
This commit is contained in:
hns 2006-08-08 15:37:09 +00:00
parent 37f26241c4
commit 56f83cb75b
4 changed files with 297 additions and 275 deletions

View file

@ -16,10 +16,11 @@
package helma.objectmodel.db;
import java.sql.Types;
/**
* A class that encapsulates the Column name and data type of a
* column in a relational table.
* A class that encapsulates the Column name and data type of a column in a
* relational table.
*/
public final class DbColumn {
private final String name;
@ -30,8 +31,6 @@ public final class DbColumn {
private final boolean isPrototype;
private final boolean isName;
private final boolean isMapped;
/**
* Constructor
*/
@ -47,8 +46,6 @@ public final class DbColumn {
isId = name.equalsIgnoreCase(dbmap.getIDField());
isPrototype = name.equalsIgnoreCase(dbmap.getPrototypeField());
isName = name.equalsIgnoreCase(dbmap.getNameField());
isMapped = relation != null || isId || isPrototype || isName;
}
/**
@ -97,7 +94,33 @@ public final class DbColumn {
* Returns true if this field is mapped by the prototype's db mapping.
*/
public boolean isMapped() {
return isMapped;
// Note: not sure if check for primitive or reference relation is really
// needed, but we did it before, so we leave it in for safety.
return isId || isPrototype || isName ||
(relation != null && (relation.isPrimitive() || relation.isReference()));
}
/**
* Checks whether values for this column need to be quoted in insert/update
* stmts
*
* @return true if values need to be wrapped in quotes
*/
public boolean needsQuotes() {
switch (type) {
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
return true;
default:
return false;
}
}
}

View file

@ -89,8 +89,13 @@ public final class DbMapping {
// db field used to identify name of prototype to use for object instantiation
private String protoField;
// name of parent prototype, if any
private String extendsProto;
// Used to map prototype ids to prototype names for
// prototypes which extend the prototype represented by
// this DbMapping.
private ResourceProperties extensionMap;
// a numeric or literal id used to represent this type in db
private String extensionId;
// dbmapping of parent prototype, if any
private DbMapping parentMapping;
@ -221,7 +226,7 @@ public final class DbMapping {
lastTypeChange = props.lastModified();
// see if this prototype extends (inherits from) any other prototype
extendsProto = props.getProperty("_extends");
String extendsProto = props.getProperty("_extends");
if (extendsProto != null) {
parentMapping = app.getDbMapping(extendsProto);
@ -245,6 +250,10 @@ public final class DbMapping {
parentMapping = null;
}
// check if there is an extension-id specified inside the type.properties
extensionId = props.getProperty("_extensionId", typename);
registerExtension(extensionId, typename);
// set the parent prototype in the corresponding Prototype object!
// this was previously done by TypeManager, but we need to do it
// ourself because DbMapping.update() may be called by other code than
@ -361,6 +370,57 @@ public final class DbMapping {
}
}
/**
* Add the given extensionId and the coresponding prototypename
* to extensionMap for later lookup.
* @param extID the id mapping to the prototypename recogniced by helma
* @param extName the name of the extending prototype
*/
private void registerExtension(String extID, String extName) {
// lazy initialization of extensionMap
if (extensionMap == null) {
extensionMap = new ResourceProperties();
extensionMap.setIgnoreCase(true);
} else if (extensionMap.containsValue(extName)) {
// remove any preexisting mapping for the given childmapping
extensionMap.values().remove(extName);
}
extensionMap.setProperty(extID, extName);
if (inheritsStorage()) {
parentMapping.registerExtension(extID, extName);
}
}
/**
* Returns the Set of Prototypes extending this prototype
* @return the Set of Prototypes extending this prototype
*/
public String[] getExtensions() {
return extensionMap == null
? new String[] { extensionId }
: (String[]) extensionMap.keySet().toArray(new String[0]);
}
/**
* Looks up the prototype-name identified by the given integer value
* @param id the id specified for the prototype
* @return the name of the extending prototype
*/
public String getPrototypeName(String id) {
if (inheritsStorage()) {
return parentMapping.getPrototypeName(id);
}
// fallback to base-prototype if the proto isn't recogniced
return extensionMap.getProperty(id, typename);
}
/**
* get the id-value of this extension
*/
public String getExtensionId() {
return extensionId;
}
/**
* Method in interface Updatable.
*/
@ -459,7 +519,7 @@ public final class DbMapping {
* Get the name of this type's parent type, if any.
*/
public String getExtends() {
return extendsProto;
return parentMapping == null ? null : parentMapping.getTypeName();
}
/**
@ -937,27 +997,20 @@ public final class DbMapping {
*/
public DbColumn getColumn(String columnName)
throws ClassNotFoundException, SQLException {
DbColumn col = (DbColumn) columnMap.get(columnName);
if (col == null) {
DbColumn[] cols = columns;
if (cols == null) {
cols = getColumns();
}
for (int i = 0; i < cols.length; i++) {
if (columnName.equalsIgnoreCase(cols[i].getName())) {
col = cols[i];
break;
}
}
columnMap.put(columnName, col);
}
return col;
}
@ -1057,24 +1110,22 @@ public final class DbMapping {
}
StringBuffer b1 = new StringBuffer("INSERT INTO ");
StringBuffer b2 = new StringBuffer(" ) VALUES ( ");
b1.append(getTableName());
b1.append(" ( ");
b1.append(getIDField());
StringBuffer b2 = new StringBuffer(" ) VALUES ( ?");
DbColumn[] cols = getColumns();
boolean needsComma = false;
for (int i = 0; i < cols.length; i++) {
Relation rel = cols[i].getRelation();
String name = cols[i].getName();
if (((rel != null) && (rel.isPrimitive() ||
rel.isReference())) ||
name.equalsIgnoreCase(getNameField()) ||
name.equalsIgnoreCase(getPrototypeField())) {
b1.append(", ").append(cols[i].getName());
b2.append(", ?");
if (cols[i].isMapped()) {
if (needsComma) {
b1.append(", ");
b2.append(", ");
}
b1.append(cols[i].getName());
b2.append("?");
needsComma = true;
}
}
@ -1115,36 +1166,16 @@ public final class DbMapping {
* Return true if values for the column identified by the parameter need
* to be quoted in SQL queries.
*/
public boolean needsQuotes(String columnName) throws SQLException {
public boolean needsQuotes(String columnName) throws SQLException, ClassNotFoundException {
if ((tableName == null) && (parentMapping != null)) {
return parentMapping.needsQuotes(columnName);
}
try {
DbColumn col = getColumn(columnName);
// This is not a mapped column. In case of doubt, add quotes.
if (col == null) {
return true;
}
switch (col.getType()) {
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
return true;
default:
return false;
}
} catch (Exception x) {
throw new SQLException(x.getMessage());
} else {
return col.needsQuotes();
}
}
@ -1367,10 +1398,7 @@ public final class DbMapping {
// note: tableName and dbSourceName are nulled out in update() if they
// are inherited from the parent mapping. This way we know that
// storage is not inherited if either of them is not null.
if (parentMapping == null || tableName != null || dbSourceName != null) {
return false;
}
return true;
return parentMapping != null && tableName == null && dbSourceName == null;
}
/**
@ -1451,4 +1479,90 @@ public final class DbMapping {
protected void addDependency(DbMapping dbmap) {
this.dependentMappings.add(dbmap);
}
/**
* Append a sql-condition for the given column which must have
* one of the values contained inside the given Set to the given
* StringBuffer.
* @param q the StringBuffer to append to
* @param column the column which must match one of the values
* @param values the list of values
* @throws SQLException
*/
protected void appendCondition(StringBuffer q, String column, String[] values)
throws SQLException, ClassNotFoundException {
if (values.length == 1) {
appendCondition(q, column, values[0]);
return;
}
if (column.indexOf('(') == -1 && column.indexOf('.') == -1) {
q.append(getTableName()).append(".");
}
q.append(column).append(" in (");
if (needsQuotes(column)) {
for (int i = 0; i < values.length; i++) {
if (i > 0)
q.append(", ");
q.append("'").append(escape(values[i])).append("'");
}
} else {
for (int i = 0; i < values.length; i++) {
if (i > 0)
q.append(", ");
q.append(values[i]);
}
}
q.append(")");
}
/**
* Append a sql-condition for the given column which must have
* the value given to the given StringBuffer.
* @param q the StringBuffer to append to
* @param column the column which must match one of the values
* @param val the value
* @throws SQLException
*/
protected void appendCondition(StringBuffer q, String column, String val)
throws SQLException, ClassNotFoundException {
if (column.indexOf('(') == -1 && column.indexOf('.') == -1) {
q.append(getTableName()).append(".");
}
q.append(column).append(" = ");
if (needsQuotes(column)) {
q.append("'").append(escape(val)).append("'");
} else {
q.append(val);
}
}
/**
* a utility method to escape single quotes used for inserting
* string-values into relational databases.
* Searches for "'" characters and escapes them by duplicating them (= "''")
* @param str the string to escape
* @return the escaped string
*/
static String escape(String str) {
if (str == null) {
return null;
} else if (str.indexOf("'") < 0) {
return str;
}
int l = str.length();
StringBuffer sbuf = new StringBuffer(l + 10);
for (int i = 0; i < l; i++) {
char c = str.charAt(i);
if (c == '\'') {
sbuf.append('\'');
}
sbuf.append(c);
}
return sbuf.toString();
}
}

View file

@ -475,50 +475,36 @@ public final class NodeManager {
String insertString = dbm.getInsert();
PreparedStatement stmt = con.prepareStatement(insertString);
// app.logEvent ("inserting relational node: "+node.getID ());
// app.logEvent ("inserting relational node: " + node.getID ());
DbColumn[] columns = dbm.getColumns();
String nameField = dbm.getNameField();
String prototypeField = dbm.getPrototypeField();
long logTimeStart = logSql ? System.currentTimeMillis() : 0;
try {
int stmtNumber = 1;
// first column of insert statement is always the primary key
stmt.setString(stmtNumber, node.getID());
Hashtable propMap = node.getPropMap();
for (int i = 0; i < columns.length; i++) {
Relation rel = columns[i].getRelation();
Property p = null;
if (rel != null && propMap != null && (rel.isPrimitive() || rel.isReference())) {
p = (Property) propMap.get(rel.getPropName());
}
String name = columns[i].getName();
if (!((rel != null) && (rel.isPrimitive() || rel.isReference())) &&
!name.equalsIgnoreCase(nameField) &&
!name.equalsIgnoreCase(prototypeField)) {
DbColumn col = columns[i];
if (!col.isMapped())
continue;
}
stmtNumber++;
if (p!=null) {
this.setStatementValues (stmt, stmtNumber, p, columns[i].getType());
} else if (name.equalsIgnoreCase(nameField)) {
stmt.setString(stmtNumber, node.getName());
} else if (name.equalsIgnoreCase(prototypeField)) {
stmt.setString(stmtNumber, node.getPrototype());
if (col.isIdField()) {
setStatementValue(stmt, stmtNumber, node.getID(), col);
} else if (col.isPrototypeField()) {
setStatementValue(stmt, stmtNumber, dbm.getExtensionId(), col);
} else {
stmt.setNull(stmtNumber, columns[i].getType());
}
}
Relation rel = col.getRelation();
Property p = rel == null ? null : node.getProperty(rel.getPropName());
if (p != null) {
setStatementValue(stmt, stmtNumber, p, col.getType());
} else if (col.isNameField()) {
stmt.setString(stmtNumber, node.getName());
} else {
stmt.setNull(stmtNumber, col.getType());
}
}
stmtNumber += 1;
}
stmt.executeUpdate();
} finally {
@ -533,7 +519,6 @@ public final class NodeManager {
} catch (Exception ignore) {}
}
}
}
/**
@ -607,16 +592,7 @@ public final class NodeManager {
}
b.append(" WHERE ");
b.append(dbm.getIDField());
b.append(" = ");
if (dbm.needsQuotes(dbm.getIDField())) {
b.append("'");
b.append(escape(node.getID()));
b.append("'");
} else {
b.append(node.getID());
}
dbm.appendCondition(b, dbm.getIDField(), node.getID());
Connection con = dbm.getConnection();
// set connection to write mode
@ -637,7 +613,7 @@ public final class NodeManager {
Relation rel = dbm.propertyToRelation(p.getName());
stmtNumber++;
this.setStatementValues (stmt, stmtNumber, p, rel.getColumnType());
setStatementValue(stmt, stmtNumber, p, rel.getColumnType());
p.dirty = false;
@ -1233,10 +1209,8 @@ public final class NodeManager {
throws Exception {
DbMapping dbm = rel.otherType;
if ((dbm == null) || !dbm.isRelational()) {
// this does nothing for objects in the embedded database
return;
} else {
if (dbm != null && dbm.isRelational()) {
int missing = cache.containsKeys(keys);
if (missing > 0) {
@ -1251,39 +1225,17 @@ public final class NodeManager {
long logTimeStart = logSql ? System.currentTimeMillis() : 0;
try {
StringBuffer b = dbm.getSelect(null);
StringBuffer b = dbm.getSelect(null).append(" WHERE ");
String idfield = (rel.groupby != null) ? rel.groupby : dbm.getIDField();
boolean needsQuotes = dbm.needsQuotes(idfield);
b.append(" WHERE ");
b.append(dbm.getTableName());
b.append(".");
b.append(idfield);
b.append(" IN (");
boolean first = true;
for (int i = 0; i < keys.length; i++) {
if (keys[i] != null) {
if (!first) {
b.append(',');
} else {
first = false;
String[] ids = new String[missing];
int j = 0;
for (int k = 0; k < keys.length; k++) {
if (keys[k] != null)
ids[j++] = keys[k].getID();
}
if (needsQuotes) {
b.append("'");
b.append(escape(keys[i].getID()));
b.append("'");
} else {
b.append(keys[i].getID());
}
}
}
b.append(") ");
dbm.appendCondition(b, idfield, ids);
dbm.addJoinConstraints(b, " AND ");
if (rel.groupby != null) {
@ -1388,7 +1340,7 @@ public final class NodeManager {
}
}
} catch (Exception x) {
System.err.println ("Error in prefetchNodes(): "+x);
app.logError("Error in prefetchNodes()", x);
} finally {
if (logSql) {
long logTimeStop = System.currentTimeMillis();
@ -1591,22 +1543,10 @@ public final class NodeManager {
DbColumn[] columns = dbm.getColumns();
Relation[] joins = dbm.getJoins();
StringBuffer b = dbm.getSelect(null).append("WHERE ")
.append(dbm.getTableName())
.append(".")
.append(idfield)
.append(" = ");
if (dbm.needsQuotes(idfield)) {
b.append("'");
b.append(escape(kstr));
b.append("'");
} else {
b.append(kstr);
}
StringBuffer b = dbm.getSelect(null).append("WHERE ");
dbm.appendCondition(b, idfield, kstr);
dbm.addJoinConstraints(b, " AND ");
query = b.toString();
ResultSet rs = stmt.executeQuery(query);
@ -1689,14 +1629,7 @@ public final class NodeManager {
if (home.getSubnodeRelation() != null && !rel.isComplexReference()) {
// combine our key with the constraints in the manually set subnode relation
b.append(" WHERE ");
if (rel.accessName.indexOf('(') == -1 && rel.accessName.indexOf('.') == -1) {
b.append(dbm.getTableName());
b.append(".");
}
b.append(rel.accessName);
b.append(" = '");
b.append(escape(kstr));
b.append("'");
dbm.appendCondition(b, rel.accessName, kstr);
// add join contraints in case this is an old oracle style join
dbm.addJoinConstraints(b, " AND ");
// add potential constraints from manually set subnodeRelation
@ -1778,7 +1711,8 @@ public final class NodeManager {
// set prototype?
if (columns[i].isPrototypeField()) {
protoName = rs.getString(i+1+offset);
String protoId = rs.getString(i + 1 + offset);
protoName = dbm.getPrototypeName(protoId);
if (protoName != null) {
dbmap = getDbMapping(protoName);
@ -1795,7 +1729,7 @@ public final class NodeManager {
// set id?
if (columns[i].isIdField()) {
id = rs.getString(i+1+offset);
id = rs.getString(i + 1 + offset);
// if id == null, the object doesn't actually exist - return null
if (id == null) {
return null;
@ -1804,14 +1738,14 @@ public final class NodeManager {
// set name?
if (columns[i].isNameField()) {
name = rs.getString(i+1+offset);
name = rs.getString(i + 1 + offset);
}
Property newprop = new Property(node);
switch (columns[i].getType()) {
case Types.BIT:
newprop.setBooleanValue(rs.getBoolean(i+1+offset));
newprop.setBooleanValue(rs.getBoolean(i + 1 + offset));
break;
@ -1819,21 +1753,21 @@ public final class NodeManager {
case Types.BIGINT:
case Types.SMALLINT:
case Types.INTEGER:
newprop.setIntegerValue(rs.getLong(i+1+offset));
newprop.setIntegerValue(rs.getLong(i + 1 + offset));
break;
case Types.REAL:
case Types.FLOAT:
case Types.DOUBLE:
newprop.setFloatValue(rs.getDouble(i+1+offset));
newprop.setFloatValue(rs.getDouble(i + 1 + offset));
break;
case Types.DECIMAL:
case Types.NUMERIC:
BigDecimal num = rs.getBigDecimal(i+1+offset);
BigDecimal num = rs.getBigDecimal(i + 1 + offset);
if (num == null) {
break;
@ -1849,16 +1783,16 @@ public final class NodeManager {
case Types.VARBINARY:
case Types.BINARY:
newprop.setStringValue(rs.getString(i+1+offset));
newprop.setStringValue(rs.getString(i + 1 + offset));
break;
case Types.LONGVARBINARY:
case Types.LONGVARCHAR:
try {
newprop.setStringValue(rs.getString(i+1+offset));
newprop.setStringValue(rs.getString(i + 1 + offset));
} catch (SQLException x) {
Reader in = rs.getCharacterStream(i+1+offset);
Reader in = rs.getCharacterStream(i + 1 + offset);
char[] buffer = new char[2048];
int read = 0;
int r;
@ -1883,14 +1817,14 @@ public final class NodeManager {
case Types.CHAR:
case Types.VARCHAR:
case Types.OTHER:
newprop.setStringValue(rs.getString(i+1+offset));
newprop.setStringValue(rs.getString(i + 1 + offset));
break;
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
newprop.setDateValue(rs.getTimestamp(i+1+offset));
newprop.setDateValue(rs.getTimestamp(i + 1 + offset));
break;
@ -1912,7 +1846,7 @@ public final class NodeManager {
break;
default:
newprop.setStringValue(rs.getString(i+1+offset));
newprop.setStringValue(rs.getString(i + 1 + offset));
break;
}
@ -1996,32 +1930,6 @@ public final class NodeManager {
return app.getDbMapping(protoname);
}
// a utility method to escape single quotes
private String escape(String str) {
if (str == null) {
return null;
}
if (str.indexOf("'") < 0) {
return str;
}
int l = str.length();
StringBuffer sbuf = new StringBuffer(l + 10);
for (int i = 0; i < l; i++) {
char c = str.charAt(i);
if (c == '\'') {
sbuf.append('\'');
}
sbuf.append(c);
}
return sbuf.toString();
}
/**
* Get an array of the the keys currently held in the object cache
*/
@ -2144,7 +2052,19 @@ public final class NodeManager {
}
}
private void setStatementValues (PreparedStatement stmt, int stmtNumber, Property p, int columnType) throws SQLException {
private void setStatementValue(PreparedStatement stmt, int stmtNumber, String value, DbColumn col)
throws SQLException {
if (value == null) {
stmt.setNull(stmtNumber, col.getType());
} else if (col.needsQuotes()) {
stmt.setString(stmtNumber, value);
} else {
stmt.setLong(stmtNumber, Long.parseLong(value));
}
}
private void setStatementValue(PreparedStatement stmt, int stmtNumber, Property p, int columnType)
throws SQLException {
if (p.getValue() == null) {
stmt.setNull(stmtNumber, columnType);
} else {

View file

@ -813,30 +813,17 @@ public final class Relation {
* and a local object.
*/
public String buildQuery(INode home, INode nonvirtual, String kstr, String pre,
boolean useOrder) throws SQLException {
boolean useOrder)
throws SQLException, ClassNotFoundException {
StringBuffer q = new StringBuffer();
String prefix = pre;
if (kstr != null && !isComplexReference()) {
q.append(prefix);
String accessColumn = (accessName == null) ? otherType.getIDField() : accessName;
if (accessColumn.indexOf('(') == -1 && accessColumn.indexOf('.') == -1) {
q.append(otherType.getTableName());
q.append(".");
}
q.append(accessColumn);
q.append(" = ");
// check if column is string type and value needs to be quoted
if (otherType.needsQuotes(accessColumn)) {
q.append("'");
q.append(escape(kstr));
q.append("'");
} else {
q.append(escape(kstr));
}
String accessColumn = (accessName == null) ?
otherType.getIDField() : accessName;
otherType.appendCondition(q, accessColumn, kstr);
prefix = " AND ";
}
@ -906,7 +893,7 @@ public final class Relation {
}
// end column version
if (value != null) {
q.append(escape(value.toString()));
q.append(DbMapping.escape(value.toString()));
} else {
q.append("NULL");
}
@ -930,7 +917,7 @@ public final class Relation {
*/
public void renderConstraints(StringBuffer q, INode home,
INode nonvirtual, String prefix)
throws SQLException {
throws SQLException, ClassNotFoundException {
if (constraints.length > 1 && logicalOperator != AND) {
q.append(prefix);
@ -949,6 +936,23 @@ public final class Relation {
prefix = " AND ";
}
// also take the prototype into consideration if someone
// specifies an extension of an prototype inside the brakets of
// a type.properties's collection, only nodes having this proto
// sould appear inside the collection
if (otherType.inheritsStorage()) {
String protoField = otherType.getPrototypeField();
String[] extensions = otherType.getExtensions();
// extensions should never be null for extension- and
// extended prototypes. nevertheless we check it here
if (extensions != null) {
q.append(prefix);
otherType.appendCondition(q, protoField, extensions);
prefix = " AND ";
}
}
if (filter != null) {
appendFilter(q, nonvirtual, prefix);
}
@ -1214,32 +1218,6 @@ public final class Relation {
return map;
}
// a utility method to escape single quotes
String escape(String str) {
if (str == null) {
return null;
}
if (str.indexOf("'") < 0) {
return str;
}
int l = str.length();
StringBuffer sbuf = new StringBuffer(l + 10);
for (int i = 0; i < l; i++) {
char c = str.charAt(i);
if (c == '\'') {
sbuf.append('\'');
}
sbuf.append(c);
}
return sbuf.toString();
}
/**
*
*
@ -1279,7 +1257,7 @@ public final class Relation {
}
public void addToQuery(StringBuffer q, INode home, INode nonvirtual)
throws SQLException {
throws SQLException, ClassNotFoundException {
String local = null;
INode ref = isGroupby ? home : nonvirtual;
@ -1292,20 +1270,7 @@ public final class Relation {
local = ref.getString(homeprop);
}
if (foreignName.indexOf('(') == -1 && foreignName.indexOf('.') == -1) {
q.append(otherType.getTableName());
q.append(".");
}
q.append(foreignName);
q.append(" = ");
if (otherType.needsQuotes(foreignName)) {
q.append("'");
q.append(escape(local));
q.append("'");
} else {
q.append(escape(local));
}
otherType.appendCondition(q, foreignName, local);
}
public boolean foreignKeyIsPrimary() {