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;