#1297 - ENH: Add DtoQuery ... such that we can map native SQL queries automatically into "DTO beans"

This commit is contained in:
Rob Bygrave
2018-03-02 23:32:01 +13:00
parent 5801f7620d
commit aad5b33db2
47 changed files with 2293 additions and 273 deletions
@@ -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<T> {
private final Map<String, DtoQueryPlan> plans = new ConcurrentHashMap<>();
private final Class<T> dtoType;
private final DtoMeta meta;
DtoBeanDescriptor(Class<T> dtoType, DtoMeta meta) {
this.dtoType = dtoType;
this.meta = meta;
}
public Class<T> 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);
}
}
}
@@ -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<Class, DtoBeanDescriptor> descriptorMap = new ConcurrentHashMap<>();
public DtoBeanManager(TypeManager typeManager) {
this.typeManager = typeManager;
}
/**
* Return the descriptor for the given DTO bean class.
*/
@SuppressWarnings("unchecked")
public <T> DtoBeanDescriptor<T> getDescriptor(Class<T> dtoType) {
return descriptorMap.computeIfAbsent(dtoType, this::createDescriptor);
}
private <T> DtoBeanDescriptor createDescriptor(Class<T> dtoType) {
try {
DtoMeta meta = new DtoMetaBuilder(dtoType, typeManager).build();
return new DtoBeanDescriptor<>(dtoType, meta);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
public List<MetaQueryMetric> collectStats(boolean reset) {
QueryPlanCollector collector = MetricFactory.get().createCollector(reset);
for (DtoBeanDescriptor value : descriptorMap.values()) {
value.collectStats(collector);
}
return collector.complete();
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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<String, DtoMetaProperty> propMap = new LinkedHashMap<>();
private final Map<Integer, DtoMetaConstructor> constructorMap = new LinkedHashMap<>();
private final DtoMetaConstructor defaultConstructor;
private final DtoMetaConstructor maxArgConstructor;
DtoMeta(Class<?> dtoType, List<DtoMetaConstructor> constructors, List<DtoMetaProperty> 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);
}
}
@@ -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.
* <p>
* 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<DtoMetaProperty> properties = new ArrayList<>();
private final List<DtoMetaConstructor> 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");
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
@@ -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;
}
}