From a727a98a212ffb7d9bf67891e4e6f3ece6399395 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 9 Dec 2016 23:45:57 +1300 Subject: [PATCH] #917 - ENH: Add findNative() ... ala automatic mapping of native SQL into object graphs / paths. --- src/main/java/com/avaje/ebean/Ebean.java | 24 +++ .../java/com/avaje/ebean/EbeanServer.java | 22 +++ src/main/java/com/avaje/ebean/Finder.java | 6 + .../com/avaje/ebeaninternal/api/SpiQuery.java | 10 ++ .../server/core/DefaultServer.java | 11 ++ .../server/core/OrmQueryRequest.java | 14 +- .../server/deploy/BeanDescriptor.java | 41 ++++- .../server/deploy/BeanProperty.java | 7 + .../server/deploy/BeanPropertyAssocOne.java | 20 +++ .../server/query/CQueryBuilder.java | 69 +++++++- .../server/query/CQueryPredicates.java | 50 ++++-- .../server/query/NativeSqlQueryPlanKey.java | 42 +++++ .../server/querydefn/DefaultOrmQuery.java | 32 +++- .../ebeaninternal/api/TDSpiEbeanServer.java | 5 + .../model/basic/finder/CustomerFinder.java | 9 + .../query/finder/TestCustomerFinder.java | 10 +- .../rawsql/nativesql/TestNativeSqlBasic.java | 159 ++++++++++++++++++ 17 files changed, 495 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/server/query/NativeSqlQueryPlanKey.java create mode 100644 src/test/java/com/avaje/tests/rawsql/nativesql/TestNativeSqlBasic.java diff --git a/src/main/java/com/avaje/ebean/Ebean.java b/src/main/java/com/avaje/ebean/Ebean.java index e7012913e..74aea15a6 100644 --- a/src/main/java/com/avaje/ebean/Ebean.java +++ b/src/main/java/com/avaje/ebean/Ebean.java @@ -1012,6 +1012,30 @@ public final class Ebean { return serverMgr.getDefaultServer().find(beanType); } + /** + * Create a query using native SQL. + *

+ * The native SQL can contain named parameters or positioned parameters. + *

