mirror of https://github.com/apache/openjpa.git
OPENJPA-703: Support Collection-valued parameters. Handle re-parameterization when collection-valued parameter has different size across invocations.
git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@740016 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
2065e905dd
commit
64329e007e
|
@ -19,7 +19,9 @@
|
|||
|
||||
package org.apache.openjpa.jdbc.kernel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -35,8 +37,9 @@ import org.apache.openjpa.kernel.Query;
|
|||
import org.apache.openjpa.kernel.QueryImpl;
|
||||
import org.apache.openjpa.kernel.QueryLanguages;
|
||||
import org.apache.openjpa.lib.rop.ResultList;
|
||||
import org.apache.openjpa.lib.util.Localizer;
|
||||
import org.apache.openjpa.util.ImplHelper;
|
||||
import org.apache.openjpa.util.InternalException;
|
||||
import org.apache.openjpa.util.UserException;
|
||||
|
||||
/**
|
||||
* Implements {@link PreparedQuery} for SQL queries.
|
||||
|
@ -45,6 +48,9 @@ import org.apache.openjpa.util.InternalException;
|
|||
*
|
||||
*/
|
||||
public class PreparedQueryImpl implements PreparedQuery {
|
||||
private static Localizer _loc =
|
||||
Localizer.forPackage(PreparedQueryImpl.class);
|
||||
|
||||
private final String _id;
|
||||
private String _sql;
|
||||
|
||||
|
@ -53,11 +59,9 @@ public class PreparedQueryImpl implements PreparedQuery {
|
|||
private boolean _subclasses;
|
||||
private boolean _isProjection;
|
||||
|
||||
// Parameters of the query
|
||||
private List _params;
|
||||
// Position of the user defined parameters in the _params list
|
||||
private Map<Object, int[]> _userParamPositions;
|
||||
|
||||
private Map<Integer, Object> _template;
|
||||
|
||||
/**
|
||||
* Construct.
|
||||
|
@ -163,31 +167,43 @@ public class PreparedQueryImpl implements PreparedQuery {
|
|||
* {@link #initialize(Object) initialization}.
|
||||
*
|
||||
* @return 0-based parameter index mapped to corresponding values.
|
||||
*
|
||||
*/
|
||||
public Map<Integer, Object> reparametrize(Map user, Broker broker) {
|
||||
Map<Integer, Object> result = new HashMap<Integer, Object>();
|
||||
for (int i = 0; i < _params.size(); i++) {
|
||||
result.put(i, _params.get(i));
|
||||
if (user == null || user.isEmpty()) {
|
||||
if (!_userParamPositions.isEmpty()) {
|
||||
throw new UserException(_loc.get("uparam-null",
|
||||
_userParamPositions.keySet(), this));
|
||||
} else {
|
||||
return _template;
|
||||
}
|
||||
}
|
||||
if (user == null || user.isEmpty())
|
||||
return result;
|
||||
if (!_userParamPositions.keySet().equals(user.keySet())) {
|
||||
throw new UserException(_loc.get("uparam-mismatch",
|
||||
_userParamPositions.keySet(), user.keySet(), this));
|
||||
}
|
||||
Map<Integer, Object> result = new HashMap<Integer, Object>(_template);
|
||||
|
||||
for (Object key : user.keySet()) {
|
||||
int[] indices = _userParamPositions.get(key);
|
||||
if (indices == null)
|
||||
continue;
|
||||
Object value = user.get(key);
|
||||
if (ImplHelper.isManageable(value)) {
|
||||
setPersistenceCapableParameter(result, value, indices, broker);
|
||||
if (indices == null || indices.length == 0)
|
||||
throw new UserException(_loc.get("uparam-no-pos", key, this));
|
||||
Object val = user.get(key);
|
||||
if (ImplHelper.isManageable(val)) {
|
||||
setPersistenceCapableParameter(result, val, indices, broker);
|
||||
} else if (val instanceof Collection) {
|
||||
setCollectionValuedParameter(result, (Collection)val, indices,
|
||||
key);
|
||||
} else {
|
||||
for (int j : indices)
|
||||
result.put(j, value);
|
||||
result.put(j, val);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate primary key identity value(s) of the given managable instance
|
||||
* Calculate primary key identity value(s) of the given manageable instance
|
||||
* and fill in the given map.
|
||||
*
|
||||
* @param values a map of integer parameter index to parameter value
|
||||
|
@ -209,7 +225,8 @@ public class PreparedQueryImpl implements PreparedQuery {
|
|||
Object[] array = (Object[])cols;
|
||||
int n = array.length;
|
||||
if (n > indices.length || indices.length%n != 0)
|
||||
throw new InternalException();
|
||||
throw new UserException(_loc.get("uparam-pc-key",
|
||||
pc.getClass(), n, Arrays.toString(indices)));
|
||||
int k = 0;
|
||||
for (int j : indices) {
|
||||
result.put(j, array[k%n]);
|
||||
|
@ -222,8 +239,23 @@ public class PreparedQueryImpl implements PreparedQuery {
|
|||
}
|
||||
}
|
||||
|
||||
private void setCollectionValuedParameter(Map<Integer,Object> result,
|
||||
Collection values, int[] indices, Object param) {
|
||||
int n = values.size();
|
||||
Object[] array = values.toArray();
|
||||
if (n > indices.length || indices.length%n != 0) {
|
||||
throw new UserException(_loc.get("uparam-coll-size", param, values,
|
||||
Arrays.toString(indices)));
|
||||
}
|
||||
int k = 0;
|
||||
for (int j : indices) {
|
||||
result.put(j, array[k%n]);
|
||||
k++;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Marks the positions of user parameters.
|
||||
* Marks the positions and keys of user parameters.
|
||||
*
|
||||
* @param list even elements are numbers representing the position of a
|
||||
* user parameter in the _param list. Odd elements are the user parameter
|
||||
|
@ -248,8 +280,11 @@ public class PreparedQueryImpl implements PreparedQuery {
|
|||
}
|
||||
|
||||
void setParameters(List list) {
|
||||
_params = new ArrayList();
|
||||
_params.addAll(list);
|
||||
Map<Integer, Object> tmp = new HashMap<Integer, Object>();
|
||||
for (int i = 0; list != null && i < list.size(); i++) {
|
||||
tmp.put(i, list.get(i));
|
||||
}
|
||||
_template = Collections.unmodifiableMap(tmp);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.apache.openjpa.jdbc.sql.Joins;
|
|||
import org.apache.openjpa.jdbc.sql.SQLBuffer;
|
||||
import org.apache.openjpa.jdbc.sql.Select;
|
||||
import org.apache.openjpa.kernel.exps.ExpressionVisitor;
|
||||
import org.apache.openjpa.kernel.exps.Parameter;
|
||||
|
||||
/**
|
||||
* Tests whether a value is IN a collection.
|
||||
|
@ -154,7 +155,8 @@ class InExpression
|
|||
|
||||
Column col = (cols != null && cols.length == 1) ? cols[0] : null;
|
||||
for (Iterator itr = coll.iterator(); itr.hasNext();) {
|
||||
buf.appendValue(itr.next(), col);
|
||||
buf.appendValue(itr.next(), col, _const instanceof Parameter
|
||||
? (Parameter)_const : null);
|
||||
if (itr.hasNext())
|
||||
buf.append(", ");
|
||||
}
|
||||
|
|
|
@ -124,4 +124,13 @@ prepared-query-add-pattern: Adding a Query exclusion pattern "{0}" has caused \
|
|||
following {1} cached queries to be removed from the cache: "{2}".
|
||||
prepared-query-remove-pattern: Removing a Query exclusion pattern "{0}" caused \
|
||||
following {1} queries to be re-inserted in the cache: "{2}".
|
||||
|
||||
uparam-mismatch: Supplied user parameters "{1}" do not match expected \
|
||||
parameters "{0}" for the prepared query "{2}".
|
||||
uparam-null: No user parameter was given. Expected parameters "{0}" for the \
|
||||
prepared query "{1}".
|
||||
uparam-coll-size: Parameter "{0}" has a value "{1}" which is not compatible \
|
||||
with the available positions {2} in the parameter list of the prepared query
|
||||
uparam-no-pos: User parameter "{0}" does not appear in any position in the \
|
||||
prepared query "{1}".
|
||||
uparam-pc-key: Class "{0}" uses {1} primary key columns but corresponding \
|
||||
positions {2} in the parameter list of the prepared query is not compatible.
|
|
@ -821,13 +821,15 @@ public class JPQLExpressionBuilder
|
|||
return eval(firstChild(node));
|
||||
|
||||
case JJTNAMEDINPUTPARAMETER:
|
||||
return getParameter(node.text, false);
|
||||
return getParameter(node.text, false, false);
|
||||
|
||||
case JJTPOSITIONALINPUTPARAMETER:
|
||||
return getParameter(node.text, true);
|
||||
return getParameter(node.text, true, false);
|
||||
|
||||
case JJTCOLLECTIONPARAMETER:
|
||||
return getCollectionValuedParameter(node);
|
||||
JPQLNode child = onlyChild(node);
|
||||
return getParameter(child.text,
|
||||
child.id == JJTPOSITIONALINPUTPARAMETER, true);
|
||||
|
||||
case JJTOR: // x OR y
|
||||
return factory.or(getExpression(left(node)),
|
||||
|
@ -1288,58 +1290,23 @@ public class JPQLExpressionBuilder
|
|||
}
|
||||
|
||||
/**
|
||||
* Record the names and order of implicit parameters.
|
||||
* Creates and records the names and order of parameters. The parameters are
|
||||
* identified by a key with its type preserved. The second argument
|
||||
* determines whether the first argument is used as-is or converted to
|
||||
* an Integer as parameter key.
|
||||
*
|
||||
* @param the text as it appears in the parsed node
|
||||
* @param positional if true the first argument is converted to an integer
|
||||
* @param isCollectionValued true for collection-valued parameters
|
||||
*/
|
||||
private Parameter getParameter(String id, boolean positional) {
|
||||
private Parameter getParameter(String id, boolean positional,
|
||||
boolean isCollectionValued) {
|
||||
if (parameterTypes == null)
|
||||
parameterTypes = new LinkedMap(6);
|
||||
Object paramKey = positional ? Integer.parseInt(id) : id;
|
||||
if (!parameterTypes.containsKey(paramKey))
|
||||
parameterTypes.put(paramKey, TYPE_OBJECT);
|
||||
|
||||
Class type = Object.class;
|
||||
ClassMetaData meta = null;
|
||||
int index;
|
||||
|
||||
if (positional) {
|
||||
try {
|
||||
// indexes in JPQL are 1-based, as opposed to 0-based in
|
||||
// the core ExpressionFactory
|
||||
index = Integer.parseInt(id) - 1;
|
||||
} catch (NumberFormatException e) {
|
||||
throw parseException(EX_USER, "bad-positional-parameter",
|
||||
new Object[]{ id }, e);
|
||||
}
|
||||
|
||||
if (index < 0)
|
||||
throw parseException(EX_USER, "bad-positional-parameter",
|
||||
new Object[]{ id }, null);
|
||||
} else {
|
||||
// otherwise the index is just the current size of the params
|
||||
index = parameterTypes.indexOf(id);
|
||||
}
|
||||
Parameter param = factory.newParameter(paramKey, type);
|
||||
param.setMetaData(meta);
|
||||
param.setIndex(index);
|
||||
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the names and order of collection valued input parameters.
|
||||
*/
|
||||
private Parameter getCollectionValuedParameter(JPQLNode node) {
|
||||
JPQLNode child = onlyChild(node);
|
||||
String id = child.text;
|
||||
boolean positional = child.id == JJTPOSITIONALINPUTPARAMETER;
|
||||
|
||||
if (parameterTypes == null)
|
||||
parameterTypes = new LinkedMap(6);
|
||||
Object paramKey = positional ? Integer.parseInt(id) : id;
|
||||
if (!parameterTypes.containsKey(id))
|
||||
parameterTypes.put(paramKey, TYPE_OBJECT);
|
||||
|
||||
Class type = Object.class;
|
||||
ClassMetaData meta = null;
|
||||
int index;
|
||||
if (positional) {
|
||||
|
@ -1359,11 +1326,12 @@ public class JPQLExpressionBuilder
|
|||
// otherwise the index is just the current size of the params
|
||||
index = parameterTypes.indexOf(id);
|
||||
}
|
||||
|
||||
Parameter param = factory.newCollectionValuedParameter(id, type);
|
||||
Parameter param = isCollectionValued
|
||||
? factory.newCollectionValuedParameter(paramKey, TYPE_OBJECT)
|
||||
: factory.newParameter(paramKey, TYPE_OBJECT);
|
||||
param.setMetaData(meta);
|
||||
param.setIndex(index);
|
||||
|
||||
|
||||
return param;
|
||||
}
|
||||
|
||||
|
@ -1502,10 +1470,10 @@ public class JPQLExpressionBuilder
|
|||
return factory.type(getValue(node));
|
||||
|
||||
case JJTNAMEDINPUTPARAMETER:
|
||||
return factory.type(getParameter(node.text, false));
|
||||
return factory.type(getParameter(node.text, false, false));
|
||||
|
||||
case JJTPOSITIONALINPUTPARAMETER:
|
||||
return factory.type(getParameter(node.text, true));
|
||||
return factory.type(getParameter(node.text, true, false));
|
||||
|
||||
default:
|
||||
// TODO: enforce jpa2.0 spec rules.
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.apache.openjpa.persistence.jdbc.sqlcache;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -424,6 +425,25 @@ public class TestPreparedQueryCache extends SQLListenerTestCase {
|
|||
compare(!IS_NAMED_QUERY, jpql, COMPANY_NAMES.length*DEPARTMENT_NAMES.length, params);
|
||||
}
|
||||
|
||||
public void testCollectionValuedParameters() {
|
||||
String jpql = "select e from Employee e where e.name in :names";
|
||||
Object[] params1 = {"names", Arrays.asList(new String[]{EMPLOYEE_NAMES[0], EMPLOYEE_NAMES[1]})};
|
||||
Object[] params2 = {"names", Arrays.asList(new String[]{EMPLOYEE_NAMES[2]})};
|
||||
Object[] params3 = {"names", Arrays.asList(EMPLOYEE_NAMES)};
|
||||
|
||||
boolean checkHits = false;
|
||||
|
||||
int expectedCount = 2*COMPANY_NAMES.length*DEPARTMENT_NAMES.length;
|
||||
run(jpql, params1, USE_CACHE, 2, !IS_NAMED_QUERY, expectedCount, checkHits);
|
||||
assertCached(jpql);
|
||||
|
||||
expectedCount = 1*COMPANY_NAMES.length*DEPARTMENT_NAMES.length;
|
||||
run(jpql, params2, USE_CACHE, 2, !IS_NAMED_QUERY, expectedCount, checkHits);
|
||||
|
||||
expectedCount = EMPLOYEE_NAMES.length*COMPANY_NAMES.length*DEPARTMENT_NAMES.length;
|
||||
run(jpql, params3, USE_CACHE, 2, !IS_NAMED_QUERY, expectedCount, checkHits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the result of execution of the same query with and without
|
||||
* Prepared Query Cache.
|
||||
|
@ -462,6 +482,10 @@ public class TestPreparedQueryCache extends SQLListenerTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
long run(String jpql, Object[] params, boolean useCache, int N,
|
||||
boolean isNamedQuery, int expectedCount) {
|
||||
return run(jpql, params, useCache, N, isNamedQuery, expectedCount, true);
|
||||
}
|
||||
/**
|
||||
* Create and run a query N times with the given parameters. The time for
|
||||
* each query execution is measured in nanosecond precision and
|
||||
|
@ -470,7 +494,7 @@ public class TestPreparedQueryCache extends SQLListenerTestCase {
|
|||
* returns median time taken for single execution.
|
||||
*/
|
||||
long run(String jpql, Object[] params, boolean useCache, int N,
|
||||
boolean isNamedQuery, int expectedCount) {
|
||||
boolean isNamedQuery, int expectedCount, boolean checkHits) {
|
||||
trace("Executing " + N + " times " + (useCache ? " with " : "without") + " cache");
|
||||
List<Long> stats = new ArrayList<Long>();
|
||||
sql.clear();
|
||||
|
@ -500,7 +524,7 @@ public class TestPreparedQueryCache extends SQLListenerTestCase {
|
|||
stats.add(end - start);
|
||||
em.close();
|
||||
}
|
||||
if (useCache) {
|
||||
if (useCache && checkHits) {
|
||||
String cacheKey = isNamedQuery ? getJPQL(jpql) : jpql;
|
||||
long total = getCache().getStatistics().getExecutionCount(cacheKey);
|
||||
long hits = getCache().getStatistics().getHitCount(cacheKey);
|
||||
|
|
|
@ -40,6 +40,7 @@ import javax.persistence.NonUniqueResultException;
|
|||
import javax.persistence.Query;
|
||||
import javax.persistence.TemporalType;
|
||||
|
||||
import org.apache.openjpa.conf.OpenJPAConfiguration;
|
||||
import org.apache.openjpa.enhance.Reflection;
|
||||
import org.apache.openjpa.kernel.Broker;
|
||||
import org.apache.openjpa.kernel.DelegatingQuery;
|
||||
|
@ -55,10 +56,13 @@ import org.apache.openjpa.kernel.QueryStatistics;
|
|||
import org.apache.openjpa.kernel.exps.AggregateListener;
|
||||
import org.apache.openjpa.kernel.exps.FilterListener;
|
||||
import org.apache.openjpa.kernel.jpql.JPQLParser;
|
||||
import org.apache.openjpa.lib.log.Log;
|
||||
import org.apache.openjpa.lib.rop.ResultList;
|
||||
import org.apache.openjpa.lib.util.Localizer;
|
||||
import org.apache.openjpa.util.ImplHelper;
|
||||
import org.apache.openjpa.util.RuntimeExceptionTranslator;
|
||||
import org.apache.openjpa.util.UserException;
|
||||
|
||||
import static org.apache.openjpa.kernel.QueryLanguages.LANG_PREPARED_SQL;
|
||||
|
||||
/**
|
||||
|
@ -86,7 +90,7 @@ public class QueryImpl implements OpenJPAQuerySPI, Serializable {
|
|||
* Constructor; supply factory exception translator and delegate.
|
||||
*
|
||||
* @param em The EntityManager which created this query
|
||||
* @param ret Exception translater for this query
|
||||
* @param ret Exception translator for this query
|
||||
* @param query The underlying "kernel" query.
|
||||
*/
|
||||
public QueryImpl(EntityManagerImpl em, RuntimeExceptionTranslator ret,
|
||||
|
@ -248,33 +252,18 @@ public class QueryImpl implements OpenJPAQuerySPI, Serializable {
|
|||
_query.compile();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
private Object execute() {
|
||||
if (_query.getOperation() != QueryOperations.OP_SELECT)
|
||||
throw new InvalidStateException(_loc.get("not-select-query", _query
|
||||
.getQueryString()), null, null, false);
|
||||
|
||||
Map params = _positional != null ? _positional : _named;
|
||||
Boolean registered = null;
|
||||
PreparedQueryCache cache = _em.getPreparedQueryCache();
|
||||
if (cache != null) {
|
||||
FetchConfiguration fetch = _query.getFetchConfiguration();
|
||||
registered = cache.register(_id, _query, fetch);
|
||||
boolean alreadyCached = (registered == null);
|
||||
String lang = _query.getLanguage();
|
||||
QueryStatistics stats = cache.getStatistics();
|
||||
if (alreadyCached && LANG_PREPARED_SQL.equals(lang)) {
|
||||
PreparedQuery pq = _em.getPreparedQuery(_id);
|
||||
params = pq.reparametrize(params, _em.getBroker());
|
||||
stats.recordExecution(pq.getOriginalQuery(), alreadyCached);
|
||||
} else {
|
||||
stats.recordExecution(_query.getQueryString(), alreadyCached);
|
||||
}
|
||||
}
|
||||
Map params = _positional != null ? _positional
|
||||
: _named != null ? _named : new HashMap();
|
||||
boolean registered = preExecute(params);
|
||||
Object result = _query.execute(params);
|
||||
|
||||
if (registered == Boolean.TRUE) {
|
||||
cache.initialize(_id, result);
|
||||
if (registered) {
|
||||
postExecute(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -636,6 +625,68 @@ public class QueryImpl implements OpenJPAQuerySPI, Serializable {
|
|||
"JPA 2.0 - Method not yet implemented");
|
||||
}
|
||||
|
||||
//
|
||||
// Prepared Query Cache related methods
|
||||
//
|
||||
|
||||
/**
|
||||
* Invoked before a query is executed.
|
||||
* If this receiver is cached as a {@linkplain PreparedQuery prepared query}
|
||||
* then re-parameterizes the given user parameters. The given map is cleared
|
||||
* and re-parameterized values are filled in.
|
||||
*
|
||||
* @param params user supplied parameter key-values. Always supply a
|
||||
* non-null map even if the user has not specified any parameter, because
|
||||
* the same map will to be populated by re-parameterization.
|
||||
*
|
||||
* @return true if this invocation caused the query being registered in the
|
||||
* cache.
|
||||
*/
|
||||
private boolean preExecute(Map params) {
|
||||
PreparedQueryCache cache = _em.getPreparedQueryCache();
|
||||
if (cache == null) {
|
||||
return false;
|
||||
}
|
||||
FetchConfiguration fetch = _query.getFetchConfiguration();
|
||||
Boolean registered = cache.register(_id, _query, fetch);
|
||||
boolean alreadyCached = (registered == null);
|
||||
String lang = _query.getLanguage();
|
||||
QueryStatistics stats = cache.getStatistics();
|
||||
if (alreadyCached && LANG_PREPARED_SQL.equals(lang)) {
|
||||
PreparedQuery pq = _em.getPreparedQuery(_id);
|
||||
try {
|
||||
Map rep = pq.reparametrize(params, _em.getBroker());
|
||||
params.clear();
|
||||
params.putAll(rep);
|
||||
} catch (UserException ue) {
|
||||
invalidatePreparedQuery();
|
||||
Log log = _em.getConfiguration().getLog(
|
||||
OpenJPAConfiguration.LOG_RUNTIME);
|
||||
if (log.isWarnEnabled())
|
||||
log.warn(ue.getMessage());
|
||||
return false;
|
||||
}
|
||||
stats.recordExecution(pq.getOriginalQuery(), alreadyCached);
|
||||
} else {
|
||||
stats.recordExecution(_query.getQueryString(), alreadyCached);
|
||||
}
|
||||
return registered == Boolean.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the registered Prepared Query from the given opaque object.
|
||||
*
|
||||
* @param result an opaque object representing execution result of a query
|
||||
*
|
||||
* @return true if the prepared query can be initialized.
|
||||
*/
|
||||
boolean postExecute(Object result) {
|
||||
PreparedQueryCache cache = _em.getPreparedQueryCache();
|
||||
if (cache == null) {
|
||||
return false;
|
||||
}
|
||||
return cache.initialize(_id, result) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this query from PreparedQueryCache.
|
||||
|
@ -680,5 +731,4 @@ public class QueryImpl implements OpenJPAQuerySPI, Serializable {
|
|||
_id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue