RelationalEntityFinder.java
package org.codefilarete.stalactite.engine.runtime;
import java.sql.ResultSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.codefilarete.stalactite.engine.SelectExecutor;
import org.codefilarete.stalactite.engine.configurer.builder.BuildLifeCycleListener;
import org.codefilarete.stalactite.engine.configurer.builder.PersisterBuilderContext;
import org.codefilarete.stalactite.engine.runtime.load.EntityInflater;
import org.codefilarete.stalactite.engine.runtime.load.EntityJoinTree;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeInflater;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeQueryBuilder;
import org.codefilarete.stalactite.engine.runtime.load.EntityTreeQueryBuilder.EntityTreeQuery;
import org.codefilarete.stalactite.engine.runtime.query.EntityCriteriaSupport;
import org.codefilarete.stalactite.engine.runtime.query.EntityQueryCriteriaSupport;
import org.codefilarete.stalactite.query.ConfiguredEntityCriteria;
import org.codefilarete.stalactite.query.EntityFinder;
import org.codefilarete.stalactite.query.builder.QuerySQLBuilderFactory.QuerySQLBuilder;
import org.codefilarete.stalactite.query.model.CriteriaChain;
import org.codefilarete.stalactite.query.model.GroupBy;
import org.codefilarete.stalactite.query.model.Having;
import org.codefilarete.stalactite.query.model.Limit;
import org.codefilarete.stalactite.query.model.OrderBy;
import org.codefilarete.stalactite.query.model.Query;
import org.codefilarete.stalactite.query.model.Select;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.query.model.Where;
import org.codefilarete.stalactite.sql.ConnectionProvider;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.Accumulator;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.stalactite.sql.result.ColumnedRowIterator;
import org.codefilarete.stalactite.sql.statement.PreparedSQL;
import org.codefilarete.stalactite.sql.statement.ReadOperation;
import org.codefilarete.stalactite.sql.statement.SQLExecutionException;
import org.codefilarete.stalactite.sql.statement.SQLOperation.SQLOperationListener;
import org.codefilarete.stalactite.sql.statement.SQLStatement;
import org.codefilarete.stalactite.sql.statement.StringParamedSQL;
import org.codefilarete.stalactite.sql.statement.binder.PreparedStatementWriter;
import org.codefilarete.stalactite.sql.statement.binder.ResultSetReader;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.Maps;
import static org.codefilarete.tool.bean.Objects.preventNull;
/**
* Class aimed at loading an entity graph which is selected by some criteria on some properties coming from a {@link CriteriaChain}.
*
* Implementation is based on {@link EntityJoinTree} to build the query and the entity graph.
*
* @author Guillaume Mary
* @see EntityFinder#select(ConfiguredEntityCriteria, Map, OrderBy, Limit)
*/
public class RelationalEntityFinder<C, I, T extends Table<T>> implements EntityFinder<C, I> {
private static final String PRIMARY_KEY_ALIAS = "rootId";
private final EntityJoinTree<C, I> entityJoinTree;
private final ConnectionProvider connectionProvider;
private final Dialect dialect;
private final EntityCriteriaSupport<C> criteriaSupport;
private SelectExecutor<C, I> selectExecutor;
private EntityTreeQuery<C> entityTreeQuery;
private Query query;
private SQLOperationListener<?> operationListener;
public RelationalEntityFinder(EntityJoinTree<C, I> entityJoinTree,
SelectExecutor<C, I> selectExecutor,
ConnectionProvider connectionProvider,
Dialect dialect) {
this.entityJoinTree = entityJoinTree;
this.selectExecutor = selectExecutor;
this.connectionProvider = connectionProvider;
this.dialect = dialect;
this.entityTreeQuery = new EntityTreeQueryBuilder<>(this.entityJoinTree, dialect.getColumnBinderRegistry()).buildSelectQuery();
this.criteriaSupport = new EntityCriteriaSupport<>(this.entityJoinTree);
PersisterBuilderContext.CURRENT.get().addBuildLifeCycleListener(new BuildLifeCycleListener() {
@Override
public void afterBuild() {
}
@Override
public void afterAllBuild() {
buildQuery();
}
});
}
public RelationalEntityFinder(AdvancedEntityPersister<C, I> mainPersister,
ConnectionProvider connectionProvider,
Dialect dialect,
boolean withImmediateQueryBuild) {
this.entityJoinTree = mainPersister.getEntityJoinTree();
this.connectionProvider = connectionProvider;
this.dialect = dialect;
this.entityTreeQuery = new EntityTreeQueryBuilder<>(this.entityJoinTree, dialect.getColumnBinderRegistry()).buildSelectQuery();
this.criteriaSupport = new EntityCriteriaSupport<>(this.entityJoinTree, withImmediateQueryBuild);
}
private void buildQuery() {
entityTreeQuery = new EntityTreeQueryBuilder<>(this.entityJoinTree, dialect.getColumnBinderRegistry()).buildSelectQuery();
query = entityTreeQuery.getQuery();
}
@Override
public void setOperationListener(SQLOperationListener<?> operationListener) {
this.operationListener = operationListener;
}
@Override
public EntityJoinTree<C, I> getEntityJoinTree() {
return entityJoinTree;
}
@Override
public EntityQueryCriteriaSupport<C, I> newCriteriaSupport() {
return new EntityQueryCriteriaSupport<>(this, criteriaSupport.copy());
}
public Set<C> selectFromQueryBean(String sql, Map<String, Object> values) {
// Computing parameter binders from values
Map<String, PreparedStatementWriter<?>> parameterBinders = new HashMap<>();
values.forEach((paramName, value) -> {
PreparedStatementWriter<?> writer = dialect.getColumnBinderRegistry().getWriter(value.getClass());
parameterBinders.put(paramName, writer);
});
return selectFromQueryBean(sql, values, parameterBinders);
}
public Set<C> selectFromQueryBean(String sql, Map<String, Object> values, Map<String, PreparedStatementWriter<?>> parameterBinders) {
// we use EntityTreeQueryBuilder to get the inflater, please note that it also build the default Query
EntityTreeInflater<C> inflater = entityTreeQuery.getInflater();
// computing SQL readers from dialect binder registry
Query query = entityTreeQuery.getQuery();
Map<Selectable<?>, ResultSetReader<?>> selectParameterBinders = new HashMap<>();
Map<Selectable<?>, String> aliases = new HashMap<>();
query.getColumns().forEach(selectable -> {
ResultSetReader<?> reader;
String alias = preventNull(query.getAliases().get(selectable), selectable.getExpression());
if (selectable instanceof Column) {
reader = dialect.getColumnBinderRegistry().getReader((Column) selectable);
selectParameterBinders.put(selectable, reader);
} else {
reader = dialect.getColumnBinderRegistry().getReader(selectable.getJavaType());
}
selectParameterBinders.put(selectable, reader);
aliases.put(selectable, alias);
});
StringParamedSQL statement = new StringParamedSQL(sql, parameterBinders);
statement.setValues(values);
return new InternalExecutor(inflater, selectParameterBinders, aliases).execute(statement);
}
/**
* Implementation note: the load is done in 2 phases: one for root ids selection from criteria, a second for the whole graph load from found root ids.
*/
@Override
public Set<C> select(ConfiguredEntityCriteria where,
Map<String, Object> valuesPerParam,
OrderBy orderBy,
Limit limit) {
// When the condition contains some criteria on a collection, the ResultSet contains only data matching it,
// then the graph is a partial view of the real entity. Therefore, when the condition contains some Collection criteria
// we must load the graph in 2 phases: a first lookup for ids matching the result, and a second phase that loads the entity graph
// according to the ids
if (where.hasCollectionCriteria()) {
Query queryClone = new Query(
new Select(),
entityTreeQuery.getQuery().getFromDelegate(),
new Where<>(where.getCriteria()),
new GroupBy(),
new Having(),
orderBy,
limit);
QuerySQLBuilder sqlQueryBuilder = dialect.getQuerySQLBuilderFactory().queryBuilder(queryClone);
// First phase: selecting ids (made by clearing selected elements for performance issue)
Column<T, I> pk = (Column<T, I>) Iterables.first(((Table) entityJoinTree.getRoot().getTable()).getPrimaryKey().getColumns());
queryClone.select(pk, PRIMARY_KEY_ALIAS);
Map<Column<?, ?>, String> aliases = Maps.asMap(pk, PRIMARY_KEY_ALIAS);
Map<Column<?, ?>, ResultSetReader<?>> columnReaders = Maps.asMap(pk, dialect.getColumnBinderRegistry().getBinder(pk));
Set<I> ids = readIds(sqlQueryBuilder.toPreparableSQL().toPreparedSQL(new HashMap<>()), columnReaders, aliases);
if (ids.isEmpty()) {
// No result found, we must stop here because request below doesn't support in(..) without values (SQL error from database)
return Collections.emptySet();
} else {
// Second phase : selecting elements by found identifiers
return selectExecutor.select(ids);
}
} else {
// The condition doesn't have a criteria on a collection property (*-to-many): the load can be done with one query because the SQL criteria
// doesn't make a subset of the entity graph
// We clone the query to avoid polluting the instance one, else, from select(..) to select(..), we append the criteria at the end of it,
// which makes the query usually returning no data (because of the condition mix)
Query queryClone = new Query(
new Select(entityTreeQuery.getQuery().getSelectDelegate()),
entityTreeQuery.getQuery().getFromDelegate(),
new Where<>(where.getCriteria()),
new GroupBy(),
new Having(),
orderBy,
limit);
QuerySQLBuilder sqlQueryBuilder = dialect.getQuerySQLBuilderFactory().queryBuilder(queryClone);
PreparedSQL preparedSQL = sqlQueryBuilder.toPreparableSQL().toPreparedSQL(valuesPerParam);
return new InternalExecutor(entityTreeQuery).execute(preparedSQL);
}
}
private Set<I> readIds(PreparedSQL preparedSQL, Map<Column<?, ?>, ResultSetReader<?>> columnReaders, Map<Column<?, ?>, String> aliases) {
EntityInflater<C, I> entityInflater = entityJoinTree.getRoot().getEntityInflater();
try (ReadOperation<Integer> closeableOperation = dialect.getReadOperationFactory().createInstance(preparedSQL, connectionProvider)) {
ColumnedRowIterator rowIterator = new ColumnedRowIterator(closeableOperation.execute(), columnReaders, aliases);
return Iterables.collect(() -> rowIterator, row -> entityInflater.giveIdentifier(row), HashSet::new);
} catch (RuntimeException e) {
throw new SQLExecutionException(preparedSQL.getSQL(), e);
}
}
@Override
public <R, O> R selectProjection(Consumer<Select> selectAdapter,
Map<String, Object> values,
Accumulator<? super Function<Selectable<O>, O>, Object, R> accumulator,
ConfiguredEntityCriteria where,
boolean distinct,
OrderBy orderBy,
Limit limit) {
Query queryClone = new Query(new Select(), query.getFromDelegate(), new Where<>(where.getCriteria()), new GroupBy(), new Having(), orderBy, limit);
queryClone.getSelectDelegate().setDistinct(distinct);
QuerySQLBuilder sqlQueryBuilder = dialect.getQuerySQLBuilderFactory().queryBuilder(queryClone);
// First phase : selecting ids (made by clearing selected elements for performance issue)
selectAdapter.accept(queryClone.getSelectDelegate());
Map<Selectable<?>, ResultSetReader<?>> columnReaders = Iterables.map(queryClone.getColumns(), Function.identity(), selectable -> dialect.getColumnBinderRegistry().getBinder(selectable.getJavaType()));
PreparedSQL preparedSQL = sqlQueryBuilder.toPreparableSQL().toPreparedSQL(values);
Map<Selectable<?>, String> aliases = queryClone.getAliases();
// Propagating aliases from the original query to clone if the user didn't mention the alias
// This avoids the later thrown exception "Column doesn't exist : null" because the column reading is based on the alias (which doesn't exist)
Map<Selectable<?>, String> defaultAliases = query.getAliases();
aliases.entrySet().forEach(alias -> {
if (alias.getValue() == null) {
alias.setValue(defaultAliases.get(alias.getKey()));
}
});
return readProjection(preparedSQL, columnReaders, aliases, accumulator);
}
private <R, O> R readProjection(PreparedSQL preparedSQL,
Map<Selectable<?>, ResultSetReader<?>> columnReaders,
Map<Selectable<?>, String> aliases,
Accumulator<? super Function<Selectable<O>, O>, Object, R> accumulator) {
try (ReadOperation<Integer> closeableOperation = dialect.getReadOperationFactory().createInstance(preparedSQL, connectionProvider)) {
ColumnedRowIterator rowIterator = new ColumnedRowIterator(closeableOperation.execute(), columnReaders, aliases);
return accumulator.collect(Iterables.stream(rowIterator).map(row -> (Function<Selectable<O>, O>) row::get).collect(Collectors.toList()));
} catch (RuntimeException e) {
throw new SQLExecutionException(preparedSQL.getSQL(), e);
}
}
/**
* Small class to avoid passing {@link EntityTreeQuery} as argument to all methods
*/
private class InternalExecutor {
private final EntityTreeInflater<C> inflater;
private final Map<Selectable<?>, ResultSetReader<?>> selectParameterBinders;
private final Map<Selectable<?>, String> columnAliases;
private InternalExecutor(EntityTreeQuery<C> entityTreeQuery) {
this(entityTreeQuery.getInflater(), entityTreeQuery.getSelectParameterBinders(), entityTreeQuery.getColumnAliases());
}
public InternalExecutor(EntityTreeInflater<C> inflater,
Map<Selectable<?>, ? extends ResultSetReader<?>> selectParameterBinders,
Map<Selectable<?>, String> columnAliases) {
this.inflater = inflater;
this.selectParameterBinders = (Map<Selectable<?>, ResultSetReader<?>>) selectParameterBinders;
this.columnAliases = columnAliases;
}
protected <ParamType> Set<C> execute(SQLStatement<ParamType> query) {
try (ReadOperation<ParamType> readOperation = dialect.getReadOperationFactory().createInstance(query, connectionProvider)) {
readOperation.setListener((SQLOperationListener<ParamType>) operationListener);
// Note that setValues must be done after operationListener set
readOperation.setValues(query.getValues());
return transform(readOperation);
} catch (RuntimeException e) {
throw new SQLExecutionException(query.getSQL(), e);
}
}
protected Set<C> transform(ReadOperation<?> closeableOperation) {
ResultSet resultSet = closeableOperation.execute();
// NB: we give the same ParametersBinders of those given at ColumnParameterizedSelect since the row iterator is expected to read column from it
ColumnedRowIterator rowIterator = new ColumnedRowIterator(resultSet, selectParameterBinders, columnAliases);
return transform(rowIterator);
}
protected Set<C> transform(Iterator<? extends ColumnedRow> rowIterator) {
return inflater.transform(() -> (Iterator<ColumnedRow>) rowIterator, 50);
}
}
}