#917 - ENH: Add findNative() ... ala automatic mapping of native SQL into object graphs / paths.

This commit is contained in:
Rob Bygrave
2016-12-09 23:45:57 +13:00
parent e75b5e8b30
commit a727a98a21
17 changed files with 495 additions and 36 deletions
+24
View File
@@ -1012,6 +1012,30 @@ public final class Ebean {
return serverMgr.getDefaultServer().find(beanType);
}
/**
* Create a query using native SQL.
* <p>
* The native SQL can contain named parameters or positioned parameters.
* </p>
* <pre>{@code
*
* String sql = "select c.id, c.name from customer c where c.name like ? order by c.name";
*
* Query<Customer> query = ebeanServer.findNative(Customer.class, sql);
* query.setParameter(1, "Rob%");
*
* List<Customer> customers = query.findList();
*
* }</pre>
*
* @param beanType The type of entity bean to fetch
* @param nativeSql The SQL that can contain named or positioned parameters
* @return The query to set parameters and execute
*/
public static <T> Query<T> findNative(Class<T> beanType, String nativeSql) {
return serverMgr.getDefaultServer().findNative(beanType, nativeSql);
}
/**
* Create an Update query to perform a bulk update.
* <p>
@@ -298,6 +298,28 @@ public interface EbeanServer {
*/
<T> Query<T> find(Class<T> beanType);
/**
* Create a query using native SQL.
* <p>
* The native SQL can contain named parameters or positioned parameters.
* </p>
* <pre>{@code
*
* String sql = "select c.id, c.name from customer c where c.name like ? order by c.name";
*
* Query<Customer> query = ebeanServer.findNative(Customer.class, sql);
* query.setParameter(1, "Rob%");
*
* List<Customer> customers = query.findList();
*
* }</pre>
*
* @param beanType The type of entity bean to fetch
* @param nativeSql The SQL that can contain named or positioned parameters
* @return The query to set parameters and execute
*/
<T> Query<T> findNative(Class<T> beanType, String nativeSql);
/**
* Return the next unique identity value for a given bean type.
* <p>
@@ -157,4 +157,10 @@ public class Finder<I, T> {
return db().find(type);
}
/**
* Creates a native sql query.
*/
public Query<T> nativeSql(String nativeSql) {
return db().findNative(type, nativeSql);
}
}
@@ -148,6 +148,16 @@ public interface SpiQuery<T> extends Query<T> {
*/
boolean isAutoTunable();
/**
* Return true if this is a native sql query.
*/
boolean isNativeSql();
/**
* Return the unmodified native sql query (with named params etc).
*/
String getNativeSql();
/**
* Return the bean descriptor for this query.
*/
@@ -897,6 +897,17 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
return createQuery(beanType);
}
@Override
public <T> Query<T> findNative(Class<T> beanType, String nativeSql) {
BeanDescriptor<T> desc = getBeanDescriptor(beanType);
if (desc == null) {
throw new PersistenceException(beanType.getName() + " is NOT an Entity Bean registered with this server?");
}
DefaultOrmQuery<T> query = new DefaultOrmQuery<>(desc, this, expressionFactory);
query.setNativeSql(nativeSql);
return query;
}
@Override
public <T> Query<T> createNamedQuery(Class<T> beanType, String namedQuery) {
BeanDescriptor<T> desc = getBeanDescriptor(beanType);
@@ -53,8 +53,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
private final Boolean readOnly;
private final RawSql rawSql;
private LoadContext loadContext;
private PersistenceContext persistenceContext;
@@ -73,7 +71,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
public OrmQueryRequest(SpiEbeanServer server, OrmQueryEngine queryEngine, SpiQuery<T> query, SpiTransaction t) {
super(server, t);
this.beanDescriptor = query.getBeanDescriptor();
this.rawSql = query.getRawSql();
this.finder = beanDescriptor.getBeanFinder();
this.queryEngine = queryEngine;
this.query = query;
@@ -161,13 +158,17 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
this.queryPlanKey = query.prepare(this);
}
public boolean isNativeSql() {
return query.isNativeSql();
}
public boolean isRawSql() {
return rawSql != null;
return query.isRawSql();
}
public DeployParser createDeployParser() {
if (rawSql != null) {
return new DeployPropertyParserMap(rawSql.getColumnMapping().getMapping());
if (query.isRawSql()) {
return new DeployPropertyParserMap(query.getRawSql().getColumnMapping().getMapping());
} else {
return beanDescriptor.createDeployPropertyParser();
}
@@ -531,4 +532,5 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
public void setDefaultFetchBuffer(int fetchSize) {
query.setDefaultFetchBuffer(fetchSize);
}
}
@@ -89,6 +89,7 @@ import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -204,6 +205,16 @@ public class BeanDescriptor<T> implements MetaBeanInfo, BeanType<T> {
*/
protected final LinkedHashMap<String, BeanProperty> propMap;
/**
* Map of DB column to property path (for nativeSql mapping).
*/
private final Map<String, String> columnPath = new HashMap<>();
/**
* Map of related table to assoc property (for nativeSql mapping).
*/
private final Map<String, BeanPropertyAssoc<?>> tablePath = new HashMap<>();
/**
* The type of bean this describes.
*/
@@ -673,6 +684,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo, BeanType<T> {
if (!prop.isId()) {
prop.initialise();
}
prop.registerColumn(this, null);
}
}
@@ -699,6 +711,16 @@ public class BeanDescriptor<T> implements MetaBeanInfo, BeanType<T> {
}
}
void registerColumn(String dbColumn, String path) {
columnPath.put(dbColumn.toLowerCase(), path);
}
void registerTable(String baseTable, BeanPropertyAssoc<?> assocProperty) {
if (baseTable != null) {
tablePath.put(baseTable.toLowerCase(), assocProperty);
}
}
/**
* Perform last initialisation for the descriptor.
*/
@@ -1081,7 +1103,7 @@ public class BeanDescriptor<T> implements MetaBeanInfo, BeanType<T> {
* Prepare the query for multi-tenancy check for document store only use.
*/
public void prepareQuery(SpiQuery<T> query) {
if (tenant != null) {
if (tenant != null && !query.isNativeSql()) {
Object tenantId = ebeanServer.currentTenantId();
if (tenantId != null) {
query.where().eq(tenant.getName(), tenantId);
@@ -2163,6 +2185,23 @@ public class BeanDescriptor<T> implements MetaBeanInfo, BeanType<T> {
return chain.add(property).build();
}
/**
* Return the property path given the db table and column.
*/
public String findBeanPath(String tableName, String columnName) {
if (tableName.length() == 0 || tableName.equalsIgnoreCase(baseTable)) {
return columnPath.get(columnName);
}
BeanPropertyAssoc<?> assocProperty = tablePath.get(tableName);
if (assocProperty != null) {
String relativePath = assocProperty.getTargetDescriptor().findBeanPath(tableName, columnName);
if (relativePath != null) {
return SplitName.add(assocProperty.getName(), relativePath);
}
}
return null;
}
/**
* Find a BeanProperty including searching the inheritance hierarchy.
* <p>
@@ -19,6 +19,7 @@ import com.avaje.ebeaninternal.server.el.ElPropertyValue;
import com.avaje.ebeaninternal.server.lib.util.StringHelper;
import com.avaje.ebeaninternal.server.properties.BeanPropertyGetter;
import com.avaje.ebeaninternal.server.properties.BeanPropertySetter;
import com.avaje.ebeaninternal.server.query.SplitName;
import com.avaje.ebeaninternal.server.query.SqlBeanLoad;
import com.avaje.ebeaninternal.server.query.SqlJoinType;
import com.avaje.ebeaninternal.server.text.json.ReadJson;
@@ -1424,4 +1425,10 @@ public class BeanProperty implements ElPropertyValue, Property {
public void merge(EntityBean bean, EntityBean existing) {
// do nothing unless Many property
}
public void registerColumn(BeanDescriptor<?> desc, String prefix) {
if (dbColumn != null) {
desc.registerColumn(dbColumn, SplitName.add(prefix, name));
}
}
}
@@ -275,6 +275,26 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> {
}
}
@Override
public void registerColumn(BeanDescriptor<?> desc, String prefix) {
if (embedded) {
for (BeanProperty prop : embeddedProps) {
prop.registerColumn(desc, SplitName.add(prefix, name));
}
} else {
if (targetIdProperty != null) {
BeanDescriptor<T> target = getTargetDescriptor();
String basePath = SplitName.add(prefix, name);
if (dbColumn != null) {
BeanProperty idProperty = target.getIdProperty();
desc.registerColumn(dbColumn, SplitName.add(basePath, idProperty.getName()));
}
desc.registerTable(target.getBaseTable(), this);
}
}
}
/**
* Return meta data for the deployment of the embedded bean specific to this
* property.
@@ -24,6 +24,12 @@ import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail;
import com.avaje.ebeaninternal.server.querydefn.OrmQueryLimitRequest;
import javax.persistence.PersistenceException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -343,13 +349,58 @@ class CQueryBuilder {
*/
private SqlTree createSqlTree(OrmQueryRequest<?> request, CQueryPredicates predicates, CQueryHistorySupport historySupport, CQueryDraftSupport draftSupport) {
if (request.isNativeSql()) {
return createNativeSqlTree(request, predicates);
}
if (request.isRawSql()) {
return createRawSqlSqlTree(request, predicates);
}
return new SqlTreeBuilder(tableAliasPlaceHolder, columnAliasPrefix, request, predicates, historySupport, draftSupport).build();
}
/**
* Create the SqlTree by reading the ResultSetMetaData and mapping table/columns to bean property paths.
*/
private SqlTree createNativeSqlTree(OrmQueryRequest<?> request, CQueryPredicates predicates) {
SpiQuery<?> query = request.getQuery();
// parse named parameters returning the final sql to execute
String sql = predicates.parseBindParams(query.getNativeSql());
query.setGeneratedSql(sql);
Connection connection = request.getTransaction().getConnection();
BeanDescriptor<?> desc = request.getBeanDescriptor();
try {
PreparedStatement statement = connection.prepareStatement(sql);
predicates.bind(statement, connection);
List<String> propertyNames = new ArrayList<>();
ResultSet resultSet = statement.executeQuery();
ResultSetMetaData metaData = resultSet.getMetaData();
int cols = 1 + metaData.getColumnCount();
for (int i = 1; i < cols; i++) {
String tableName = metaData.getTableName(i).toLowerCase();
String columnName = metaData.getColumnName(i).toLowerCase();
String path = desc.findBeanPath(tableName, columnName);
if (path != null) {
propertyNames.add(path);
} else {
propertyNames.add(RawSqlBuilder.IGNORE_COLUMN);
}
}
RawSql rawSql = RawSqlBuilder.resultSet(resultSet, propertyNames.toArray(new String[propertyNames.size()]));
query.setRawSql(rawSql);
return createRawSqlSqlTree(request, predicates);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private SqlTree createRawSqlSqlTree(OrmQueryRequest<?> request, CQueryPredicates predicates) {
BeanDescriptor<?> descriptor = request.getBeanDescriptor();
@@ -376,8 +427,12 @@ class CQueryBuilder {
throw new PersistenceException("Property [" + propertyName + "] not found on " + descriptor.getFullName());
} else {
BeanProperty beanProperty = el.getBeanProperty();
if (beanProperty.isId() || beanProperty.isDiscriminator()) {
// For @Id properties we chop off the last part of the path
if (beanProperty.isId()) {
if (propertyName.contains(".")) {
// For @Id properties we chop off the last part of the path
propertyName = SplitName.parent(propertyName);
}
} else if (beanProperty.isDiscriminator()) {
propertyName = SplitName.parent(propertyName);
} else if (beanProperty instanceof BeanPropertyAssocOne<?>) {
String msg = "Column [" + column.getDbColumn() + "] mapped to complex Property[" + propertyName + "]";
@@ -436,9 +491,11 @@ class CQueryBuilder {
SpiQuery<?> query = request.getQuery();
RawSql rawSql = query.getRawSql();
if (rawSql != null) {
return rawSqlHandler.buildSql(request, predicates, rawSql.getSql());
if (query.isNativeSql()) {
return new SqlLimitResponse(query.getGeneratedSql(), false);
}
if (query.isRawSql()) {
return rawSqlHandler.buildSql(request, predicates, query.getRawSql().getSql());
}
BeanPropertyAssocMany<?> manyProp = select.getManyProperty();
@@ -136,16 +136,19 @@ public class CQueryPredicates {
dataBind.append(", ");
}
int asOfTableCount = request.getQueryPlan().getAsOfTableCount();
if (asOfTableCount > 0) {
// bind the asOf value for each table alias as part of the from/join clauses
// there is one effective date predicate per table alias
Timestamp asOf = query.getAsOf();
dataBind.append("asOf ").append(asOf);
for (int i = 0; i < asOfTableCount * binder.getAsOfBindCount(); i++) {
binder.bindObject(dataBind, asOf);
CQueryPlan queryPlan = request.getQueryPlan();
if (queryPlan != null) {
int asOfTableCount = queryPlan.getAsOfTableCount();
if (asOfTableCount > 0) {
// bind the asOf value for each table alias as part of the from/join clauses
// there is one effective date predicate per table alias
Timestamp asOf = query.getAsOf();
dataBind.append("asOf ").append(asOf);
for (int i = 0; i < asOfTableCount * binder.getAsOfBindCount(); i++) {
binder.bindObject(dataBind, asOf);
}
dataBind.append(", ");
}
dataBind.append(", ");
}
if (idValue != null) {
@@ -184,18 +187,33 @@ public class CQueryPredicates {
}
}
public String parseBindParams(String sql) {
if (bindParams != null && bindParams.requiresNamedParamsPrepare()) {
return BindParamsParser.parse(bindParams, sql);
} else {
return sql;
}
}
/**
* Convert named parameters into an OrderedList.
*/
private void buildBindWhereRawSql(boolean buildSql) {
if (!buildSql && query.isRawSql() && bindParams != null && bindParams.requiresNamedParamsPrepare()) {
// RawSql query hit cached query plan. Need to convert
// named parameters into positioned parameters so that
// the named parameters are bound
RawSql.Sql sql = query.getRawSql().getSql();
String s = sql.isParsed() ? sql.getPreWhere() : sql.getUnparsedSql();
BindParamsParser.parse(bindParams, s);
if (!buildSql && bindParams != null && bindParams.requiresNamedParamsPrepare()) {
if (query.isNativeSql()) {
// convert named params into positioned params
String sql = query.getNativeSql();
BindParamsParser.parse(bindParams, sql);
} else if (query.isRawSql()) {
// RawSql query hit cached query plan. Need to convert
// named parameters into positioned parameters so that
// the named parameters are bound
RawSql.Sql sql = query.getRawSql().getSql();
String s = sql.isParsed() ? sql.getPreWhere() : sql.getUnparsedSql();
BindParamsParser.parse(bindParams, s);
}
}
}
@@ -0,0 +1,42 @@
package com.avaje.ebeaninternal.server.query;
import com.avaje.ebeaninternal.api.CQueryPlanKey;
/**
* QueryPlanKey for native sql queries.
*/
public class NativeSqlQueryPlanKey implements CQueryPlanKey {
private final String sql;
public NativeSqlQueryPlanKey(String sql) {
this.sql = sql;
}
public String toString() {
return getPartialKey();
}
/**
* Return as a partial key. For rawSql hash the sql is part of the key and as such
* needs to be included in order to have a complete key. Typically the MD5 of the sql
* can be used as a short form proxy for the actual sql.
*/
public String getPartialKey() {
return hashCode() + "_n";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NativeSqlQueryPlanKey that = (NativeSqlQueryPlanKey) o;
return sql.equals(that.sql);
}
@Override
public int hashCode() {
return sql.hashCode();
}
}
@@ -26,6 +26,7 @@ import com.avaje.ebeaninternal.server.deploy.TableJoin;
import com.avaje.ebeaninternal.server.expression.DefaultExpressionList;
import com.avaje.ebeaninternal.server.expression.SimpleExpression;
import com.avaje.ebeaninternal.server.query.CancelableQuery;
import com.avaje.ebeaninternal.server.query.NativeSqlQueryPlanKey;
import java.sql.Timestamp;
import java.util.ArrayList;
@@ -234,6 +235,8 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
private OrmUpdateProperties updateProperties;
private String nativeSql;
public DefaultOrmQuery(BeanDescriptor<T> desc, EbeanServer server, ExpressionFactory expressionFactory) {
this.beanDescriptor = desc;
this.beanType = desc.getBeanType();
@@ -242,6 +245,10 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
this.detail = new OrmQueryDetail();
}
public void setNativeSql(String nativeSql) {
this.nativeSql = nativeSql;
}
@Override
public BeanDescriptor<T> getBeanDescriptor() {
return beanDescriptor;
@@ -259,7 +266,7 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
@Override
public boolean isAutoTunable() {
return beanDescriptor.isAutoTunable();
return nativeSql == null && beanDescriptor.isAutoTunable();
}
@Override
@@ -901,14 +908,27 @@ public class DefaultOrmQuery<T> implements SpiQuery<T> {
*/
CQueryPlanKey createQueryPlanKey() {
queryPlanKey = new OrmQueryPlanKey(m2mIncludeJoin, type, detail, maxRows, firstRow,
disableLazyLoading, orderBy,
distinct, sqlDistinct, mapKey, id, bindParams, whereExpressions, havingExpressions,
temporalMode, forUpdate, rootTableAlias, rawSql, updateProperties);
if (isNativeSql()) {
queryPlanKey = new NativeSqlQueryPlanKey(nativeSql);
} else {
queryPlanKey = new OrmQueryPlanKey(m2mIncludeJoin, type, detail, maxRows, firstRow,
disableLazyLoading, orderBy,
distinct, sqlDistinct, mapKey, id, bindParams, whereExpressions, havingExpressions,
temporalMode, forUpdate, rootTableAlias, rawSql, updateProperties);
}
return queryPlanKey;
}
@Override
public boolean isNativeSql() {
return nativeSql != null;
}
@Override
public String getNativeSql() {
return nativeSql;
}
/**
* Prepare the query which prepares any expressions (sub-query expressions etc) and calculates the query plan key.
*/