Fix for #206 - Reuse SqlUpdate when binding a list that can vary in size

This commit is contained in:
rbygrave
2014-11-22 23:40:14 +13:00
parent 216806074c
commit 6ecb77dcc2
10 changed files with 406 additions and 233 deletions
@@ -91,6 +91,11 @@ public interface SqlUpdate {
*/
public String getSql();
/**
* Return the generated sql that has named parameters converted to positioned parameters.
*/
public String getGeneratedSql();
/**
* Return the timeout used to execute this statement.
*/
@@ -31,6 +31,12 @@ public class BindParams implements Serializable {
*/
private String preparedSql;
/**
* Bind hash and count used to detect when the bind values have changed such
* that the generated SQL (with named parameters) needs to be recalculated.
*/
private int[] bindHash;
public BindParams() {
}
@@ -51,20 +57,33 @@ public class BindParams implements Serializable {
* </p>
*/
public void buildQueryPlanHash(HashQueryPlanBuilder builder) {
int[] vals = calcQueryPlanHash();
builder.add(vals[0]).bind(vals[1]);
}
/**
* Calculate and return a query plan bind hash with total bind count.
*/
public int[] calcQueryPlanHash() {
int tempBindCount;
int bc = 0;
int hc = 31;
for (Param param : positionedParameters) {
hc = hc * 31 + param.queryBindCount();
tempBindCount = param.queryBindCount();
bc += tempBindCount;
hc = hc * 31 + tempBindCount;
}
for (Map.Entry<String, Param> entry : namedParameters.entrySet()) {
tempBindCount = entry.getValue().queryBindCount();
bc += tempBindCount;
hc = hc * 31 + entry.getKey().hashCode();
hc = hc * 31 + entry.getValue().queryBindCount();
hc = hc * 31 + tempBindCount;
}
int bindCount = positionedParameters.size() + namedParameters.size();
builder.add(hc).bind(bindCount);
return new int[]{hc, bc};
}
/**
* Return a deep copy of the BindParams.
*/
@@ -110,7 +129,7 @@ public class BindParams implements Serializable {
* parameters ordered.
*/
public boolean requiresNamedParamsPrepare() {
return !namedParameters.isEmpty() && positionedParameters.isEmpty();
return !namedParameters.isEmpty();
}
/**
@@ -150,7 +169,7 @@ public class BindParams implements Serializable {
}
private Param getParam(String name) {
Param p = (Param) namedParameters.get(name);
Param p = namedParameters.get(name);
if (p == null) {
p = new Param();
namedParameters.put(name, p);
@@ -165,7 +184,7 @@ public class BindParams implements Serializable {
positionedParameters.add(new Param());
}
}
return (Param) positionedParameters.get(position - 1);
return positionedParameters.get(position - 1);
}
/**
@@ -253,7 +272,29 @@ public class BindParams implements Serializable {
return preparedSql;
}
/**
/**
* Return true if the bind hash and count has not changed.
*/
public boolean isSameBindHash() {
if (bindHash == null) {
bindHash = calcQueryPlanHash();
return false;
}
int[] oldPlan = bindHash;
bindHash = calcQueryPlanHash();
return bindHash[0] == oldPlan[0] && bindHash[1] == oldPlan[1];
}
/**
* Create a new positioned parameters orderedList.
*/
public OrderedList createOrderedList() {
positionedParameters.clear();
return new OrderedList(positionedParameters);
}
/**
* The bind parameters in the correct binding order.
* <p>
* This is the result of converting sql with named parameters
@@ -327,8 +368,6 @@ public class BindParams implements Serializable {
private Object outValue;
private int textLocation;
/**
* Construct a Parameter.
*/
@@ -368,17 +407,8 @@ public class BindParams implements Serializable {
}
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (o == this) {
return true;
}
if (o instanceof Param) {
return hashCode() == o.hashCode();
}
return false;
}
return o != null && (o == this || (o instanceof Param) && hashCode() == o.hashCode());
}
/**
* Return true if this is an In parameter that needs to be bound before
@@ -463,21 +493,6 @@ public class BindParams implements Serializable {
this.outValue = out;
}
/**
* Return the location this parameter was found in the sql text.
*/
public int getTextLocation() {
return textLocation;
}
/**
* Set the location in the sql text this parameter was located. This is
* used to control order for named parameters.
*/
public void setTextLocation(int textLocation) {
this.textLocation = textLocation;
}
/**
* If true do not include this value in a transaction log.
*/
@@ -5,4 +5,6 @@ import com.avaje.ebean.SqlUpdate;
public interface SpiSqlUpdate extends SqlUpdate {
public BindParams getBindParams();
public void setGeneratedSql(String sql);
}
@@ -93,6 +93,11 @@ public final class DefaultSqlUpdate implements Serializable, SpiSqlUpdate {
*/
private final String sql;
/**
* The actual sql with named parameters converted.
*/
private String generatedSql;
/**
* Some descriptive text that can be put into the transaction log.
*/
@@ -170,7 +175,16 @@ public final class DefaultSqlUpdate implements Serializable, SpiSqlUpdate {
return this;
}
public String getSql() {
public String getGeneratedSql() {
return generatedSql;
}
@Override
public void setGeneratedSql(String generatedSql) {
this.generatedSql = generatedSql;
}
public String getSql() {
return sql;
}
@@ -47,7 +47,7 @@ public class Binder {
ArrayList<BindValues.Value> list = bindValues.values();
for (int i = 0; i < list.size(); i++) {
BindValues.Value bindValue = (BindValues.Value) list.get(i);
BindValues.Value bindValue = list.get(i);
if (bindValue.isComment()) {
if (bindBuf != null) {
bindBuf.append(bindValue.getName());
@@ -226,14 +226,12 @@ public class Binder {
try {
switch (dataType) {
case java.sql.Types.BOOLEAN:
Boolean bo = (Boolean) data;
b.setBoolean(bo.booleanValue());
b.setBoolean((Boolean) data);
break;
case java.sql.Types.BIT:
// Types.BIT should map to Java Boolean
Boolean bitBool = (Boolean) data;
b.setBoolean(bitBool.booleanValue());
break;
case java.sql.Types.BIT:
// Types.BIT should map to Java Boolean
b.setBoolean((Boolean) data);
break;
case java.sql.Types.VARCHAR:
b.setString((String) data);
@@ -244,32 +242,32 @@ public class Binder {
break;
case java.sql.Types.TINYINT:
b.setByte(((Byte) data).byteValue());
b.setByte((Byte) data);
break;
case java.sql.Types.SMALLINT:
b.setShort(((Short) data).shortValue());
b.setShort((Short) data);
break;
case java.sql.Types.INTEGER:
b.setInt(((Integer) data).intValue());
b.setInt((Integer) data);
break;
case java.sql.Types.BIGINT:
b.setLong(((Long) data).longValue());
b.setLong((Long) data);
break;
case java.sql.Types.REAL:
b.setFloat(((Float) data).floatValue());
b.setFloat((Float) data);
break;
case java.sql.Types.FLOAT:
// DB Float in theory maps to Java Double type
b.setDouble(((Double) data).doubleValue());
b.setDouble((Double) data);
break;
case java.sql.Types.DOUBLE:
b.setDouble(((Double) data).doubleValue());
b.setDouble((Double) data);
break;
case java.sql.Types.NUMERIC:
@@ -98,6 +98,7 @@ public class ExeUpdateSql {
// process named parameters if required
sql = BindParamsParser.parse(bindParams, sql);
updateSql.setGeneratedSql(sql);
boolean logSql = request.isLogSql();
@@ -18,205 +18,201 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;
*/
public class BindParamsParser {
public static final String ENCRYPTKEY_PREFIX = "encryptkey_";
public static final String ENCRYPTKEY_GAP = "___";
public static final String ENCRYPTKEY_PREFIX = "encryptkey_";
public static final String ENCRYPTKEY_GAP = "___";
private static final int ENCRYPTKEY_PREFIX_LEN = ENCRYPTKEY_PREFIX.length();
private static final int ENCRYPTKEY_GAP_LEN = ENCRYPTKEY_GAP.length();
private static final int ENCRYPTKEY_PREFIX_LEN = ENCRYPTKEY_PREFIX.length();
private static final int ENCRYPTKEY_GAP_LEN = ENCRYPTKEY_GAP.length();
/**
* Used to parse sql looking for named parameters.
*/
private static final String quote = "'";
/**
* Used to parse sql looking for named parameters.
*/
private static final String quote = "'";
/**
* Used to parse sql looking for named parameters.
*/
private static final String colon = ":";
/**
* Used to parse sql looking for named parameters.
*/
private static final String colon = ":";
private final BindParams params;
private final String sql;
private final BindParams params;
private final String sql;
private final BeanDescriptor<?> beanDescriptor;
private final BeanDescriptor<?> beanDescriptor;
public static String parse(BindParams params, String sql) {
return parse(params, sql, null);
}
public static String parse(BindParams params, String sql) {
return parse(params, sql, null);
}
public static String parse(BindParams params, String sql, BeanDescriptor<?> beanDescriptor) {
return new BindParamsParser(params, sql, beanDescriptor).parseSql();
public static String parse(BindParams params, String sql, BeanDescriptor<?> beanDescriptor) {
return new BindParamsParser(params, sql, beanDescriptor).parseSql();
}
public static OrderedList parseNamedParams(BindParams params, String sql) {
return new BindParamsParser(params, sql, null).parseSqlNamedParams();
}
private BindParamsParser(BindParams params, String sql, BeanDescriptor<?> beanDescriptor) {
this.params = params;
this.sql = sql;
this.beanDescriptor = beanDescriptor;
}
/**
* Used for parsing having clauses with named parameters.
* <p>
* The issue here is that BindParams contains named parameters for
* both where and having clauses. BindParams.positionedParameters is
* used for the where and the OrderedList for the having.
* </p>
*/
private OrderedList parseSqlNamedParams() {
OrderedList orderedList = new OrderedList();
parseNamedParams(orderedList);
return orderedList;
}
/**
* Parse the sql changed named parameters to positioned parameters if required.
* <p>
* The sql is used when named parameters are used.
* </p>
* <p>
* This is used in most cases of named parameters. The case it is NOT used for is
* named parameters in a having clause. In this case some of the named parameters
* could be for a where clause and some for the having clause.
* </p>
*/
private String parseSql() {
if (params.isSameBindHash()) {
String preparedSql = params.getPreparedSql();
if (preparedSql != null && preparedSql.length() > 0) {
// the sql has already been parsed and positionedParameters are set in order
return preparedSql;
}
}
public static OrderedList parseNamedParams(BindParams params, String sql) {
return new BindParamsParser(params, sql, null).parseSqlNamedParams();
String preparedSql;
if (params.requiresNamedParamsPrepare()) {
// convert named parameters into ordered list
OrderedList orderedList = params.createOrderedList();
parseNamedParams(orderedList);
preparedSql = orderedList.getPreparedSql();
} else {
preparedSql = sql;
}
private BindParamsParser(BindParams params, String sql, BeanDescriptor<?> beanDescriptor) {
this.params = params;
this.sql = sql;
this.beanDescriptor = beanDescriptor;
params.setPreparedSql(preparedSql);
return preparedSql;
}
/**
* Named parameters need to be parsed and replaced with ?.
*/
private void parseNamedParams(OrderedList orderedList) {
parseNamedParams(0, orderedList);
}
private void parseNamedParams(int startPos, OrderedList orderedList) {
if (sql == null) {
throw new PersistenceException("query does not contain any named bind parameters?");
}
/**
* Used for parsing having clauses with named parameters.
* <p>
* The issue here is that BindParams contains named parameters for
* both where and having clauses. BindParams.positionedParameters is
* used for the where and the OrderedList for the having.
* </p>
*/
private OrderedList parseSqlNamedParams() {
OrderedList orderedList = new OrderedList();
parseNamedParams(orderedList);
return orderedList;
if (startPos > sql.length()) {
return;
}
/**
* Parse the sql changed named parameters to positioned parameters if required.
* <p>
* The sql is used when named parameters are used.
* </p>
* <p>
* This is used in most cases of named parameters. The case it is NOT used for is
* named parameters in a having clause. In this case some of the named parameters
* could be for a where clause and some for the having clause.
* </p>
*/
private String parseSql() {
String preparedSql = params.getPreparedSql();
if (preparedSql != null && preparedSql.length() > 0){
// the sql has already been parsed and
// positionedParameters are set in order
return preparedSql;
}
String prepardSql;
if (params.requiresNamedParamsPrepare()) {
OrderedList orderedList = new OrderedList(params.positionedParameters());
parseNamedParams(orderedList);
prepardSql = orderedList.getPreparedSql();
// search for quotes and named params... in order...
int beginQuotePos = sql.indexOf(quote, startPos);
int nameParamStart = sql.indexOf(colon, startPos);
if (beginQuotePos > 0 && beginQuotePos < nameParamStart) {
// the quote precedes the named parameter...
// find and add up to the end quote
int endQuotePos = sql.indexOf(quote, beginQuotePos + 1);
String sub = sql.substring(startPos, endQuotePos + 1);
orderedList.appendSql(sub);
// start again after the end quote
parseNamedParams(endQuotePos + 1, orderedList);
} else {
if (nameParamStart < 0) {
// no more params, add the rest
String sub = sql.substring(startPos, sql.length());
orderedList.appendSql(sub);
} else {
// find the end of the parameter name
int endOfParam = nameParamStart + 1;
do {
char c = sql.charAt(endOfParam);
if (c != '_' && !Character.isLetterOrDigit(c)) {
break;
}
endOfParam++;
} while (endOfParam < sql.length());
// add the named parameter value to bindList
String paramName = sql.substring(nameParamStart + 1, endOfParam);
Param param;
if (paramName.startsWith(ENCRYPTKEY_PREFIX)) {
param = addEncryptKeyParam(paramName);
} else {
prepardSql = sql;
}
params.setPreparedSql(prepardSql);
return prepardSql;
}
/**
* Named parameters need to be parsed and replaced with ?.
*/
private void parseNamedParams(OrderedList orderedList) {
parseNamedParams(0, orderedList);
}
private void parseNamedParams(int startPos, OrderedList orderedList) {
if (sql == null){
throw new PersistenceException("query does not contain any named bind parameters?");
}
if (startPos > sql.length()) {
return;
param = params.getParameter(paramName);
}
// search for quotes and named params... in order...
int beginQuotePos = sql.indexOf(quote, startPos);
int nameParamStart = sql.indexOf(colon, startPos);
if (beginQuotePos > 0 && beginQuotePos < nameParamStart) {
// the quote precedes the named parameter...
// find and add up to the end quote
int endQuotePos = sql.indexOf(quote, beginQuotePos + 1);
String sub = sql.substring(startPos, endQuotePos + 1);
orderedList.appendSql(sub);
if (param == null) {
String msg = "Bind value is not set or null for [" + paramName + "] in [" + sql + "]";
throw new PersistenceException(msg);
}
// start again after the end quote
parseNamedParams(endQuotePos + 1, orderedList);
String sub = sql.substring(startPos, nameParamStart);
orderedList.appendSql(sub);
} else {
if (nameParamStart < 0) {
// no more params, add the rest
String sub = sql.substring(startPos, sql.length());
orderedList.appendSql(sub);
} else {
// find the end of the parameter name
int endOfParam = nameParamStart + 1;
do {
char c = sql.charAt(endOfParam);
if (c != '_' && !Character.isLetterOrDigit(c)) {
break;
}
endOfParam++;
} while (endOfParam < sql.length());
// add the named parameter value to bindList
String paramName = sql.substring(nameParamStart + 1, endOfParam);
Param param;
if (paramName.startsWith(ENCRYPTKEY_PREFIX)){
param = addEncryptKeyParam(paramName);
} else {
param = params.getParameter(paramName);
}
if (param == null) {
String msg = "Bind value is not set or null for [" + paramName
+ "] in [" + sql+ "]";
throw new PersistenceException(msg);
}
String sub = sql.substring(startPos, nameParamStart);
orderedList.appendSql(sub);
// check if inValue is a Collection type...
Object inValue = param.getInValue();
if (inValue != null && inValue instanceof Collection<?>){
// Chop up Collection parameter into a number
// of individual parameters and add each one individually
Collection<?> collection = (Collection<?>)inValue;
int c = 0;
for (Object elVal : collection) {
if (++c > 1){
orderedList.appendSql(",");
}
orderedList.appendSql("?");
BindParams.Param elParam = new BindParams.Param();
elParam.setInValue(elVal);
orderedList.add(elParam);
}
} else {
// its a normal scalar value parameter...
orderedList.add(param);
orderedList.appendSql("?");
}
// continue on after the end of the parameter
parseNamedParams(endOfParam, orderedList);
// check if inValue is a Collection type...
Object inValue = param.getInValue();
if (inValue != null && inValue instanceof Collection<?>) {
// Chop up Collection parameter into a number
// of individual parameters and add each one individually
Collection<?> collection = (Collection<?>) inValue;
int c = 0;
for (Object elVal : collection) {
if (++c > 1) {
orderedList.appendSql(",");
}
}
}
orderedList.appendSql("?");
BindParams.Param elParam = new BindParams.Param();
elParam.setInValue(elVal);
orderedList.add(elParam);
}
/**
* Add an encryption key bind parameter.
*/
private Param addEncryptKeyParam(String keyNamedParam) {
int pos = keyNamedParam.indexOf(ENCRYPTKEY_GAP, ENCRYPTKEY_PREFIX_LEN);
String tableName = keyNamedParam.substring(ENCRYPTKEY_PREFIX_LEN, pos);
String columnName = keyNamedParam.substring(pos+ENCRYPTKEY_GAP_LEN);
EncryptKey key = beanDescriptor.getEncryptKey(tableName, columnName);
String strKey = key.getStringValue();
return params.setEncryptionKey(keyNamedParam, strKey);
} else {
// its a normal scalar value parameter...
orderedList.add(param);
orderedList.appendSql("?");
}
// continue on after the end of the parameter
parseNamedParams(endOfParam, orderedList);
}
}
}
/**
* Add an encryption key bind parameter.
*/
private Param addEncryptKeyParam(String keyNamedParam) {
int pos = keyNamedParam.indexOf(ENCRYPTKEY_GAP, ENCRYPTKEY_PREFIX_LEN);
String tableName = keyNamedParam.substring(ENCRYPTKEY_PREFIX_LEN, pos);
String columnName = keyNamedParam.substring(pos + ENCRYPTKEY_GAP_LEN);
EncryptKey key = beanDescriptor.getEncryptKey(tableName, columnName);
String strKey = key.getStringValue();
return params.setEncryptionKey(keyNamedParam, strKey);
}
}