SqlNativeRepositoryQuery.java
package org.codefilarete.stalactite.spring.repository.query.nativ;
import javax.annotation.Nullable;
import java.lang.reflect.Executable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.LongSupplier;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.MethodReferenceCapturer;
import org.codefilarete.stalactite.engine.runtime.AdvancedEntityPersister;
import org.codefilarete.stalactite.engine.runtime.RelationalEntityFinder;
import org.codefilarete.stalactite.query.model.JoinLink;
import org.codefilarete.stalactite.spring.repository.query.execution.AbstractQueryExecutor;
import org.codefilarete.stalactite.spring.repository.query.execution.AbstractRepositoryQuery;
import org.codefilarete.stalactite.spring.repository.query.NativeQuery;
import org.codefilarete.stalactite.spring.repository.query.projection.ProjectionMappingFinder;
import org.codefilarete.stalactite.spring.repository.query.StalactiteQueryMethod;
import org.codefilarete.stalactite.spring.repository.query.execution.StalactiteQueryMethodInvocationParameters;
import org.codefilarete.stalactite.sql.ConnectionProvider;
import org.codefilarete.stalactite.sql.Dialect;
import org.codefilarete.stalactite.sql.result.ResultSetIterator;
import org.codefilarete.stalactite.sql.statement.ReadOperation;
import org.codefilarete.stalactite.sql.statement.SQLExecutionException;
import org.codefilarete.stalactite.sql.statement.StringParamedSQL;
import org.codefilarete.tool.Reflections;
import org.springframework.data.projection.ProjectionFactory;
public class SqlNativeRepositoryQuery<C, R> extends AbstractRepositoryQuery<C, R> {
private final String sql;
/**
* Only available when a {@link org.springframework.data.domain.Page} type is expected as a result. Null otherwise.
* Because the {@link org.springframework.data.domain.Page} instantiation algorithm requires the total number of elements.
*/
@Nullable
private final String sqlCount;
private final ProjectionFactory factory;
private final Dialect dialect;
private final ConnectionProvider connectionProvider;
private final RelationalEntityFinder<C, ?, ?> relationalEntityFinder;
private final ProjectionMappingFinder<C> projectionMappingFinder;
public SqlNativeRepositoryQuery(StalactiteQueryMethod queryMethod,
String sql,
@Nullable String sqlCount,
AdvancedEntityPersister<C, ?> entityPersister,
ProjectionFactory factory,
Dialect dialect,
ConnectionProvider connectionProvider) {
super(queryMethod);
this.sql = sql;
this.sqlCount = sqlCount;
this.factory = factory;
this.dialect = dialect;
this.connectionProvider = connectionProvider;
this.projectionMappingFinder = new ProjectionMappingFinder<>(factory, entityPersister);
// Note that at this stage we can afford to ask for immediate Query creation because we are at a high layer (Spring Data Query discovery) and
// persister is supposed to be finalized and up-to-date (containing the whole entity aggregate graph), that why we pass "true" as argument
this.relationalEntityFinder = new RelationalEntityFinder<>(
entityPersister,
connectionProvider,
dialect,
true);
// TODO: when upgrading to Spring Data 3.x.y, add an assertion on Limit parameter presence as it's done in StringBasedJdbcQuery
// https://github.com/spring-projects/spring-data-relational/blob/main/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java#L176
}
@Override
protected AbstractQueryExecutor<List<Object>, Object> buildQueryExecutor(StalactiteQueryMethodInvocationParameters invocationParameters) {
// Note that if the query is a projection then the result List must contain Map<String, Object> else it must contain entities
AbstractQueryExecutor<List<Object>, Object> queryExecutor;
if (method.getParameters().hasDynamicProjection() && factory.getProjectionInformation(invocationParameters.getDynamicProjectionType()).isClosed()
|| getQueryMethod().getResultProcessor().getReturnedType().isProjecting()) {
// Extracting the Selectable and PropertyPath from the projection type
Class<?> projectionType = method.getParameters().hasDynamicProjection()
? invocationParameters.getDynamicProjectionType()
: method.getReturnedObjectType();
IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> columnToProperties = this.projectionMappingFinder.lookup(projectionType);
// Building aliases. The way we build them has no impact on other algorithms, they could be random, but for debugging and clarity purpose
// we generate them as closest as possible to the property name
IdentityHashMap<JoinLink<?, ?>, String> aliases = buildAliases(columnToProperties);
queryExecutor = (AbstractQueryExecutor) new TupleNativeQueryExecutor(getQueryMethod(), sql, dialect, connectionProvider, aliases, columnToProperties, invocationParameters::getLimit);
} else {
queryExecutor = (AbstractQueryExecutor) new DomainEntityNativeQueryExecutor<>(getQueryMethod(), sql, relationalEntityFinder, dialect);
}
return queryExecutor;
}
@Override
protected LongSupplier buildCountSupplier(StalactiteQueryMethodInvocationParameters invocationParameters) {
return () -> {
if (sqlCount == null || sqlCount.isEmpty()) {
MethodReferenceCapturer methodReferenceCapturer = new MethodReferenceCapturer();
Executable countQueryAccessor = methodReferenceCapturer.findExecutable(NativeQuery::countQuery);
throw new IllegalStateException("Count query is mandatory for paged queries, please provide one through " + Reflections.toString(countQueryAccessor));
}
StringParamedSQL query = new StringParamedSQL(sqlCount, invocationParameters.bindParameters(dialect));
query.setValues(invocationParameters.getNamedValues());
try (ReadOperation<?> readOperation = dialect.getReadOperationFactory().createInstance(query, connectionProvider)) {
// readOperation.setListener((SQLOperation.SQLOperationListener<ParamType>) operationListener);
// Note that setValues must be done after operationListener set
readOperation.setValues((Map) query.getValues());
ResultSet resultSet = readOperation.execute();
ResultSetIterator<Long> resultSetIterator = new ResultSetIterator<Long>(resultSet) {
@Override
public Long convert(ResultSet resultSet) throws SQLException {
return resultSet.getLong(1);
}
};
return resultSetIterator.hasNext() ? resultSetIterator.next() : 0;
} catch (RuntimeException e) {
throw new SQLExecutionException(query.getSQL(), e);
}
};
}
}