From aad5b33db225ccbba074ec896512bbdfbe26b0f6 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 2 Mar 2018 23:32:01 +1300 Subject: [PATCH] #1297 - ENH: Add DtoQuery ... such that we can map native SQL queries automatically into "DTO beans" --- src/main/java/io/ebean/DtoQuery.java | 133 +++++++ src/main/java/io/ebean/Ebean.java | 15 + src/main/java/io/ebean/EbeanServer.java | 13 + .../java/io/ebean/meta/MetaInfoManager.java | 5 + .../java/io/ebean/meta/MetaQueryMetric.java | 23 ++ .../java/io/ebean/meta/MetaTimedMetric.java | 13 +- .../io/ebeaninternal/api/SpiDtoQuery.java | 46 +++ .../io/ebeaninternal/api/SpiEbeanServer.java | 22 ++ .../io/ebeaninternal/api/SpiSqlBinding.java | 38 ++ .../io/ebeaninternal/api/SpiSqlQuery.java | 32 +- .../ebeaninternal/metric/MetricFactory.java | 9 + .../metric/QueryPlanCollector.java | 26 ++ .../ebeaninternal/metric/QueryPlanMetric.java | 17 + .../io/ebeaninternal/metric/TimedMetric.java | 7 +- .../server/core/AbstractSqlQueryRequest.java | 182 ++++++++++ .../server/core/DefaultMetaInfoManager.java | 6 + .../server/core/DefaultServer.java | 69 ++++ .../server/core/DtoQueryRequest.java | 103 ++++++ .../server/core/InternalConfiguration.java | 17 +- .../server/core/RelationalQueryEngine.java | 5 + .../server/core/RelationalQueryRequest.java | 202 ++--------- .../server/dto/DtoBeanDescriptor.java | 45 +++ .../server/dto/DtoBeanManager.java | 53 +++ .../ebeaninternal/server/dto/DtoColumn.java | 23 ++ .../server/dto/DtoMappingRequest.java | 49 +++ .../io/ebeaninternal/server/dto/DtoMeta.java | 119 +++++++ .../server/dto/DtoMetaBuilder.java | 77 ++++ .../server/dto/DtoMetaConstructor.java | 67 ++++ .../server/dto/DtoMetaProperty.java | 68 ++++ .../server/dto/DtoQueryPlan.java | 27 ++ .../server/dto/DtoQueryPlanBase.java | 28 ++ .../server/dto/DtoQueryPlanConPlus.java | 32 ++ .../server/dto/DtoQueryPlanConSetter.java | 32 ++ .../server/dto/DtoQueryPlanConstructor.java | 24 ++ .../ebeaninternal/server/dto/DtoReadSet.java | 21 ++ .../server/dto/DtoReadSetColumnSkip.java | 21 ++ .../server/profile/DMetricFactory.java | 12 + .../server/profile/DQueryPlanCollector.java | 33 ++ .../server/profile/DQueryPlanMeta.java | 31 ++ .../server/profile/DQueryPlanMetric.java | 102 ++++++ .../server/profile/DTimeMetricStats.java | 12 +- .../server/profile/DTimedMetric.java | 51 +-- .../query/DefaultRelationalQueryEngine.java | 32 +- .../server/query/dto/DtoQueryEngine.java | 66 ++++ .../server/querydefn/DefaultDtoQuery.java | 196 ++++++++++ src/test/java/io/ebean/DtoQueryTest.java | 336 ++++++++++++++++++ .../ebeaninternal/api/TDSpiEbeanServer.java | 26 ++ 47 files changed, 2293 insertions(+), 273 deletions(-) create mode 100644 src/main/java/io/ebean/DtoQuery.java create mode 100644 src/main/java/io/ebean/meta/MetaQueryMetric.java create mode 100644 src/main/java/io/ebeaninternal/api/SpiDtoQuery.java create mode 100644 src/main/java/io/ebeaninternal/api/SpiSqlBinding.java create mode 100644 src/main/java/io/ebeaninternal/metric/QueryPlanCollector.java create mode 100644 src/main/java/io/ebeaninternal/metric/QueryPlanMetric.java create mode 100644 src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java create mode 100644 src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoBeanDescriptor.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoBeanManager.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoColumn.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoMappingRequest.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoMeta.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoMetaConstructor.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoQueryPlan.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanBase.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConPlus.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConSetter.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConstructor.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoReadSet.java create mode 100644 src/main/java/io/ebeaninternal/server/dto/DtoReadSetColumnSkip.java create mode 100644 src/main/java/io/ebeaninternal/server/profile/DQueryPlanCollector.java create mode 100644 src/main/java/io/ebeaninternal/server/profile/DQueryPlanMeta.java create mode 100644 src/main/java/io/ebeaninternal/server/profile/DQueryPlanMetric.java create mode 100644 src/main/java/io/ebeaninternal/server/query/dto/DtoQueryEngine.java create mode 100644 src/main/java/io/ebeaninternal/server/querydefn/DefaultDtoQuery.java create mode 100644 src/test/java/io/ebean/DtoQueryTest.java diff --git a/src/main/java/io/ebean/DtoQuery.java b/src/main/java/io/ebean/DtoQuery.java new file mode 100644 index 000000000..c99ffdfbb --- /dev/null +++ b/src/main/java/io/ebean/DtoQuery.java @@ -0,0 +1,133 @@ +package io.ebean; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Query for performing native SQL queries that return DTO Bean's. + *

+ * These beans are just normal classes. They must have public constructors + * and setters. + *

+ * Constructors with arguments are used if the number of constructor arguments + * matches the number of columns in the resultSet. + *

+ *

+ * If the number of columns in the resultSet is greater than the largest constructor + * then the largest constructor is used for the first columns and remaining columns + * are mapped by setter methods. + *