+ *
{@code
+   *
+   *   String sql = "select c.id, c.name from customer c where c.name like ? order by c.name";
+   *
+   *   Query query = ebeanServer.findNative(Customer.class, sql);
+   *   query.setParameter(1, "Rob%");
+   *
+   *   List customers = query.findList();
+   *
+   * }
+ * + * @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 Query findNative(Class beanType, String nativeSql) { + return serverMgr.getDefaultServer().findNative(beanType, nativeSql); + } + /** * Create an Update query to perform a bulk update. *

diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java index 21f795437..8281d2813 100644 --- a/src/main/java/com/avaje/ebean/EbeanServer.java +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -298,6 +298,28 @@ public interface EbeanServer { */ Query find(Class beanType); + /** + * Create a query using native SQL. + *

+ * The native SQL can contain named parameters or positioned parameters. + *

+ *
{@code
+   *
+   *   String sql = "select c.id, c.name from customer c where c.name like ? order by c.name";
+   *
+   *   Query query = ebeanServer.findNative(Customer.class, sql);
+   *   query.setParameter(1, "Rob%");
+   *
+   *   List customers = query.findList();
+   *
+   * }
+ * + * @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 + */ + Query findNative(Class beanType, String nativeSql); + /** * Return the next unique identity value for a given bean type. *

diff --git a/src/main/java/com/avaje/ebean/Finder.java b/src/main/java/com/avaje/ebean/Finder.java index 6cd7cf1bd..b904ec891 100644 --- a/src/main/java/com/avaje/ebean/Finder.java +++ b/src/main/java/com/avaje/ebean/Finder.java @@ -157,4 +157,10 @@ public class Finder { return db().find(type); } + /** + * Creates a native sql query. + */ + public Query nativeSql(String nativeSql) { + return db().findNative(type, nativeSql); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index b4f9ca1af..0b0a57311 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -148,6 +148,16 @@ public interface SpiQuery extends Query { */ 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. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index fbead895c..7c0a8d1ca 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -897,6 +897,17 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return createQuery(beanType); } + @Override + public Query findNative(Class beanType, String nativeSql) { + BeanDescriptor desc = getBeanDescriptor(beanType); + if (desc == null) { + throw new PersistenceException(beanType.getName() + " is NOT an Entity Bean registered with this server?"); + } + DefaultOrmQuery query = new DefaultOrmQuery<>(desc, this, expressionFactory); + query.setNativeSql(nativeSql); + return query; + } + @Override public Query createNamedQuery(Class beanType, String namedQuery) { BeanDescriptor desc = getBeanDescriptor(beanType); diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java index 8e9ac1e77..a17d0e5c3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java @@ -53,8 +53,6 @@ public final class OrmQueryRequest 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 extends BeanRequest implements BeanQueryRe public OrmQueryRequest(SpiEbeanServer server, OrmQueryEngine queryEngine, SpiQuery 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 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 extends BeanRequest implements BeanQueryRe public void setDefaultFetchBuffer(int fetchSize) { query.setDefaultFetchBuffer(fetchSize); } + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 360a8e41a..de27672fe 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -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 implements MetaBeanInfo, BeanType { */ protected final LinkedHashMap propMap; + /** + * Map of DB column to property path (for nativeSql mapping). + */ + private final Map columnPath = new HashMap<>(); + + /** + * Map of related table to assoc property (for nativeSql mapping). + */ + private final Map> tablePath = new HashMap<>(); + /** * The type of bean this describes. */ @@ -673,6 +684,7 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { if (!prop.isId()) { prop.initialise(); } + prop.registerColumn(this, null); } } @@ -699,6 +711,16 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { } } + 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 implements MetaBeanInfo, BeanType { * Prepare the query for multi-tenancy check for document store only use. */ public void prepareQuery(SpiQuery 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 implements MetaBeanInfo, BeanType { 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. *

diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index 00efd2115..a8eeb6889 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -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)); + } + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index ef723bd65..fce69143b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -275,6 +275,26 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { } } + @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 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. diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java index 39ac47505..22ecc40f8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -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 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(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java index dcd59eaa3..f56363322 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryPredicates.java @@ -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); + } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/NativeSqlQueryPlanKey.java b/src/main/java/com/avaje/ebeaninternal/server/query/NativeSqlQueryPlanKey.java new file mode 100644 index 000000000..b43fae81d --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/query/NativeSqlQueryPlanKey.java @@ -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(); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index c6cac7176..c8b0b4dad 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -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 implements SpiQuery { private OrmUpdateProperties updateProperties; + private String nativeSql; + public DefaultOrmQuery(BeanDescriptor desc, EbeanServer server, ExpressionFactory expressionFactory) { this.beanDescriptor = desc; this.beanType = desc.getBeanType(); @@ -242,6 +245,10 @@ public class DefaultOrmQuery implements SpiQuery { this.detail = new OrmQueryDetail(); } + public void setNativeSql(String nativeSql) { + this.nativeSql = nativeSql; + } + @Override public BeanDescriptor getBeanDescriptor() { return beanDescriptor; @@ -259,7 +266,7 @@ public class DefaultOrmQuery implements SpiQuery { @Override public boolean isAutoTunable() { - return beanDescriptor.isAutoTunable(); + return nativeSql == null && beanDescriptor.isAutoTunable(); } @Override @@ -901,14 +908,27 @@ public class DefaultOrmQuery implements SpiQuery { */ 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. */ diff --git a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java index 91a381297..af816ab28 100644 --- a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java @@ -314,6 +314,11 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return null; } + @Override + public Query findNative(Class beanType, String nativeSql) { + return null; + } + @Override public Set validateQuery(Query query) { return null; diff --git a/src/test/java/com/avaje/tests/model/basic/finder/CustomerFinder.java b/src/test/java/com/avaje/tests/model/basic/finder/CustomerFinder.java index 9554b865d..41aeed436 100644 --- a/src/test/java/com/avaje/tests/model/basic/finder/CustomerFinder.java +++ b/src/test/java/com/avaje/tests/model/basic/finder/CustomerFinder.java @@ -4,6 +4,8 @@ import com.avaje.ebean.Finder; import com.avaje.tests.model.basic.Customer; import org.jetbrains.annotations.Nullable; +import java.util.List; + /** * Finder methods for Customer. */ @@ -25,4 +27,11 @@ public class CustomerFinder extends Finder { return query().where().eq("name", name).findUnique(); } + + public List namesStartingWith(String name) { + + return nativeSql("select name from o_customer where name like ? order by name") + .setParameter(1, name+"%") + .findSingleAttributeList(); + } } diff --git a/src/test/java/com/avaje/tests/query/finder/TestCustomerFinder.java b/src/test/java/com/avaje/tests/query/finder/TestCustomerFinder.java index d956b9030..7170dc250 100644 --- a/src/test/java/com/avaje/tests/query/finder/TestCustomerFinder.java +++ b/src/test/java/com/avaje/tests/query/finder/TestCustomerFinder.java @@ -8,7 +8,7 @@ import org.junit.Test; import java.util.List; -import static org.assertj.core.api.StrictAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; public class TestCustomerFinder extends BaseTestCase { @@ -62,4 +62,12 @@ public class TestCustomerFinder extends BaseTestCase { assertThat(notThere).isNull(); } + @Test + public void test_nativeSingleAttribute() { + + ResetBasicData.reset(); + + List names = Customer.find.namesStartingWith("F"); + assertThat(names).isNotEmpty(); + } } diff --git a/src/test/java/com/avaje/tests/rawsql/nativesql/TestNativeSqlBasic.java b/src/test/java/com/avaje/tests/rawsql/nativesql/TestNativeSqlBasic.java new file mode 100644 index 000000000..9e2f23ac8 --- /dev/null +++ b/src/test/java/com/avaje/tests/rawsql/nativesql/TestNativeSqlBasic.java @@ -0,0 +1,159 @@ +package com.avaje.tests.rawsql.nativesql; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.BeanState; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.Query; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.Order; +import com.avaje.tests.model.basic.ResetBasicData; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestNativeSqlBasic extends BaseTestCase { + + @Test + public void noBindParams() { + + ResetBasicData.reset(); + + EbeanServer server = Ebean.getDefaultServer(); + + String nativeSql = "select id, name from o_customer"; + + Query query = server.findNative(Customer.class, nativeSql); + + List customers = query.findList(); + assertThat(customers).isNotEmpty(); + + BeanState beanState = server.getBeanState(customers.get(0)); + assertThat(beanState.getLoadedProps()).contains("id", "name"); + } + + @Test + public void selectAll() { + + ResetBasicData.reset(); + + EbeanServer server = Ebean.getDefaultServer(); + + String nativeSql = "select * from o_customer"; + Query query = server.findNative(Customer.class, nativeSql); + + List customers = query.findList(); + assertThat(customers).isNotEmpty(); + + BeanState beanState = server.getBeanState(customers.get(0)); + assertThat(beanState.getLoadedProps().size()).isGreaterThan(10); + } + + @Test + public void selectWithAssocOne() { + + ResetBasicData.reset(); + + EbeanServer server = Ebean.getDefaultServer(); + + String nativeSql = "select c.*, b.city from o_customer c join o_address b on b.id = c.billing_address_id"; + Query query = server.findNative(Customer.class, nativeSql); + + List customers = query.findList(); + assertThat(customers).isNotEmpty(); + } + + @Test + public void namedParam() { + + ResetBasicData.reset(); + + EbeanServer server = Ebean.getDefaultServer(); + + String nativeSql = "select id, name from o_customer where id > :some"; + + Query query = server.findNative(Customer.class, nativeSql); + query.setParameter("some", 1); + + List customers = query.findList(); + assertThat(customers).isNotEmpty(); + + Query query2 = server.findNative(Customer.class, nativeSql); + query2.setParameter("some", 2); + + List customers2 = query2.findList(); + assertThat(customers2).isNotEmpty(); + } + + @Test + public void partialAndLazyLoad() { + + ResetBasicData.reset(); + + EbeanServer server = Ebean.getDefaultServer(); + + String nativeSql = "select id, name from o_customer where id > ?"; + + List customers = server.findNative(Customer.class, nativeSql) + .setParameter(1, 1) + .findList(); + + for (Customer customer : customers) { + customer.getStatus(); + } + } + + @Test + public void partialAssoc() { + + ResetBasicData.reset(); + + EbeanServer server = Ebean.getDefaultServer(); + + String nativeSql = "select o.id, o.status, c.id, c.name, c.version from o_order o join o_customer c on c.id = o.kcustomer_id "; + + List orders = server.findNative(Order.class, nativeSql) + .findList(); + + for (Order order : orders) { + order.getStatus(); + order.getCustomer().getName(); + } + } + + @Test + public void fetchQuery() { + + ResetBasicData.reset(); + + String nativeSql = "select * from o_customer where id > ?"; + List customers = + Ebean.findNative(Customer.class, nativeSql) + .setParameter(1, 1) + .fetchQuery("contacts") + .findList(); + + assertThat(customers).isNotEmpty(); + + BeanState beanState = Ebean.getBeanState(customers.get(0)); + assertThat(beanState.getLoadedProps().size()).isGreaterThan(10); + } + + @Test + public void columnAlias() { + + ResetBasicData.reset(); + + String nativeSql = "select c.id, 'foo' as name from o_customer c"; + List customers = + Ebean.findNative(Customer.class, nativeSql) + .findList(); + + assertThat(customers).isNotEmpty(); + + BeanState beanState = Ebean.getBeanState(customers.get(0)); + assertThat(beanState.getLoadedProps()).contains("id", "name"); + } +}