+ * + *
{@code
+ *
+ *   // CustomerDto is just a 'bean like' class
+ *   // with public constructor(s) and public setter methods
+ *
+ *   String sql = "select id, name from customer where name like :name and status_code = :status";
+ *
+ *   List beans =
+ *     Ebean.findDto(CustomrDto.class, sql)
+ *     .setParameter("name", "Acme%")
+ *     .setParameter("status", "ACTIVE")
+ *     .findList();
+ *
+ * }
+ */ +public interface DtoQuery { + + /** + * Execute the query returning a list. + */ + @Nonnull + List findList(); + + /** + * Execute the query iterating a row at a time. + *

+ * This streaming type query is useful for large query execution as only 1 row needs to be held in memory. + *

+ */ + void findEach(Consumer consumer); + + /** + * Execute the query iterating a row at a time with the ability to stop consuming part way through. + *

+ * Returning false after processing a row stops the iteration through the query results. + *

+ *

+ * This streaming type query is useful for large query execution as only 1 row needs to be held in memory. + *

+ */ + void findEachWhile(Predicate consumer); + + /** + * Execute the query returning a single bean. + */ + @Nullable + T findOne(); + + /** + * Execute the query returning an optional bean. + */ + @Nonnull + Optional findOneOrEmpty(); + + /** + * The same as bind for named parameters. + */ + DtoQuery setParameter(String name, Object value); + + /** + * The same as bind for positioned parameters. + */ + DtoQuery setParameter(int position, Object value); + + /** + * Set the index of the first row of the results to return. + */ + DtoQuery setFirstRow(int firstRow); + + /** + * Set the maximum number of query results to return. + */ + DtoQuery setMaxRows(int maxRows); + + /** + * When resultSet columns are not able to be mapped to a bean property then instead of + * throwing effectively skip reading that column. + */ + DtoQuery setRelaxedMode(); + + /** + * Set a label on the query to make it easier to identify queries related to query execution statistics. + * + * @param label A label that is unique to the DTO bean type. + */ + DtoQuery setLabel(String label); + + /** + * Set a timeout on this query. + *

+ * This will typically result in a call to setQueryTimeout() on a + * preparedStatement. If the timeout occurs an exception will be thrown - this + * will be a SQLException wrapped up in a PersistenceException. + *

+ * + * @param secs the query timeout limit in seconds. Zero means there is no limit. + */ + DtoQuery setTimeout(int secs); + + /** + * A hint which for JDBC translates to the Statement.fetchSize(). + *

+ * Gives the JDBC driver a hint as to the number of rows that should be + * fetched from the database when more rows are needed for ResultSet. + *

+ */ + DtoQuery setBufferFetchSizeHint(int bufferFetchSizeHint); + +} diff --git a/src/main/java/io/ebean/Ebean.java b/src/main/java/io/ebean/Ebean.java index 643e2179d..2f0031fd1 100644 --- a/src/main/java/io/ebean/Ebean.java +++ b/src/main/java/io/ebean/Ebean.java @@ -1116,6 +1116,21 @@ public final class Ebean { return serverMgr.getDefaultServer().findNative(beanType, nativeSql); } + /** + * Create a Query for DTO beans. + *

+ * DTO beans are just normal bean like classes with public constructor(s) and setters. + * They do not need to be registered with Ebean before use. + *

+ * + * @param dtoType The type of the DTO bean the rows will be mapped into. + * @param sql The SQL query to execute. + * @param The type of the DTO bean. + */ + public static DtoQuery findDto(Class dtoType, String sql) { + return serverMgr.getDefaultServer().findDto(dtoType, sql); + } + /** * Create an Update query to perform a bulk update. *

diff --git a/src/main/java/io/ebean/EbeanServer.java b/src/main/java/io/ebean/EbeanServer.java index d8bead322..eab9ee668 100644 --- a/src/main/java/io/ebean/EbeanServer.java +++ b/src/main/java/io/ebean/EbeanServer.java @@ -424,6 +424,19 @@ public interface EbeanServer { */ Update createUpdate(Class beanType, String ormUpdate); + /** + * Create a Query for DTO beans. + *

+ * DTO beans are just normal bean like classes with public constructor(s) and setters. + * They do not need to be registered with Ebean before use. + *

+ * + * @param dtoType The type of the DTO bean the rows will be mapped into. + * @param sql The SQL query to execute. + * @param The type of the DTO bean. + */ + DtoQuery findDto(Class dtoType, String sql); + /** * Create a SqlQuery for executing native sql * query statements. diff --git a/src/main/java/io/ebean/meta/MetaInfoManager.java b/src/main/java/io/ebean/meta/MetaInfoManager.java index d9113d156..c8d88cf83 100644 --- a/src/main/java/io/ebean/meta/MetaInfoManager.java +++ b/src/main/java/io/ebean/meta/MetaInfoManager.java @@ -12,6 +12,11 @@ public interface MetaInfoManager { */ List collectTransactionStatistics(boolean reset); + /** + * Collect query plan statistics (new, will migrate ORM query stats over to this). + */ + List collectQueryStatistics(boolean reset); + /** * Collect and return the non-empty query plan statistics for all the beans. *

diff --git a/src/main/java/io/ebean/meta/MetaQueryMetric.java b/src/main/java/io/ebean/meta/MetaQueryMetric.java new file mode 100644 index 000000000..c59142dc6 --- /dev/null +++ b/src/main/java/io/ebean/meta/MetaQueryMetric.java @@ -0,0 +1,23 @@ +package io.ebean.meta; + +/** + * Query execution metrics. + */ +public interface MetaQueryMetric extends MetaTimedMetric { + + /** + * The type of entity or DTO bean. + */ + Class getType(); + + /** + * The label for the query (can be null). + */ + String getLabel(); + + /** + * The actual SQL of the query. + */ + String getSql(); + +} diff --git a/src/main/java/io/ebean/meta/MetaTimedMetric.java b/src/main/java/io/ebean/meta/MetaTimedMetric.java index 719ff07fa..ada73a66f 100644 --- a/src/main/java/io/ebean/meta/MetaTimedMetric.java +++ b/src/main/java/io/ebean/meta/MetaTimedMetric.java @@ -27,17 +27,24 @@ public interface MetaTimedMetric { long getCount(); /** - * Return the total execution time. + * Return the total execution time in micros. */ long getTotal(); /** - * Return the max execution time. + * Return the max execution time in micros. */ long getMax(); /** - * Return the mean execution time. + * Return the mean execution time in micros. */ long getMean(); + + /** + * Return the total beans or rows processed or loaded. + * + * This will be 0 if the metric isn't a query plan (like transaction execution statistics). + */ + long getBeanCount(); } diff --git a/src/main/java/io/ebeaninternal/api/SpiDtoQuery.java b/src/main/java/io/ebeaninternal/api/SpiDtoQuery.java new file mode 100644 index 000000000..d2c4b1fb9 --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/SpiDtoQuery.java @@ -0,0 +1,46 @@ +package io.ebeaninternal.api; + +import io.ebean.DtoQuery; +import io.ebeaninternal.server.dto.DtoMappingRequest; +import io.ebeaninternal.server.dto.DtoQueryPlan; + +/** + * Internal extension to DtoQuery. + */ +public interface SpiDtoQuery extends DtoQuery, SpiSqlBinding { + + /** + * Return the key for query plan. + */ + String planKey(); + + /** + * Get the query plan for the cache. + */ + DtoQueryPlan getQueryPlan(String planKey); + + /** + * Build the query plan. + */ + DtoQueryPlan buildPlan(DtoMappingRequest request); + + /** + * Put the query plan into the cache. + */ + void putQueryPlan(String planKey, DtoQueryPlan plan); + + /** + * Return true if the query is in relaxed mapping mode. + */ + boolean isRelaxedMode(); + + /** + * Return the label for the query. + */ + String getLabel(); + + /** + * Return the associated DTO bean type. + */ + Class getType(); +} diff --git a/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java b/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java index 423c37792..0e6af2a99 100644 --- a/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java +++ b/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java @@ -20,6 +20,8 @@ import io.ebeaninternal.server.query.CQuery; import io.ebeaninternal.server.transaction.RemoteTransactionEvent; import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; /** * Service Provider extension to EbeanServer. @@ -224,4 +226,24 @@ public interface SpiEbeanServer extends EbeanServer, BeanLoader, BeanCollectionL */ void scopedTransactionExit(Object returnOrThrowable, int opCode); + /** + * DTO findList query. + */ + List findDtoList(SpiDtoQuery query); + + /** + * DTO findOne query. + */ + T findDtoOne(SpiDtoQuery query); + + /** + * DTO findEach query. + */ + void findDtoEach(SpiDtoQuery query, Consumer consumer); + + /** + * DTO findEachWhile query. + */ + void findDtoEachWhile(SpiDtoQuery query, Predicate consumer); + } diff --git a/src/main/java/io/ebeaninternal/api/SpiSqlBinding.java b/src/main/java/io/ebeaninternal/api/SpiSqlBinding.java new file mode 100644 index 000000000..6da641dea --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/SpiSqlBinding.java @@ -0,0 +1,38 @@ +package io.ebeaninternal.api; + +/** + * SQL query binding (for SqlQuery and DtoQuery). + */ +public interface SpiSqlBinding { + + /** + * Return the named or positioned parameters. + */ + BindParams getBindParams(); + + /** + * return the query. + */ + String getQuery(); + + /** + * Return the first row to fetch. + */ + int getFirstRow(); + + /** + * Return the maximum number of rows to fetch. + */ + int getMaxRows(); + + /** + * Return the query timeout. + */ + int getTimeout(); + + /** + * Return the hint for Statement.setFetchSize(). + */ + int getBufferFetchSizeHint(); + +} diff --git a/src/main/java/io/ebeaninternal/api/SpiSqlQuery.java b/src/main/java/io/ebeaninternal/api/SpiSqlQuery.java index b296badcd..e289b5b65 100644 --- a/src/main/java/io/ebeaninternal/api/SpiSqlQuery.java +++ b/src/main/java/io/ebeaninternal/api/SpiSqlQuery.java @@ -5,36 +5,6 @@ import io.ebean.SqlQuery; /** * SQL query - Internal extension to SqlQuery. */ -public interface SpiSqlQuery extends SqlQuery { - - /** - * Return the named or positioned parameters. - */ - BindParams getBindParams(); - - /** - * return the query. - */ - String getQuery(); - - /** - * Return the first row to fetch. - */ - int getFirstRow(); - - /** - * Return the maximum number of rows to fetch. - */ - int getMaxRows(); - - /** - * Return the query timeout. - */ - int getTimeout(); - - /** - * Return the hint for Statement.setFetchSize(). - */ - int getBufferFetchSizeHint(); +public interface SpiSqlQuery extends SqlQuery, SpiSqlBinding { } diff --git a/src/main/java/io/ebeaninternal/metric/MetricFactory.java b/src/main/java/io/ebeaninternal/metric/MetricFactory.java index 3ea15470e..6f83c3e89 100644 --- a/src/main/java/io/ebeaninternal/metric/MetricFactory.java +++ b/src/main/java/io/ebeaninternal/metric/MetricFactory.java @@ -22,4 +22,13 @@ public interface MetricFactory { */ TimedMetric createTimedMetric(String name); + /** + * Create a Timed metric. + */ + QueryPlanMetric createQueryPlanMetric(Class type, String label, String sql); + + /** + * Return a instance used to collect Query plan metrics. + */ + QueryPlanCollector createCollector(boolean reset); } diff --git a/src/main/java/io/ebeaninternal/metric/QueryPlanCollector.java b/src/main/java/io/ebeaninternal/metric/QueryPlanCollector.java new file mode 100644 index 000000000..aa7e901d5 --- /dev/null +++ b/src/main/java/io/ebeaninternal/metric/QueryPlanCollector.java @@ -0,0 +1,26 @@ +package io.ebeaninternal.metric; + +import io.ebean.meta.MetaQueryMetric; + +import java.util.List; + +/** + * Object used to collect query plan metrics. + */ +public interface QueryPlanCollector { + + /** + * Return true if the statistics should be reset. + */ + boolean isReset(); + + /** + * Add the query plan statistic. + */ + void add(MetaQueryMetric stats); + + /** + * Return all the collected query plan statistics. + */ + List complete(); +} diff --git a/src/main/java/io/ebeaninternal/metric/QueryPlanMetric.java b/src/main/java/io/ebeaninternal/metric/QueryPlanMetric.java new file mode 100644 index 000000000..893a97593 --- /dev/null +++ b/src/main/java/io/ebeaninternal/metric/QueryPlanMetric.java @@ -0,0 +1,17 @@ +package io.ebeaninternal.metric; + +/** + * Internal Query plan metric holder. + */ +public interface QueryPlanMetric { + + /** + * Return the underlying timed metric. + */ + TimedMetric getMetric(); + + /** + * Collect the non-empty query plan metrics. + */ + void collect(QueryPlanCollector collector); +} diff --git a/src/main/java/io/ebeaninternal/metric/TimedMetric.java b/src/main/java/io/ebeaninternal/metric/TimedMetric.java index c45fa19dc..a2a29e030 100644 --- a/src/main/java/io/ebeaninternal/metric/TimedMetric.java +++ b/src/main/java/io/ebeaninternal/metric/TimedMetric.java @@ -12,7 +12,12 @@ public interface TimedMetric { /** * Add a time event (usually in microseconds). */ - void add(long value); + void add(long micros); + + /** + * Add a time event with the number of loaded beans or rows. + */ + void add(long micros, long beans); /** * Return true if there are no metrics collected since the last collection. diff --git a/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java new file mode 100644 index 000000000..7726fae36 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java @@ -0,0 +1,182 @@ +package io.ebeaninternal.server.core; + +import io.ebean.EbeanServer; +import io.ebean.Transaction; +import io.ebean.util.JdbcClose; +import io.ebeaninternal.api.BindParams; +import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.api.SpiSqlBinding; +import io.ebeaninternal.api.SpiTransaction; +import io.ebeaninternal.server.lib.util.Str; +import io.ebeaninternal.server.persist.Binder; +import io.ebeaninternal.server.persist.TrimLogSql; +import io.ebeaninternal.server.transaction.TransactionManager; +import io.ebeaninternal.server.util.BindParamsParser; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Wraps the objects involved in executing a SQL / Relational Query. + */ +public abstract class AbstractSqlQueryRequest { + + private final SpiSqlBinding query; + + protected final SpiEbeanServer ebeanServer; + + protected SpiTransaction trans; + + private boolean createdTransaction; + + protected String sql; + + protected ResultSet resultSet; + + protected String bindLog = ""; + + protected PreparedStatement pstmt; + + protected long startNano; + + /** + * Create the BeanFindRequest. + */ + AbstractSqlQueryRequest(SpiEbeanServer server, SpiSqlBinding query, Transaction t) { + this.ebeanServer = server; + this.query = query; + this.trans = (SpiTransaction) t; + } + + /** + * Create a transaction if none currently exists. + */ + public void initTransIfRequired() { + if (trans == null) { + trans = ebeanServer.currentServerTransaction(); + if (trans == null || !trans.isActive()) { + // create a local readOnly transaction + trans = ebeanServer.createQueryTransaction(null); + createdTransaction = true; + } + } + } + + /** + * End the transaction if it was locally created. + */ + public void endTransIfRequired() { + if (createdTransaction) { + trans.commit(); + } + } + + public EbeanServer getEbeanServer() { + return ebeanServer; + } + + public SpiTransaction getTransaction() { + return trans; + } + + public boolean isLogSql() { + return trans.isLogSql(); + } + + abstract void setResultSet(ResultSet resultSet) throws SQLException; + + /** + * Return the bindLog for this request. + */ + public String getBindLog() { + return bindLog; + } + + /** + * Return true if we can navigate to the next row. + */ + public boolean next() throws SQLException { + return resultSet.next(); + } + + protected abstract void requestComplete(); + + /** + * Close the underlying resources. + */ + public void close() { + requestComplete(); + JdbcClose.close(resultSet); + JdbcClose.close(pstmt); + } + + + /** + * Prepare the SQL taking into account named bind parameters. + */ + private void prepareSql() { + + String sql = query.getQuery(); + BindParams bindParams = query.getBindParams(); + if (!bindParams.isEmpty()) { + // convert any named parameters if required + sql = BindParamsParser.parse(bindParams, sql); + } + this.sql = limitOffset(sql); + } + + private String limitOffset(String sql) { + + int firstRow = query.getFirstRow(); + int maxRows = query.getMaxRows(); + if (firstRow > 0 || maxRows > 0) { + return ebeanServer.getDatabasePlatform().getBasicSqlLimiter().limit(sql, firstRow, maxRows); + } + return sql; + } + + /** + * Prepare and execute the SQL using the Binder. + */ + public void executeSql(Binder binder) throws SQLException { + + startNano = System.nanoTime(); + + prepareSql(); + + Connection conn = trans.getInternalConnection(); + + pstmt = conn.prepareStatement(sql); + if (query.getTimeout() > 0) { + pstmt.setQueryTimeout(query.getTimeout()); + } + if (query.getBufferFetchSizeHint() > 0) { + pstmt.setFetchSize(query.getBufferFetchSizeHint()); + } + + BindParams bindParams = query.getBindParams(); + if (!bindParams.isEmpty()) { + this.bindLog = binder.bind(bindParams, pstmt, conn); + } + + if (isLogSql()) { + String logSql = TrimLogSql.trim(sql); + if (TransactionManager.SQL_LOGGER.isTraceEnabled()) { + logSql = Str.add(logSql, "; --bind(", bindLog, ")"); + } + trans.logSql(logSql); + } + + setResultSet(pstmt.executeQuery()); + } + + /** + * Return the SQL executed for this query. + */ + public String getSql() { + return sql; + } + +} diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java b/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java index f69aea9d2..b52c66390 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultMetaInfoManager.java @@ -2,6 +2,7 @@ package io.ebeaninternal.server.core; import io.ebean.meta.MetaInfoManager; import io.ebean.meta.MetaObjectGraphNodeStats; +import io.ebean.meta.MetaQueryMetric; import io.ebean.meta.MetaQueryPlanStatistic; import io.ebean.meta.MetaTimedMetric; import io.ebeaninternal.server.deploy.BeanDescriptor; @@ -26,6 +27,11 @@ public class DefaultMetaInfoManager implements MetaInfoManager { return server.collectTransactionStatistics(reset); } + @Override + public List collectQueryStatistics(boolean reset) { + return server.collectQueryStatistics(reset); + } + @Override public List collectQueryPlanStatistics(boolean reset) { diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 5a8b7b4b8..e74e14e53 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -5,6 +5,7 @@ import io.ebean.BackgroundExecutor; import io.ebean.BeanState; import io.ebean.CallableSql; import io.ebean.DocumentStore; +import io.ebean.DtoQuery; import io.ebean.ExpressionFactory; import io.ebean.ExpressionList; import io.ebean.Filter; @@ -47,6 +48,7 @@ import io.ebean.event.BeanPersistController; import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; import io.ebean.meta.MetaInfoManager; +import io.ebean.meta.MetaQueryMetric; import io.ebean.meta.MetaTimedMetric; import io.ebean.plugin.BeanType; import io.ebean.plugin.Plugin; @@ -58,6 +60,7 @@ import io.ebeaninternal.api.LoadBeanRequest; import io.ebeaninternal.api.LoadManyRequest; import io.ebeaninternal.api.ScopedTransaction; import io.ebeaninternal.api.SpiBackgroundExecutor; +import io.ebeaninternal.api.SpiDtoQuery; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiJsonContext; import io.ebeaninternal.api.SpiQuery; @@ -74,6 +77,8 @@ import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanDescriptorManager; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.InheritInfo; +import io.ebeaninternal.server.dto.DtoBeanDescriptor; +import io.ebeaninternal.server.dto.DtoBeanManager; import io.ebeaninternal.server.el.ElFilter; import io.ebeaninternal.server.grammer.EqlParser; import io.ebeaninternal.server.lib.ShutdownManager; @@ -86,6 +91,8 @@ import io.ebeaninternal.server.query.LimitOffsetPagedList; import io.ebeaninternal.server.query.QueryFutureIds; import io.ebeaninternal.server.query.QueryFutureList; import io.ebeaninternal.server.query.QueryFutureRowCount; +import io.ebeaninternal.server.query.dto.DtoQueryEngine; +import io.ebeaninternal.server.querydefn.DefaultDtoQuery; import io.ebeaninternal.server.querydefn.DefaultOrmQuery; import io.ebeaninternal.server.querydefn.DefaultOrmUpdate; import io.ebeaninternal.server.querydefn.DefaultRelationalQuery; @@ -147,9 +154,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { private final OrmQueryEngine queryEngine; private final RelationalQueryEngine relationalQueryEngine; + private final DtoQueryEngine dtoQueryEngine; private final ServerCacheManager serverCacheManager; + private final DtoBeanManager dtoBeanManager; private final BeanDescriptorManager beanDescriptorManager; private final AutoTuneService autoTuneService; @@ -220,6 +229,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { */ public DefaultServer(InternalConfiguration config, ServerCacheManager cache) { + this.dtoBeanManager = config.getDtoBeanManager(); this.serverConfig = config.getServerConfig(); this.objectGraphStats = new ConcurrentHashMap<>(); this.metaInfoManager = new DefaultMetaInfoManager(this); @@ -249,6 +259,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { this.persister = config.createPersister(this); this.queryEngine = config.createOrmQueryEngine(); this.relationalQueryEngine = config.createRelationalQueryEngine(); + this.dtoQueryEngine = config.createDtoQueryEngine(); this.autoTuneService = config.createAutoTuneService(this); this.readAuditPrepare = config.getReadAuditPrepare(); @@ -967,6 +978,13 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return new DefaultOrmUpdate<>(beanType, this, desc.getBaseTable(), ormUpdate); } + @Override + public DtoQuery findDto(Class dtoType, String sql) { + + DtoBeanDescriptor descriptor = dtoBeanManager.getDescriptor(dtoType); + return new DefaultDtoQuery<>(this, descriptor, sql.trim()); + } + @Override public SqlQuery createSqlQuery(String sql) { return new DefaultRelationalQuery(this, sql.trim()); @@ -1500,6 +1518,54 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { } } + @Override + public void findDtoEach(SpiDtoQuery query, Consumer consumer) { + DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + try { + request.initTransIfRequired(); + request.findEach(consumer); + + } finally { + request.endTransIfRequired(); + } + } + + @Override + public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) { + DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + try { + request.initTransIfRequired(); + request.findEachWhile(consumer); + + } finally { + request.endTransIfRequired(); + } + } + + @Override + public List findDtoList(SpiDtoQuery query) { + DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + try { + request.initTransIfRequired(); + return request.findList(); + + } finally { + request.endTransIfRequired(); + } + } + + @Override + public T findDtoOne(SpiDtoQuery query) { + DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + try { + request.initTransIfRequired(); + return extractUnique(request.findList()); + + } finally { + request.endTransIfRequired(); + } + } + /** * Persist the bean by either performing an insert or update. */ @@ -2188,4 +2254,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return null; } + public List collectQueryStatistics(boolean reset) { + return dtoBeanManager.collectStats(reset); + } } diff --git a/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java new file mode 100644 index 000000000..b0801eb2e --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java @@ -0,0 +1,103 @@ +package io.ebeaninternal.server.core; + +import io.ebeaninternal.api.SpiDtoQuery; +import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.server.dto.DtoColumn; +import io.ebeaninternal.server.dto.DtoMappingRequest; +import io.ebeaninternal.server.dto.DtoQueryPlan; +import io.ebeaninternal.server.query.dto.DtoQueryEngine; +import io.ebeaninternal.server.type.DataReader; +import io.ebeaninternal.server.type.RsetDataReader; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Wraps the objects involved in executing a DtoQuery. + */ +public final class DtoQueryRequest extends AbstractSqlQueryRequest { + + private final SpiDtoQuery query; + + private final DtoQueryEngine queryEngine; + + private DtoQueryPlan plan; + + private DataReader dataReader; + + private int beanCount; + + DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query) { + super(server, query, null); + this.queryEngine = engine; + this.query = query; + } + + @Override + protected void setResultSet(ResultSet resultSet) throws SQLException { + this.resultSet = resultSet; + this.dataReader = new RsetDataReader(ebeanServer.getDataTimeZone(), resultSet); + obtainPlan(); + } + + private void obtainPlan() throws SQLException { + String planKey = query.planKey(); + plan = query.getQueryPlan(planKey); + if (plan == null) { + plan = query.buildPlan(mappingRequest()); + query.putQueryPlan(planKey, plan); + } + } + + @Override + protected void requestComplete() { + if (plan != null) { + long exeMicros = (System.nanoTime() - startNano) / 1000L; + plan.collect(exeMicros, beanCount); + } + } + + public void findEach(Consumer consumer) { + queryEngine.findEach(this, consumer); + } + + public void findEachWhile(Predicate consumer) { + queryEngine.findEachWhile(this, consumer); + } + + public List findList() { + return queryEngine.findList(this); + } + + @SuppressWarnings("unchecked") + public T readNextBean() throws SQLException { + beanCount++; + dataReader.resetColumnPosition(); + return (T)plan.readRow(dataReader); + } + + private DtoMappingRequest mappingRequest() throws SQLException { + return new DtoMappingRequest(query, sql, readMeta()); + } + + private DtoColumn[] readMeta() throws SQLException { + + ResultSetMetaData metaData = resultSet.getMetaData(); + int cols = metaData.getColumnCount(); + DtoColumn[] meta = new DtoColumn[cols]; + for (int i = 0; i < cols; i++) { + int pos = i+1; + String columnLabel = metaData.getColumnLabel(pos); + if (columnLabel == null) { + columnLabel = metaData.getColumnName(pos); + } + meta[i] = new DtoColumn(columnLabel); + } + return meta; + } + +} diff --git a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index a65dd2194..dc835fb6c 100644 --- a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -40,6 +40,7 @@ import io.ebeaninternal.server.deploy.generatedproperty.GeneratedPropertyFactory import io.ebeaninternal.server.deploy.parse.DeployCreateProperties; import io.ebeaninternal.server.deploy.parse.DeployInherit; import io.ebeaninternal.server.deploy.parse.DeployUtil; +import io.ebeaninternal.server.dto.DtoBeanManager; import io.ebeaninternal.server.expression.DefaultExpressionFactory; import io.ebeaninternal.server.persist.Binder; import io.ebeaninternal.server.persist.DefaultPersister; @@ -48,6 +49,7 @@ import io.ebeaninternal.server.persist.platform.PostgresMultiValueBind; import io.ebeaninternal.server.query.CQueryEngine; import io.ebeaninternal.server.query.DefaultOrmQueryEngine; import io.ebeaninternal.server.query.DefaultRelationalQueryEngine; +import io.ebeaninternal.server.query.dto.DtoQueryEngine; import io.ebeaninternal.server.readaudit.DefaultReadAuditLogger; import io.ebeaninternal.server.readaudit.DefaultReadAuditPrepare; import io.ebeaninternal.server.text.json.DJsonContext; @@ -97,6 +99,8 @@ public class InternalConfiguration { private final TypeManager typeManager; + private final DtoBeanManager dtoBeanManager; + private final DataTimeZone dataTimeZone; private final Binder binder; @@ -149,6 +153,7 @@ public class InternalConfiguration { this.deployCreateProperties = new DeployCreateProperties(typeManager); this.deployUtil = new DeployUtil(typeManager, serverConfig); + this.dtoBeanManager = new DtoBeanManager(typeManager); this.beanDescriptorManager = new BeanDescriptorManager(this); Map asOfTableMapping = beanDescriptorManager.deploy(); @@ -294,8 +299,12 @@ public class InternalConfiguration { return AutoTuneServiceFactory.create(server, serverConfig); } + public DtoQueryEngine createDtoQueryEngine() { + return new DtoQueryEngine(binder); + } + public RelationalQueryEngine createRelationalQueryEngine() { - return new DefaultRelationalQueryEngine(binder, serverConfig.getDatabaseBooleanTrue()); + return new DefaultRelationalQueryEngine(binder, serverConfig.getDatabaseBooleanTrue(), serverConfig.getDbTypeConfig().getDbUuid().useBinaryOptimized()); } public OrmQueryEngine createOrmQueryEngine() { @@ -435,7 +444,7 @@ public class InternalConfiguration { /** * Create the TransactionScopeManager taking into account JTA or external transaction manager. */ - public TransactionScopeManager createTransactionScopeManager() { + private TransactionScopeManager createTransactionScopeManager() { ExternalTransactionManager externalTransactionManager = serverConfig.getExternalTransactionManager(); if (externalTransactionManager == null && serverConfig.isUseJtaTransactionManager()) { @@ -505,4 +514,8 @@ public class InternalConfiguration { public MultiValueBind getMultiValueBind() { return multiValueBind; } + + public DtoBeanManager getDtoBeanManager() { + return dtoBeanManager; + } } diff --git a/src/main/java/io/ebeaninternal/server/core/RelationalQueryEngine.java b/src/main/java/io/ebeaninternal/server/core/RelationalQueryEngine.java index b0f01d8cb..ae4dde8f1 100644 --- a/src/main/java/io/ebeaninternal/server/core/RelationalQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/core/RelationalQueryEngine.java @@ -9,6 +9,11 @@ import java.util.function.Predicate; public interface RelationalQueryEngine { + /** + * Return a new SqlRow with appropriate mapping for DB true and optimised binary UUID etc. + */ + SqlRow createSqlRow(int estimateCapacity); + /** * Find a list of beans using relational query. */ diff --git a/src/main/java/io/ebeaninternal/server/core/RelationalQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/RelationalQueryRequest.java index 7f8ab2e8a..81ae0aad9 100644 --- a/src/main/java/io/ebeaninternal/server/core/RelationalQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/RelationalQueryRequest.java @@ -1,24 +1,11 @@ package io.ebeaninternal.server.core; -import io.ebean.EbeanServer; import io.ebean.SqlQuery; import io.ebean.SqlRow; import io.ebean.Transaction; -import io.ebean.util.JdbcClose; -import io.ebean.config.ServerConfig; -import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.SpiEbeanServer; -import io.ebeaninternal.api.SpiSqlQuery; -import io.ebeaninternal.api.SpiTransaction; -import io.ebeaninternal.server.lib.util.Str; -import io.ebeaninternal.server.persist.Binder; -import io.ebeaninternal.server.persist.TrimLogSql; -import io.ebeaninternal.server.query.DefaultSqlRow; -import io.ebeaninternal.server.transaction.TransactionManager; -import io.ebeaninternal.server.util.BindParamsParser; +import io.ebeaninternal.api.SpiSqlBinding; -import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -30,63 +17,36 @@ import java.util.function.Predicate; /** * Wraps the objects involved in executing a SqlQuery. */ -public final class RelationalQueryRequest { - - private final SpiSqlQuery query; +public final class RelationalQueryRequest extends AbstractSqlQueryRequest { private final RelationalQueryEngine queryEngine; - private final SpiEbeanServer ebeanServer; - - private SpiTransaction trans; - - private boolean createdTransaction; - - private String sql; - - private ResultSet resultSet; - - private int rowCount; - - private String bindLog = ""; - private String[] propertyNames; private int estimateCapacity; - private PreparedStatement pstmt; + private int rows; /** * Create the BeanFindRequest. */ RelationalQueryRequest(SpiEbeanServer server, RelationalQueryEngine engine, SqlQuery q, Transaction t) { - this.ebeanServer = server; + super(server, (SpiSqlBinding) q, t); this.queryEngine = engine; - this.query = (SpiSqlQuery) q; - this.trans = (SpiTransaction) t; } - /** - * Create a transaction if none currently exists. - */ - public void initTransIfRequired() { - if (trans == null) { - trans = ebeanServer.currentServerTransaction(); - if (trans == null || !trans.isActive()) { - // create a local readOnly transaction - trans = ebeanServer.beginServerTransaction(); - createdTransaction = true; - } - } + @Override + protected void setResultSet(ResultSet resultSet) throws SQLException { + this.resultSet = resultSet; + this.propertyNames = getPropertyNames(); + // calculate the initialCapacity of the Map to reduce rehashing + float initCap = (propertyNames.length) / 0.7f; + this.estimateCapacity = (int) initCap + 1; } - /** - * End the transaction if it was locally created. - */ - public void endTransIfRequired() { - if (createdTransaction) { - ebeanServer.commitTransaction(); - } + @Override + protected void requestComplete() { + } public void findEach(Consumer consumer) { @@ -101,37 +61,6 @@ public final class RelationalQueryRequest { return queryEngine.findList(this); } - /** - * Return the find that is to be performed. - */ - public SpiSqlQuery getQuery() { - return query; - } - - public EbeanServer getEbeanServer() { - return ebeanServer; - } - - public SpiTransaction getTransaction() { - return trans; - } - - public boolean isLogSql() { - return trans.isLogSql(); - } - - public boolean isLogSummary() { - return trans.isLogSummary(); - } - - private void setResultSet(ResultSet resultSet) throws SQLException { - this.resultSet = resultSet; - this.propertyNames = getPropertyNames(); - // calculate the initialCapacity of the Map to reduce rehashing - float initCap = (propertyNames.length) / 0.7f; - this.estimateCapacity = (int) initCap + 1; - } - /** * Build the list of property names. */ @@ -147,36 +76,14 @@ public final class RelationalQueryRequest { return propNames.toArray(new String[propNames.size()]); } - /** - * Return the bindLog for this request. - */ - public String getBindLog() { - return bindLog; - } - - /** - * Return true if we can navigate to the next row. - */ - public boolean next() throws SQLException { - rowCount++; - return resultSet.next(); - } - - /** - * Close the underlying resources. - */ - public void close() { - JdbcClose.close(resultSet); - JdbcClose.close(pstmt); - } - /** * Read and return the next SqlRow. */ - public SqlRow createNewRow(String dbTrueValue) throws SQLException { - ServerConfig.DbUuid dbUuid = ebeanServer.getServerConfig().getDbTypeConfig().getDbUuid(); - SqlRow sqlRow = new DefaultSqlRow(estimateCapacity, 0.75f, dbTrueValue, dbUuid.useBinaryOptimized()); + public SqlRow createNewRow() throws SQLException { + rows++; + + SqlRow sqlRow = queryEngine.createSqlRow(estimateCapacity); int index = 0; for (String propertyName : propertyNames) { index++; @@ -186,76 +93,11 @@ public final class RelationalQueryRequest { return sqlRow; } - /** - * Prepare the SQL taking into account named bind parameters. - */ - private void prepareSql() { - - String sql = query.getQuery(); - BindParams bindParams = query.getBindParams(); - if (!bindParams.isEmpty()) { - // convert any named parameters if required - sql = BindParamsParser.parse(bindParams, sql); + public void logSummary() { + if (trans.isLogSummary()) { + long micros = (System.nanoTime() - startNano) / 1000L; + trans.logSummary("SqlQuery rows[" + rows + "] micros[" + micros + "] bind[" + bindLog + "]"); } - this.sql = limitOffset(sql); } - private String limitOffset(String sql) { - - int firstRow = query.getFirstRow(); - int maxRows = query.getMaxRows(); - if (firstRow > 0 || maxRows > 0) { - return ebeanServer.getDatabasePlatform().getBasicSqlLimiter().limit(sql, firstRow, maxRows); - } - return sql; - } - - /** - * Prepare and execute the SQL using the Binder. - */ - public void executeSql(Binder binder) throws SQLException { - - prepareSql(); - - Connection conn = trans.getInternalConnection(); - - // synchronise for query.cancel() support - pstmt = conn.prepareStatement(sql); - if (query.getTimeout() > 0) { - pstmt.setQueryTimeout(query.getTimeout()); - } - if (query.getBufferFetchSizeHint() > 0) { - pstmt.setFetchSize(query.getBufferFetchSizeHint()); - } - - BindParams bindParams = query.getBindParams(); - if (!bindParams.isEmpty()) { - this.bindLog = binder.bind(bindParams, pstmt, conn); - } - - if (isLogSql()) { - String logSql = TrimLogSql.trim(sql); - if (TransactionManager.SQL_LOGGER.isTraceEnabled()) { - logSql = Str.add(logSql, "; --bind(", bindLog, ")"); - } - trans.logSql(logSql); - } - - setResultSet(pstmt.executeQuery()); - - } - - /** - * Return the SQL executed for this query. - */ - public String getSql() { - return sql; - } - - /** - * Return the rows read. - */ - public int getRowCount() { - return rowCount - 1; - } } diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoBeanDescriptor.java b/src/main/java/io/ebeaninternal/server/dto/DtoBeanDescriptor.java new file mode 100644 index 000000000..6d7698857 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoBeanDescriptor.java @@ -0,0 +1,45 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.metric.QueryPlanCollector; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages the query plans for a given DTO bean type. + */ +public class DtoBeanDescriptor { + + private final Map plans = new ConcurrentHashMap<>(); + + private final Class dtoType; + + private final DtoMeta meta; + + DtoBeanDescriptor(Class dtoType, DtoMeta meta) { + this.dtoType = dtoType; + this.meta = meta; + } + + public Class getType() { + return dtoType; + } + + public DtoQueryPlan getQueryPlan(String planKey) { + return plans.get(planKey); + } + + public DtoQueryPlan buildPlan(DtoMappingRequest request) { + return meta.match(request); + } + + public void putQueryPlan(String planKey, DtoQueryPlan plan) { + plans.put(planKey, plan); + } + + public void collectStats(QueryPlanCollector collector) { + for (DtoQueryPlan plan : plans.values()) { + plan.collectStats(collector); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoBeanManager.java b/src/main/java/io/ebeaninternal/server/dto/DtoBeanManager.java new file mode 100644 index 000000000..87e58a453 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoBeanManager.java @@ -0,0 +1,53 @@ +package io.ebeaninternal.server.dto; + +import io.ebean.meta.MetaQueryMetric; +import io.ebeaninternal.metric.MetricFactory; +import io.ebeaninternal.metric.QueryPlanCollector; +import io.ebeaninternal.server.type.TypeManager; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages all the DTO bean descriptors. + */ +public class DtoBeanManager { + + private final TypeManager typeManager; + + private final Map descriptorMap = new ConcurrentHashMap<>(); + + public DtoBeanManager(TypeManager typeManager) { + this.typeManager = typeManager; + } + + /** + * Return the descriptor for the given DTO bean class. + */ + @SuppressWarnings("unchecked") + public DtoBeanDescriptor getDescriptor(Class dtoType) { + + return descriptorMap.computeIfAbsent(dtoType, this::createDescriptor); + } + + private DtoBeanDescriptor createDescriptor(Class dtoType) { + + try { + DtoMeta meta = new DtoMetaBuilder(dtoType, typeManager).build(); + return new DtoBeanDescriptor<>(dtoType, meta); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + public List collectStats(boolean reset) { + + QueryPlanCollector collector = MetricFactory.get().createCollector(reset); + + for (DtoBeanDescriptor value : descriptorMap.values()) { + value.collectStats(collector); + } + return collector.complete(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoColumn.java b/src/main/java/io/ebeaninternal/server/dto/DtoColumn.java new file mode 100644 index 000000000..187caf97f --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoColumn.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.dto; + +/** + * A column in the resultSet that we want to map to a bean property. + */ +public class DtoColumn { + + private final String label; + + public DtoColumn(String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + + @Override + public String toString() { + return label; + } + +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoMappingRequest.java b/src/main/java/io/ebeaninternal/server/dto/DtoMappingRequest.java new file mode 100644 index 000000000..d781e35c0 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoMappingRequest.java @@ -0,0 +1,49 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.api.SpiDtoQuery; +import io.ebeaninternal.metric.MetricFactory; +import io.ebeaninternal.metric.QueryPlanMetric; + +/** + * Request to map a resultSet columns for a query into a DTO bean. + */ +public class DtoMappingRequest { + + private final Class type; + + private final String label; + + private final String sql; + + private final boolean relaxedMode; + + private final DtoColumn[] columnMeta; + + public DtoMappingRequest(SpiDtoQuery query, String sql, DtoColumn[] columnMeta) { + this.type = query.getType(); + this.label = query.getLabel(); + this.sql = sql; + this.relaxedMode = query.isRelaxedMode(); + this.columnMeta = columnMeta; + } + + public DtoColumn[] getColumnMeta() { + return columnMeta; + } + + public boolean isRelaxedMode() { + return relaxedMode; + } + + public String getLabel() { + return label; + } + + public String getSql() { + return sql; + } + + public QueryPlanMetric createMetric() { + return MetricFactory.get().createQueryPlanMetric(type, label, sql); + } +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoMeta.java b/src/main/java/io/ebeaninternal/server/dto/DtoMeta.java new file mode 100644 index 000000000..272e962b5 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoMeta.java @@ -0,0 +1,119 @@ +package io.ebeaninternal.server.dto; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Holds property and constructor meta data for a given DTO bean type. + * + * Uses this to map a mapping request (columns) to a 'query plan' (constructor and setters). + */ +class DtoMeta { + + private final Class dtoType; + private final Map propMap = new LinkedHashMap<>(); + + private final Map constructorMap = new LinkedHashMap<>(); + + private final DtoMetaConstructor defaultConstructor; + private final DtoMetaConstructor maxArgConstructor; + + DtoMeta(Class dtoType, List constructors, List properties) { + this.dtoType = dtoType; + + for (DtoMetaProperty property : properties) { + propMap.put(property.getName().toUpperCase(), property); + } + + int maxArg = 0; + + DtoMetaConstructor defaultConstructor = null; + DtoMetaConstructor maxArgConstructor = null; + + for (DtoMetaConstructor constructor : constructors) { + int args = constructor.getArgCount(); + constructorMap.put(args, constructor); + if (args == 0) { + defaultConstructor = constructor; + } else if (args > maxArg) { + maxArgConstructor = constructor; + maxArg = args; + } + } + this.defaultConstructor = defaultConstructor; + this.maxArgConstructor = maxArgConstructor; + } + + public DtoQueryPlan match(DtoMappingRequest request) { + + DtoColumn[] cols = request.getColumnMeta(); + + int colLen = cols.length; + DtoMetaConstructor constructor = constructorMap.get(colLen); + if (constructor != null) { + return new DtoQueryPlanConstructor(request, constructor); + } + if (maxArgConstructor != null && colLen > maxArgConstructor.getArgCount()) { + // maxArgConst + setters + return matchMaxArgPlusSetters(request); + } + if (defaultConstructor != null) { + return matchSetters(request); + } + + String msg = "Unable to map the resultSet columns " + Arrays.toString(cols) + + " to the bean type ["+dtoType+"] as the number of columns in the resultSet is less than the constructor" + + " (and that there is no default constructor) ?"; + throw new IllegalStateException(msg); + } + + private DtoQueryPlanConPlus matchMaxArgPlusSetters(DtoMappingRequest request) { + + + int firstOnes = maxArgConstructor.getArgCount(); + + DtoColumn[] cols = request.getColumnMeta(); + + DtoReadSet[] setterProps = new DtoReadSet[cols.length - firstOnes]; + + int pos = 0; + for (int i = firstOnes; i < cols.length; i++) { + String label = cols[i].getLabel(); + DtoReadSet property = propMap.get(label.toUpperCase()); + if (property == null || property.isReadOnly()) { + if (request.isRelaxedMode()) { + property = DtoReadSetColumnSkip.INSTANCE; + } else { + throw new IllegalStateException("Unable to map DB column " + cols[i] + " to a property with a setter method on " + dtoType); + } + } + setterProps[pos++] = property; + } + + return new DtoQueryPlanConPlus(request, maxArgConstructor, setterProps); + } + + private DtoQueryPlan matchSetters(DtoMappingRequest request) { + + DtoColumn[] cols = request.getColumnMeta(); + + DtoReadSet[] setterProps = new DtoReadSet[cols.length]; + + for (int i = 0; i < cols.length; i++) { + String label = cols[i].getLabel(); + DtoReadSet property = propMap.get(label.toUpperCase()); + if (property == null || property.isReadOnly()) { + if (request.isRelaxedMode()) { + property = DtoReadSetColumnSkip.INSTANCE; + } else { + throw new IllegalStateException("Unable to map DB column " + cols[i] + " to a property with a setter method on " + dtoType); + } + } + setterProps[i] = property; + } + + return new DtoQueryPlanConSetter(request, defaultConstructor, setterProps); + } +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java b/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java new file mode 100644 index 000000000..d1c249f85 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java @@ -0,0 +1,77 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.server.type.TypeManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +/** + * Build the DtoMeta for a bean. + *

+ * Use TypeManager to map bean property types to ScalarTypes. + */ +class DtoMetaBuilder { + + private static final Logger log = LoggerFactory.getLogger(DtoMetaBuilder.class); + + private final TypeManager typeManager; + + private final Class dtoType; + + private final List properties = new ArrayList<>(); + + private final List constructorList = new ArrayList<>(); + + DtoMetaBuilder(Class dtoType, TypeManager typeManager) { + this.dtoType = dtoType; + this.typeManager = typeManager; + } + + public DtoMeta build() throws IntrospectionException { + + readConstructors(); + readProperties(); + + return new DtoMeta(dtoType, constructorList, properties); + } + + private void readProperties() throws IntrospectionException { + + BeanInfo beanInfo = Introspector.getBeanInfo(dtoType); + for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) { + if (include(propertyDescriptor)) { + try { + properties.add(new DtoMetaProperty(typeManager, propertyDescriptor, dtoType)); + } catch (Exception e) { + log.debug("exclude on " + dtoType + " property " + propertyDescriptor.getName(), e); + } + } + } + } + + private void readConstructors() { + + Constructor[] constructors = dtoType.getConstructors(); + + for (Constructor constructor : constructors) { + try { + constructorList.add(new DtoMetaConstructor(typeManager, constructor, dtoType)); + } catch (Exception e) { + // we don't want that constructor + log.debug("exclude on " + dtoType + " constructor " + constructor, e); + } + } + } + + private boolean include(PropertyDescriptor property) { + return !property.getName().equals("class"); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoMetaConstructor.java b/src/main/java/io/ebeaninternal/server/dto/DtoMetaConstructor.java new file mode 100644 index 000000000..05aec36b6 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoMetaConstructor.java @@ -0,0 +1,67 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.server.type.DataReader; +import io.ebeaninternal.server.type.ScalarType; +import io.ebeaninternal.server.type.TypeManager; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.sql.SQLException; + +class DtoMetaConstructor { + + private final Class[] types; + private final MethodHandle handle; + private final ScalarType[] scalarTypes; + + DtoMetaConstructor(TypeManager typeManager, Constructor constructor, Class someClass) throws NoSuchMethodException, IllegalAccessException { + + this.types = constructor.getParameterTypes(); + this.scalarTypes = new ScalarType[types.length]; + for (int i = 0; i < types.length; i++) { + scalarTypes[i] = typeManager.getScalarType(types[i]); + } + + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + this.handle = lookup.findConstructor(someClass, typeFor(types)); + } + + private MethodType typeFor(Class[] types) { + return MethodType.methodType(void.class, types); + } + + Class[] getTypes() { + return types; + } + + int getArgCount() { + return types.length; + } + + Object defaultConstructor() { + try { + return handle.invokeWithArguments(); + } catch (Throwable e) { + throw new RuntimeException("Unexpected error invoking constructor", e); + } + } + + public Object process(DataReader dataReader) throws SQLException { + Object[] values = new Object[scalarTypes.length]; + for (int i = 0; i < scalarTypes.length; i++) { + values[i] = scalarTypes[i].read(dataReader); + } + return invoke(values); + } + + private Object invoke(Object... args) { + try { + return handle.invokeWithArguments(args); + } catch (Throwable e) { + throw new RuntimeException("Unexpected error invoking constructor", e); + } + } + +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java b/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java new file mode 100644 index 000000000..3b672b100 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java @@ -0,0 +1,68 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.server.type.DataReader; +import io.ebeaninternal.server.type.ScalarType; +import io.ebeaninternal.server.type.TypeManager; + +import java.beans.PropertyDescriptor; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.sql.SQLException; + +class DtoMetaProperty implements DtoReadSet { + + private final Class dtoType; + private final String name; + private final MethodHandle setter; + private final ScalarType scalarType; + + DtoMetaProperty(TypeManager typeManager, PropertyDescriptor descriptor, Class dtoType) throws IllegalAccessException, NoSuchMethodException { + + this.dtoType = dtoType; + this.name = descriptor.getName(); + + Method writeMethod = descriptor.getWriteMethod(); + if (writeMethod != null) { + + Class propertyType = descriptor.getPropertyType(); + + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + this.setter = lookup.findVirtual(dtoType, writeMethod.getName(), MethodType.methodType(void.class, propertyType)); + this.scalarType = typeManager.getScalarType(propertyType); + + } else { + this.scalarType = null; + this.setter = null; + } + } + + String getName() { + return name; + } + + @Override + public boolean isReadOnly() { + return scalarType == null; + } + + @Override + public void readSet(Object bean, DataReader dataReader) throws SQLException { + Object value = scalarType.read(dataReader); + invoke(bean, value); + } + + private void invoke(Object instance, Object arg) { + try { + setter.invoke(instance, arg); + } catch (Throwable e) { + throw new RuntimeException("Error calling setter for property " + fullname() + " with arg: " + arg, e); + } + } + + private String fullname() { + return dtoType.getName() + "." + name; + } + +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlan.java b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlan.java new file mode 100644 index 000000000..36d49c351 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlan.java @@ -0,0 +1,27 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.metric.QueryPlanCollector; +import io.ebeaninternal.server.type.DataReader; + +import java.sql.SQLException; + +/** + * Knows how to read and map rows into a Bean. + */ +public interface DtoQueryPlan { + + /** + * Read the row data and return the DTO bean. + */ + Object readRow(DataReader dataReader) throws SQLException; + + /** + * Add an event to the query execution statistics. + */ + void collect(long exeMicros, int rows); + + /** + * Collect the query plan statistics. + */ + void collectStats(QueryPlanCollector collector); +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanBase.java b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanBase.java new file mode 100644 index 000000000..b4a867a50 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanBase.java @@ -0,0 +1,28 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.metric.QueryPlanCollector; +import io.ebeaninternal.metric.QueryPlanMetric; +import io.ebeaninternal.metric.TimedMetric; + +abstract class DtoQueryPlanBase implements DtoQueryPlan { + + private final QueryPlanMetric planMetric; + + private final TimedMetric metric; + + DtoQueryPlanBase(DtoMappingRequest request) { + this.planMetric = request.createMetric(); + this.metric = planMetric.getMetric(); + } + + @Override + public void collect(long exeTime, int rows) { + metric.add(exeTime, rows); + } + + @Override + public void collectStats(QueryPlanCollector collector) { + planMetric.collect(collector); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConPlus.java b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConPlus.java new file mode 100644 index 000000000..a2d188ab0 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConPlus.java @@ -0,0 +1,32 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.server.type.DataReader; + +import java.sql.SQLException; + +/** + * Plan based on Constructor plus some setter methods. + */ +class DtoQueryPlanConPlus extends DtoQueryPlanBase { + + private final DtoMetaConstructor maxArgConstructor; + + private final DtoReadSet[] setterProps; + + DtoQueryPlanConPlus(DtoMappingRequest request, DtoMetaConstructor maxArgConstructor, DtoReadSet[] setterProps) { + super(request); + this.maxArgConstructor = maxArgConstructor; + this.setterProps = setterProps; + } + + @Override + public Object readRow(DataReader dataReader) throws SQLException { + + Object bean = maxArgConstructor.process(dataReader); + for (DtoReadSet setterProp : setterProps) { + setterProp.readSet(bean, dataReader); + } + return bean; + } + +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConSetter.java b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConSetter.java new file mode 100644 index 000000000..16d6075f0 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConSetter.java @@ -0,0 +1,32 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.server.type.DataReader; + +import java.sql.SQLException; + +/** + * Plan based on default constructor and setter methods. + */ +class DtoQueryPlanConSetter extends DtoQueryPlanBase { + + private final DtoMetaConstructor defaultConstructor; + + private final DtoReadSet[] setterProps; + + DtoQueryPlanConSetter(DtoMappingRequest request, DtoMetaConstructor defaultConstructor, DtoReadSet[] setterProps) { + super(request); + this.defaultConstructor = defaultConstructor; + this.setterProps = setterProps; + } + + @Override + public Object readRow(DataReader dataReader) throws SQLException { + + Object bean = defaultConstructor.defaultConstructor(); + for (DtoReadSet setterProp : setterProps) { + setterProp.readSet(bean, dataReader); + } + return bean; + } + +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConstructor.java b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConstructor.java new file mode 100644 index 000000000..1626a18a4 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoQueryPlanConstructor.java @@ -0,0 +1,24 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.server.type.DataReader; + +import java.sql.SQLException; + +/** + * Plan based on mapping via single constructor only. + */ +class DtoQueryPlanConstructor extends DtoQueryPlanBase { + + private final DtoMetaConstructor constructor; + + DtoQueryPlanConstructor(DtoMappingRequest request, DtoMetaConstructor constructor) { + super(request); + this.constructor = constructor; + } + + @Override + public Object readRow(DataReader dataReader) throws SQLException { + return constructor.process(dataReader); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoReadSet.java b/src/main/java/io/ebeaninternal/server/dto/DtoReadSet.java new file mode 100644 index 000000000..37c701a07 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoReadSet.java @@ -0,0 +1,21 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.server.type.DataReader; + +import java.sql.SQLException; + +/** + * Read and set a property value. + */ +public interface DtoReadSet { + + /** + * Read the value from the dataReader and set it to the bean. + */ + void readSet(Object bean, DataReader dataReader) throws SQLException; + + /** + * Return true if this maps to a read only property (no setter method). + */ + boolean isReadOnly(); +} diff --git a/src/main/java/io/ebeaninternal/server/dto/DtoReadSetColumnSkip.java b/src/main/java/io/ebeaninternal/server/dto/DtoReadSetColumnSkip.java new file mode 100644 index 000000000..013aa4045 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/dto/DtoReadSetColumnSkip.java @@ -0,0 +1,21 @@ +package io.ebeaninternal.server.dto; + +import io.ebeaninternal.server.type.DataReader; + +/** + * Placeholder to skip reading a column that isn't mapped to a bean property. + */ +class DtoReadSetColumnSkip implements DtoReadSet { + + static final DtoReadSet INSTANCE = new DtoReadSetColumnSkip(); + + @Override + public void readSet(Object bean, DataReader dataReader) { + dataReader.incrementPos(1); + } + + @Override + public boolean isReadOnly() { + return false; + } +} diff --git a/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java b/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java index c2f127d0a..8eae9626c 100644 --- a/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java +++ b/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java @@ -1,6 +1,8 @@ package io.ebeaninternal.server.profile; import io.ebeaninternal.metric.MetricFactory; +import io.ebeaninternal.metric.QueryPlanCollector; +import io.ebeaninternal.metric.QueryPlanMetric; import io.ebeaninternal.metric.TimedMetric; import io.ebeaninternal.metric.TimedMetricMap; @@ -18,4 +20,14 @@ public class DMetricFactory implements MetricFactory { public TimedMetric createTimedMetric(String name) { return new DTimedMetric(name); } + + @Override + public QueryPlanMetric createQueryPlanMetric(Class type, String label, String sql) { + return new DQueryPlanMetric(new DQueryPlanMeta(type, label, sql), createTimedMetric(label)); + } + + @Override + public QueryPlanCollector createCollector(boolean reset) { + return new DQueryPlanCollector(reset); + } } diff --git a/src/main/java/io/ebeaninternal/server/profile/DQueryPlanCollector.java b/src/main/java/io/ebeaninternal/server/profile/DQueryPlanCollector.java new file mode 100644 index 000000000..a3903ec1f --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/profile/DQueryPlanCollector.java @@ -0,0 +1,33 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.meta.MetaQueryMetric; +import io.ebeaninternal.metric.QueryPlanCollector; + +import java.util.ArrayList; +import java.util.List; + +class DQueryPlanCollector implements QueryPlanCollector { + + private final boolean reset; + + private final List list = new ArrayList<>(); + + DQueryPlanCollector(boolean reset) { + this.reset = reset; + } + + @Override + public boolean isReset() { + return reset; + } + + @Override + public void add(MetaQueryMetric stats) { + list.add(stats); + } + + @Override + public List complete() { + return list; + } +} diff --git a/src/main/java/io/ebeaninternal/server/profile/DQueryPlanMeta.java b/src/main/java/io/ebeaninternal/server/profile/DQueryPlanMeta.java new file mode 100644 index 000000000..02c6859c4 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/profile/DQueryPlanMeta.java @@ -0,0 +1,31 @@ +package io.ebeaninternal.server.profile; + +class DQueryPlanMeta { + + private final Class type; + private final String label; + private final String sql; + + DQueryPlanMeta(Class type, String label, String sql) { + this.type = type; + this.label = label; + this.sql = sql; + } + + public Class getType() { + return type; + } + + public String getLabel() { + return label; + } + + public String getSql() { + return sql; + } + + @Override + public String toString() { + return "type:" + type + " label:" + label; + } +} diff --git a/src/main/java/io/ebeaninternal/server/profile/DQueryPlanMetric.java b/src/main/java/io/ebeaninternal/server/profile/DQueryPlanMetric.java new file mode 100644 index 000000000..4ba29d8c3 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/profile/DQueryPlanMetric.java @@ -0,0 +1,102 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.meta.MetaQueryMetric; +import io.ebeaninternal.metric.QueryPlanCollector; +import io.ebeaninternal.metric.QueryPlanMetric; +import io.ebeaninternal.metric.TimedMetric; +import io.ebeaninternal.metric.TimedMetricStats; + +class DQueryPlanMetric implements QueryPlanMetric { + + private final DQueryPlanMeta meta; + private final TimedMetric metric; + + DQueryPlanMetric(DQueryPlanMeta meta, TimedMetric metric) { + this.meta = meta; + this.metric = metric; + } + + @Override + public void collect(QueryPlanCollector collector) { + TimedMetricStats stats = metric.collect(collector.isReset()); + if (stats != null) { + collector.add(new Stats(meta, stats)); + } + } + + @Override + public TimedMetric getMetric() { + return metric; + } + + private static class Stats implements MetaQueryMetric { + + private final DQueryPlanMeta meta; + private final TimedMetricStats stats; + + private Stats(DQueryPlanMeta meta, TimedMetricStats stats) { + this.meta = meta; + this.stats = stats; + } + + @Override + public String toString() { + return meta +" "+ stats; + } + + @Override + public Class getType() { + return meta.getType(); + } + + @Override + public String getLabel() { + return meta.getLabel(); + } + + @Override + public String getSql() { + return meta.getSql(); + } + + @Override + public String getName() { + return stats.getName(); + } + + @Override + public String getLocation() { + return stats.getLocation(); + } + + @Override + public long getStartTime() { + return stats.getStartTime(); + } + + @Override + public long getCount() { + return stats.getCount(); + } + + @Override + public long getTotal() { + return stats.getTotal(); + } + + @Override + public long getMax() { + return stats.getMax(); + } + + @Override + public long getMean() { + return stats.getMean(); + } + + @Override + public long getBeanCount() { + return stats.getBeanCount(); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/profile/DTimeMetricStats.java b/src/main/java/io/ebeaninternal/server/profile/DTimeMetricStats.java index a30c41ff4..1a9d61f8f 100644 --- a/src/main/java/io/ebeaninternal/server/profile/DTimeMetricStats.java +++ b/src/main/java/io/ebeaninternal/server/profile/DTimeMetricStats.java @@ -19,7 +19,9 @@ class DTimeMetricStats implements TimedMetricStats { private final long max; - DTimeMetricStats(String name, long collectionStart, long count, long total, long max) { + private final long beanCount; + + DTimeMetricStats(String name, long collectionStart, long count, long total, long max, long beanCount) { this.name = name; this.startTime = collectionStart; this.count = count; @@ -27,6 +29,7 @@ class DTimeMetricStats implements TimedMetricStats { // collection is racy so sanitize the max value if it has not been set // this most likely would happen when count = 1 so max = mean this.max = max != Long.MIN_VALUE ? max : (count < 1 ? 0 : Math.round(total / count)); + this.beanCount = beanCount; } @Override @@ -40,7 +43,8 @@ class DTimeMetricStats implements TimedMetricStats { } sb.append("count:").append(count) .append(" total:").append(total) - .append(" max:").append(max); + .append(" max:").append(max) + .append(" beanCount:").append(beanCount); return sb.toString(); } @@ -99,4 +103,8 @@ class DTimeMetricStats implements TimedMetricStats { return (count < 1) ? 0L : Math.round((double)(total / count)); } + @Override + public long getBeanCount() { + return beanCount; + } } diff --git a/src/main/java/io/ebeaninternal/server/profile/DTimedMetric.java b/src/main/java/io/ebeaninternal/server/profile/DTimedMetric.java index 4ba32caa7..42a7f682c 100644 --- a/src/main/java/io/ebeaninternal/server/profile/DTimedMetric.java +++ b/src/main/java/io/ebeaninternal/server/profile/DTimedMetric.java @@ -16,15 +16,17 @@ import java.util.concurrent.atomic.LongAdder; */ class DTimedMetric implements TimedMetric { - protected final String name; + private final String name; - protected final LongAdder count = new LongAdder(); + private final LongAdder beanCount = new LongAdder(); - protected final LongAdder total = new LongAdder(); + private final LongAdder count = new LongAdder(); - protected final LongAccumulator max = new LongAccumulator(Math::max, Long.MIN_VALUE); + private final LongAdder total = new LongAdder(); - protected final AtomicLong startTime = new AtomicLong(System.currentTimeMillis()); + private final LongAccumulator max = new LongAccumulator(Math::max, Long.MIN_VALUE); + + private final AtomicLong startTime = new AtomicLong(System.currentTimeMillis()); DTimedMetric(String name) { this.name = name; @@ -41,6 +43,12 @@ class DTimedMetric implements TimedMetric { max.accumulate(value); } + @Override + public void add(long micros, long beans) { + add(micros); + beanCount.add(beans); + } + @Override public boolean isEmpty() { return count.sum() == 0; @@ -75,14 +83,15 @@ class DTimedMetric implements TimedMetric { if (reset) { // Note these values are not guaranteed to be consistent wrt each other // but should be reasonably consistent (small time between count and total) + final long beans = beanCount.sumThenReset(); final long maxVal = max.getThenReset(); final long totalVal = total.sumThenReset(); final long countVal = count.sumThenReset(); final long startTimeVal = startTime.getAndSet(System.currentTimeMillis()); - return new DTimeMetricStats(name, startTimeVal, countVal, totalVal, maxVal); + return new DTimeMetricStats(name, startTimeVal, countVal, totalVal, maxVal, beans); } else { - return new DTimeMetricStats(name, startTime.get(), count.sum(), total.sum(), max.get()); + return new DTimeMetricStats(name, startTime.get(), count.sum(), total.sum(), max.get(), beanCount.sum()); } } @@ -96,32 +105,4 @@ class DTimedMetric implements TimedMetric { total.reset(); } - /** - * Return the start time. - */ - public long getStartTime() { - return startTime.get(); - } - - /** - * Return the count of values. - */ - public long getCount() { - return count.sum(); - } - - /** - * Return the total of values. - */ - public long getTotal() { - return total.sum(); - } - - /** - * Return the max value. - */ - public long getMax() { - return max.get(); - } - } diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java index 7447983db..f401863a2 100644 --- a/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java @@ -22,15 +22,22 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { private final String dbTrueValue; - public DefaultRelationalQueryEngine(Binder binder, String dbTrueValue) { + private final boolean binaryOptimizedUUID; + + public DefaultRelationalQueryEngine(Binder binder, String dbTrueValue, boolean binaryOptimizedUUID) { this.binder = binder; this.dbTrueValue = dbTrueValue == null ? "true" : dbTrueValue; + this.binaryOptimizedUUID = binaryOptimizedUUID; + } + + @Override + public SqlRow createSqlRow(int estimateCapacity) { + return new DefaultSqlRow(estimateCapacity, 0.75f, dbTrueValue, binaryOptimizedUUID); } @Override public void findEach(RelationalQueryRequest request, Predicate consumer) { - long startTime = System.currentTimeMillis(); try { request.executeSql(binder); while (request.next()) { @@ -38,7 +45,7 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { break; } } - logSummary(request, startTime); + request.logSummary(); } catch (Exception e) { throw new PersistenceException(Message.msg("fetch.error", e.getMessage(), request.getSql()), e); @@ -51,14 +58,12 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { @Override public void findEach(RelationalQueryRequest request, Consumer consumer) { - long startTime = System.currentTimeMillis(); - try { request.executeSql(binder); while (request.next()) { consumer.accept(readRow(request)); } - logSummary(request, startTime); + request.logSummary(); } catch (Exception e) { throw new PersistenceException(Message.msg("fetch.error", e.getMessage(), request.getSql()), e); @@ -71,17 +76,14 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { @Override public List findList(RelationalQueryRequest request) { - long startTime = System.currentTimeMillis(); try { request.executeSql(binder); - List rows = new ArrayList<>(); while (request.next()) { rows.add(readRow(request)); } - logSummary(request, startTime); - + request.logSummary(); return rows; } catch (Exception e) { @@ -92,19 +94,11 @@ public class DefaultRelationalQueryEngine implements RelationalQueryEngine { } } - private void logSummary(RelationalQueryRequest request, long startTime) { - - if (request.isLogSummary()) { - long exeTime = System.currentTimeMillis() - startTime; - request.getTransaction().logSummary("SqlQuery rows[" + request.getRowCount() + "] time[" + exeTime + "] bind[" + request.getBindLog() + "]"); - } - } - /** * Read the row from the ResultSet and return as a MapBean. */ private SqlRow readRow(RelationalQueryRequest request) throws SQLException { - return request.createNewRow(dbTrueValue); + return request.createNewRow(); } } diff --git a/src/main/java/io/ebeaninternal/server/query/dto/DtoQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/dto/DtoQueryEngine.java new file mode 100644 index 000000000..29944d117 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/query/dto/DtoQueryEngine.java @@ -0,0 +1,66 @@ +package io.ebeaninternal.server.query.dto; + +import io.ebeaninternal.server.core.DtoQueryRequest; +import io.ebeaninternal.server.core.Message; +import io.ebeaninternal.server.persist.Binder; + +import javax.persistence.PersistenceException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class DtoQueryEngine { + + private final Binder binder; + + public DtoQueryEngine(Binder binder) { + this.binder = binder; + } + + public List findList(DtoQueryRequest request) { + try { + request.executeSql(binder); + List rows = new ArrayList<>(); + while (request.next()) { + rows.add(request.readNextBean()); + } + return rows; + + } catch (Throwable e) { + throw new PersistenceException(Message.msg("fetch.error", e.getMessage(), request.getSql()), e); + } finally { + request.close(); + } + } + + public void findEach(DtoQueryRequest request, Consumer consumer) { + try { + request.executeSql(binder); + while (request.next()) { + consumer.accept(request.readNextBean()); + } + } catch (Exception e) { + throw new PersistenceException(Message.msg("fetch.error", e.getMessage(), request.getSql()), e); + + } finally { + request.close(); + } + } + + public void findEachWhile(DtoQueryRequest request, Predicate consumer) { + try { + request.executeSql(binder); + while (request.next()) { + if (!consumer.test(request.readNextBean())) { + break; + } + } + } catch (Exception e) { + throw new PersistenceException(Message.msg("fetch.error", e.getMessage(), request.getSql()), e); + + } finally { + request.close(); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/querydefn/DefaultDtoQuery.java b/src/main/java/io/ebeaninternal/server/querydefn/DefaultDtoQuery.java new file mode 100644 index 000000000..a3def9baa --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/querydefn/DefaultDtoQuery.java @@ -0,0 +1,196 @@ +package io.ebeaninternal.server.querydefn; + +import io.ebean.DtoQuery; +import io.ebeaninternal.api.BindParams; +import io.ebeaninternal.api.SpiDtoQuery; +import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.server.dto.DtoBeanDescriptor; +import io.ebeaninternal.server.dto.DtoMappingRequest; +import io.ebeaninternal.server.dto.DtoQueryPlan; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Default implementation of DtoQuery. + */ +public class DefaultDtoQuery implements SpiDtoQuery { + + private final SpiEbeanServer server; + + private final DtoBeanDescriptor descriptor; + + private String sql; + + private int firstRow; + + private int maxRows; + + private int timeout; + + private int bufferFetchSizeHint; + + private boolean relaxedMode; + + private String label; + + /** + * Bind parameters when using the query language. + */ + private final BindParams bindParams = new BindParams(); + + /** + * Additional supply a query detail object. + */ + public DefaultDtoQuery(SpiEbeanServer server, DtoBeanDescriptor descriptor, String sql) { + this.server = server; + this.descriptor = descriptor; + this.sql = sql; + } + + @Override + public String planKey() { + return sql+":first"+firstRow+":max"+maxRows; + } + + @Override + public DtoQueryPlan getQueryPlan(String planKey) { + return descriptor.getQueryPlan(planKey); + } + + @Override + public DtoQueryPlan buildPlan(DtoMappingRequest request) { + return descriptor.buildPlan(request); + } + + @Override + public void putQueryPlan(String planKey, DtoQueryPlan plan) { + descriptor.putQueryPlan(planKey, plan); + } + + @Override + public void findEach(Consumer consumer) { + server.findDtoEach(this, consumer); + } + + @Override + public void findEachWhile(Predicate consumer) { + server.findDtoEachWhile(this, consumer); + } + + @Override + public List findList() { + return server.findDtoList(this); + } + + @Override + public T findOne() { + return server.findDtoOne(this); + } + + @Override + public Optional findOneOrEmpty() { + return Optional.ofNullable(findOne()); + } + + @Override + public DtoQuery setParameter(int position, Object value) { + bindParams.setParameter(position, value); + return this; + } + + @Override + public DtoQuery setParameter(String paramName, Object value) { + bindParams.setParameter(paramName, value); + return this; + } + + @Override + public String toString() { + return "DtoQuery [" + sql + "]"; + } + + @Override + public Class getType() { + return descriptor.getType(); + } + + @Override + public DtoQuery setRelaxedMode() { + this.relaxedMode = true; + return this; + } + + @Override + public boolean isRelaxedMode() { + return relaxedMode; + } + + @Override + public DtoQuery setLabel(String label) { + this.label = label; + return this; + } + + @Override + public String getLabel() { + return label; + } + + @Override + public int getFirstRow() { + return firstRow; + } + + @Override + public DtoQuery setFirstRow(int firstRow) { + this.firstRow = firstRow; + return this; + } + + @Override + public int getMaxRows() { + return maxRows; + } + + @Override + public DtoQuery setMaxRows(int maxRows) { + this.maxRows = maxRows; + return this; + } + + @Override + public int getTimeout() { + return timeout; + } + + @Override + public DtoQuery setTimeout(int secs) { + this.timeout = secs; + return this; + } + + @Override + public BindParams getBindParams() { + return bindParams; + } + + @Override + public DtoQuery setBufferFetchSizeHint(int bufferFetchSizeHint) { + this.bufferFetchSizeHint = bufferFetchSizeHint; + return this; + } + + @Override + public int getBufferFetchSizeHint() { + return bufferFetchSizeHint; + } + + @Override + public String getQuery() { + return sql; + } + +} diff --git a/src/test/java/io/ebean/DtoQueryTest.java b/src/test/java/io/ebean/DtoQueryTest.java new file mode 100644 index 000000000..53bf02730 --- /dev/null +++ b/src/test/java/io/ebean/DtoQueryTest.java @@ -0,0 +1,336 @@ +package io.ebean; + +import io.ebean.meta.MetaQueryMetric; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tests.model.basic.ResetBasicData; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DtoQueryTest extends BaseTestCase { + + private static final Logger log = LoggerFactory.getLogger(DtoQueryTest.class); + + @Test + public void dto_findList_constructorMatch() { + + ResetBasicData.reset(); + + DtoQuery dtoQuery = server().findDto(DCust.class, "select id, name from o_customer"); + + List list = dtoQuery.findList(); + + log.info(list.toString()); + assertThat(list).isNotEmpty(); + } + + @Test + public void dto_findEach_constructorMatch() { + + ResetBasicData.reset(); + + LoggedSqlCollector.start(); + server().findDto(DCust.class, "select id, name from o_customer where id > :id") + .setParameter("id", 0) + .findEach(it -> log.info("got " + it.getId() + " " + it.getName())); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql.get(0)).contains("select id, name from o_customer where id > ?"); + } + + @Test + public void dto_findEachWhile_constructorMatch() { + + ResetBasicData.reset(); + + LoggedSqlCollector.start(); + server().findDto(DCust.class, "select id, name from o_customer where id > :id order by id desc") + .setParameter("id", 0) + .findEachWhile(customer -> { + log.info("got " + customer.getId() + " " + customer.getName()); + return customer.getId() > 3; + }); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql.get(0)).contains("select id, name from o_customer where id > ?"); + } + + @Test + public void dto_findOneEmpty() { + + ResetBasicData.reset(); + + Optional rob = server().findDto(DCust.class, "select id, name from o_customer where name = :name") + .setParameter("name", "Fiona") + .findOneOrEmpty(); + + assertThat(rob.isPresent()).isTrue(); + + Optional oneOrEmpty = server().findDto(DCust.class, "select id, name from o_customer where name = :name") + .setParameter("name", "DoesNotExistMyFriend") + .findOneOrEmpty(); + + assertThat(oneOrEmpty.isPresent()).isFalse(); + } + + @Test + public void dto_findOne() { + + ResetBasicData.reset(); + + DCust fiona = server().findDto(DCust.class, "select id, name from o_customer where name = :name") + .setParameter("name", "Fiona") + .findOne(); + + assertThat(fiona.getName()).isEqualTo("Fiona"); + + DCust empty = server().findDto(DCust.class, "select id, name from o_customer where name = :name") + .setParameter("name", "DoesNotExistMyFriend") + .findOne(); + + assertThat(empty).isNull(); + } + + + @Test + public void dto_queryPlanHits() { + + ResetBasicData.reset(); + + server().getMetaInfoManager().collectQueryStatistics(true); + + String[] names = {"Rob", "Fiona", "Shrek"}; + + for (String name : names) { + + List custs = server().findDto(DCust.class, "select c3.id, c3.name from o_customer c3 where c3.name = :name") + .setLabel("basic") + .setParameter("name", name) + .findList(); + + log.info("Found " + custs); + } + + List stats = server().getMetaInfoManager().collectQueryStatistics(false); + + assertThat(stats).hasSize(1); + + MetaQueryMetric queryMetric = stats.get(0); + + assertThat(queryMetric.getLabel()).isEqualTo("basic"); + assertThat(queryMetric.getCount()).isEqualTo(3); + + + server().findDto(DCust.class, "select c4.id, c4.name from o_customer c4 where lower(c4.name) = :name") + .setLabel("basic2") + .setParameter("name", "rob") + .findList(); + + stats = server().getMetaInfoManager().collectQueryStatistics(true); + assertThat(stats).hasSize(2); + + log.info("stats " + stats); + + } + + @Test + public void dto_findList_relaxedMode() { + + ResetBasicData.reset(); + + List list = server().findDto(DCust3.class, "select id, name, 42 as total, '42' as something_we_cannot_map from o_customer") + .setRelaxedMode() + .findList(); + + log.info(list.toString()); + assertThat(list).isNotEmpty(); + } + + @Test + public void dto_findList_relaxedMode_defaultConstructor() { + + ResetBasicData.reset(); + + List list = server().findDto(DCust2.class, "select id, '42' as something_we_cannot_map, name from o_customer") + .setRelaxedMode() + .findList(); + + log.info(list.toString()); + assertThat(list).isNotEmpty(); + } + + @Test + public void dto_findList_constructorPlusMatch() { + + ResetBasicData.reset(); + + String sql = "select c.id, c.name, count(o.id) as totalOrders " + + "from o_customer c " + + "join o_order o on o.kcustomer_id = c.id " + + "where c.name like :name " + + "group by c.id, c.name"; + + List dtos = server().findDto(DCust.class, sql) + .setParameter("name", "Rob") + .findList(); + + log.info(dtos.toString()); + assertThat(dtos).isNotEmpty(); + } + + @Test + public void dto_findList_setters() { + + ResetBasicData.reset(); + + DtoQuery dtoQuery = server().findDto(DCust2.class, "select id, name from o_customer"); + + List list = dtoQuery.findList(); + + assertThat(list).isNotEmpty(); + } + + @Test + public void dto3_findList_constructorMatch() { + + ResetBasicData.reset(); + + List robs = server().findDto(DCust3.class, "select id, name, 42 as totalOrders from o_customer where name like ?") + .setParameter(1, "Rob") + .setMaxRows(10) + .findList(); + + + log.info(robs.toString()); + assertThat(robs).isNotEmpty(); + } + + @Test + public void dto3_findList_settersMatch() { + + ResetBasicData.reset(); + + List robs = server().findDto(DCust3.class, "select id, name from o_customer where name = :name") + .setParameter("name", "Rob") + .findList(); + + + log.info(robs.toString()); + assertThat(robs).isNotEmpty(); + } + + public static class DCust { + + final Integer id; + + final String name; + + int totalOrders; + + public DCust(Integer id, String name) { + this.id = id; + this.name = name; + } + + @Override + public String toString() { + return "id:" + id + " name:" + name + " totalOrders:" + totalOrders; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public int getTotalOrders() { + return totalOrders; + } + + public void setTotalOrders(int totalOrders) { + this.totalOrders = totalOrders; + } + } + + public static class DCust2 { + + Integer id; + + String name; + + @Override + public String toString() { + return "id:" + id + " name:" + name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + public static class DCust3 { + + Integer id; + + String name; + + int totalOrders; + + public DCust3() { + } + + public DCust3(Integer id, String name, int totalOrders) { + this.id = id; + this.name = name; + this.totalOrders = totalOrders; + } + + @Override + public String toString() { + return "id:" + id + " name:" + name + " totalOrders:" + totalOrders; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public int getTotalOrders() { + return totalOrders; + } + + public void setTotalOrders(int totalOrders) { + this.totalOrders = totalOrders; + } + + public void setId(Integer id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java index c9c15f702..28bf61ec4 100644 --- a/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java @@ -5,6 +5,7 @@ import io.ebean.BackgroundExecutor; import io.ebean.BeanState; import io.ebean.CallableSql; import io.ebean.DocumentStore; +import io.ebean.DtoQuery; import io.ebean.ExpressionFactory; import io.ebean.Filter; import io.ebean.FutureIds; @@ -388,6 +389,31 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return null; } + @Override + public void findDtoEach(SpiDtoQuery query, Consumer consumer) { + + } + + @Override + public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) { + + } + + @Override + public List findDtoList(SpiDtoQuery query) { + return null; + } + + @Override + public T findDtoOne(SpiDtoQuery query) { + return null; + } + + @Override + public DtoQuery findDto(Class dtoType, String sql) { + return null; + } + @Override public SqlQuery createSqlQuery(String sql) { return null;