Skip to content

Commit

Permalink
Support procedure invocation (#2502)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov authored Sep 19, 2023
1 parent 5e1fcef commit 3ec6680
Show file tree
Hide file tree
Showing 64 changed files with 1,948 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
*
* @param <S> The session type
* @param <Q> The query type
* @param <P> The selection query
* @param <P> The selection query type
* @author Denis Stepaov
* @since 3.5.0
*/
Expand Down Expand Up @@ -199,6 +199,44 @@ public Map<String, Object> getQueryHints(@NonNull StoredQuery<?, ?> storedQuery)
*/
protected abstract void setParameterList(Q query, String parameterName, Collection<Object> value, Argument<?> argument);

/**
* Sets a parameter into query.
*
* @param query The query
* @param parameterIndex The parameter index
* @param value The value
*/
protected abstract void setParameter(Q query, int parameterIndex, Object value);

/**
* Sets parameter into query.
*
* @param query The query
* @param parameterIndex The parameter index
* @param value The value
* @param argument The argument
*/
protected abstract void setParameter(Q query, int parameterIndex, Object value, Argument<?> argument);

/**
* Sets a list parameter into query.
*
* @param query The query
* @param parameterIndex The parameter index
* @param value The value
*/
protected abstract void setParameterList(Q query, int parameterIndex, Collection<Object> value);

/**
* Sets a list parameter into query.
*
* @param query The query
* @param parameterIndex The parameter index
* @param value The value
* @param argument The argument
*/
protected abstract void setParameterList(Q query, int parameterIndex, Collection<Object> value, Argument<?> argument);

/**
* Sets a hint.
*
Expand Down Expand Up @@ -371,13 +409,17 @@ public ConversionService getConversionService() {
* Bind parameters into query.
*
* @param q The query
* @param preparedQuery THe prepared query
* @param preparedQuery The prepared query
* @param bindNamed If parameter should be bind by the name
* @param <T> The entity type
* @param <R> The result type
*/
protected <T, R> void bindParameters(Q q, @NonNull PreparedQuery<T, R> preparedQuery) {
protected <T, R> void bindParameters(Q q, @NonNull PreparedQuery<T, R> preparedQuery, boolean bindNamed) {
BindableParametersPreparedQuery<T, R> bindableParametersPreparedQuery = getBindableParametersPreparedQuery(preparedQuery);
bindableParametersPreparedQuery.bindParameters(new BindableParametersStoredQuery.Binder() {

int index = 1;

@Override
public Object autoPopulateRuntimeProperty(RuntimePersistentProperty<?> persistentProperty, Object previousValue) {
return runtimeEntityRegistry.autoPopulateRuntimeProperty(persistentProperty, previousValue);
Expand All @@ -401,7 +443,11 @@ public void bindOne(QueryParameterBinding binding, Object value) {
Argument<?> argument = preparedQuery.getArguments()[parameterIndex];
Class<?> argumentType = argument.getType();
if (Collection.class.isAssignableFrom(argumentType)) {
setParameterList(q, parameterName, value == null ? Collections.emptyList() : (Collection<Object>) value, argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT));
if (bindNamed) {
setParameterList(q, parameterName, value == null ? Collections.emptyList() : (Collection<Object>) value, argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT));
} else {
setParameterList(q, index, value == null ? Collections.emptyList() : (Collection<Object>) value, argument.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT));
}
} else if (Object[].class.isAssignableFrom(argumentType)) {
Collection<Object> coll;
if (value == null) {
Expand All @@ -411,13 +457,22 @@ public void bindOne(QueryParameterBinding binding, Object value) {
} else {
coll = Arrays.asList((Object[]) value);
}
setParameterList(q, parameterName, coll);
} else {
if (bindNamed) {
setParameterList(q, parameterName, coll);
} else {
setParameterList(q, index, coll);
}
} else if (bindNamed) {
setParameter(q, parameterName, value, argument);
} else {
setParameter(q, index, value, argument);
}
} else {
} else if (bindNamed) {
setParameter(q, parameterName, value);
} else {
setParameter(q, index, value);
}
index++;
}

@Override
Expand All @@ -429,7 +484,7 @@ public void bindMany(QueryParameterBinding binding, Collection<Object> values) {
}

private <T, R> void bindPreparedQuery(P q, @NonNull PreparedQuery<T, R> preparedQuery, S currentSession) {
bindParameters(q, preparedQuery);
bindParameters(q, preparedQuery, true);
bindPageable(q, preparedQuery.getPageable());
bindQueryHints(q, preparedQuery, currentSession);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.data.annotation.QueryHint;
import io.micronaut.data.annotation.sql.Procedure;
import io.micronaut.data.hibernate.conf.RequiresSyncHibernate;
import io.micronaut.data.jpa.annotation.EntityGraph;
import io.micronaut.data.jpa.operations.JpaRepositoryOperations;
Expand All @@ -50,15 +51,16 @@
import io.micronaut.data.runtime.operations.ExecutorReactiveOperations;
import io.micronaut.transaction.TransactionOperations;
import jakarta.inject.Named;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.graph.RootGraph;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.ParameterMode;
import jakarta.persistence.Tuple;
import jakarta.persistence.criteria.CriteriaQuery;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.graph.RootGraph;
import org.hibernate.procedure.ProcedureCall;
import org.hibernate.query.CommonQueryContract;
import org.hibernate.query.MutationQuery;
import org.hibernate.query.Query;
Expand Down Expand Up @@ -155,6 +157,30 @@ protected void setParameterList(CommonQueryContract query, String parameterName,
query.setParameter(parameterName, value);
}

@Override
protected void setParameter(CommonQueryContract query, int parameterIndex, Object value) {
query.setParameter(parameterIndex, value);
}

@Override
protected void setParameter(CommonQueryContract query, int parameterIndex, Object value, Argument<?> argument) {
query.setParameter(parameterIndex, value);
}

@Override
protected void setParameterList(CommonQueryContract query, int parameterIndex, Collection<Object> value) {
query.setParameter(parameterIndex, value);
}

@Override
protected void setParameterList(CommonQueryContract query, int parameterIndex, Collection<Object> value, Argument<?> argument) {
if (value == null) {
value = Collections.emptyList();
}
// Can we ignore type? Was needed before Hibernate 6
query.setParameter(parameterIndex, parameterIndex);
}

@Override
protected void setHint(Query<?> query, String hintName, Object value) {
query.setHint(hintName, value);
Expand Down Expand Up @@ -383,13 +409,72 @@ public Optional<Number> executeUpdate(@NonNull PreparedQuery<?, Number> prepared
return executeWrite(session -> {
String query = preparedQuery.getQuery();
MutationQuery q = preparedQuery.isNative() ? session.createNativeMutationQuery(query) : session.createMutationQuery(query);
bindParameters(q, preparedQuery);
bindParameters(q, preparedQuery, true);
int numAffected = q.executeUpdate();
flushIfNecessary(session, preparedQuery.getAnnotationMetadata(), true);
return Optional.of(numAffected);
});
}

@Override
public <R> Optional<R> execute(PreparedQuery<?, R> preparedQuery) {
return executeWrite(session -> {
boolean needsOutRegistered = false;
if (preparedQuery.isProcedure()) {
Optional<String> named = preparedQuery.getAnnotationMetadata().stringValue(Procedure.class, "named");
ProcedureCall procedureQuery;
if (named.isPresent()) {
procedureQuery = session.createNamedStoredProcedureQuery(named.get());
} else {
String procedureName = preparedQuery.getAnnotationMetadata().stringValue(Procedure.class).orElseGet(preparedQuery::getName);
if (preparedQuery.getResultArgument().isVoid()) {
procedureQuery = session.createStoredProcedureQuery(procedureName);
} else {
procedureQuery = session.createStoredProcedureQuery(
procedureName,
preparedQuery.getResultArgument().getType()
);
needsOutRegistered = true;
}
int index = 1;
for (QueryParameterBinding queryBinding : preparedQuery.getQueryBindings()) {
int parameterIndex = queryBinding.getParameterIndex();
Argument<?> argument = preparedQuery.getArguments()[parameterIndex];
procedureQuery.registerStoredProcedureParameter(
index++,
argument.getType(),
ParameterMode.IN);
}
if (needsOutRegistered) {
procedureQuery.registerStoredProcedureParameter(
index,
preparedQuery.getResultArgument().getType(),
ParameterMode.OUT);
}
}
boolean bindNamed = procedureQuery.getRegisteredParameters().stream().anyMatch(p -> p.getName() != null);
bindParameters(procedureQuery, preparedQuery, bindNamed);
procedureQuery.execute();
if (preparedQuery.getResultArgument().isVoid()) {
flushIfNecessary(session, preparedQuery.getAnnotationMetadata(), true);
return Optional.empty();
}
jakarta.persistence.Parameter procedureParameter = procedureQuery.getRegisteredParameters().stream().filter(p -> p.getMode() == ParameterMode.OUT)
.findFirst()
.orElseThrow(() -> new IllegalStateException("Cannot determine the output parameter!"));
Object result;
if (bindNamed) {
result = procedureQuery.getOutputParameterValue(procedureParameter.getName());
} else {
result = procedureQuery.getOutputParameterValue(preparedQuery.getQueryBindings().size() + 1);
}
return Optional.ofNullable((R) result);
} else {
throw new IllegalStateException("Not supported!");
}
});
}

@Override
public <T> int delete(@NonNull DeleteOperation<T> operation) {
StoredQuery<T, ?> storedQuery = operation.getStoredQuery();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,22 @@ package io.micronaut.data.hibernate

import io.micronaut.context.annotation.Property
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject

@MicronautTest(packages = "io.micronaut.data.tck.entities", rollback = false, transactional = false)
@Property(name = "datasources.default.name", value = "mydb")
@Property(name = 'datasources.default.db-type', value = 'postgres')
@Property(name = 'jpa.default.properties.hibernate.hbm2ddl.auto', value = 'create-drop')
class HibernatePostgresQuerySpec extends AbstractHibernateQuerySpec {

@Inject
AnimalRepository animalRepository

void "test procedure"() {
expect:
animalRepository.add1Named(123) == 124
animalRepository.add1Indexed(123) == 124
animalRepository.add1(123) == 124
animalRepository.add1Alias(123) == 124
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.micronaut.data.hibernate;

import io.micronaut.context.annotation.Context;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import io.micronaut.jdbc.BasicJdbcConfiguration;
import jakarta.inject.Singleton;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Locale;
import java.util.Properties;

@Context
@Singleton
public class PostgresDbInit implements BeanCreatedEventListener<BasicJdbcConfiguration> {

@Override
public BasicJdbcConfiguration onCreated(BeanCreatedEvent<BasicJdbcConfiguration> event) {
BasicJdbcConfiguration configuration = event.getBean();
if (!configuration.getConfiguredDriverClassName().toLowerCase(Locale.ROOT).contains("postgres")) {
return configuration;
}

try {
// Rancher local testing delay
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

final Properties info = new Properties();
info.put("user", configuration.getUsername());
info.put("password", configuration.getPassword());

try {
try (Connection connection = DriverManager.getConnection(configuration.getUrl(), info)) {
try (CallableStatement st = connection.prepareCall("""
CREATE PROCEDURE add1(IN myInput integer, OUT myOutput integer)
LANGUAGE plpgsql
AS $$
BEGIN
myOutput := myInput + 1;
END;
$$;
""")) {
st.execute();
} catch (SQLException e) {
e.printStackTrace();
// Ignore if already exists
}

}
} catch (Exception e) {
throw new RuntimeException(e);
}
return configuration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.micronaut.data.hibernate;

import io.micronaut.data.annotation.Repository;
import io.micronaut.data.annotation.sql.Procedure;
import io.micronaut.data.hibernate.entities.Animal;
import io.micronaut.data.jpa.repository.JpaRepository;

@Repository
public interface AnimalRepository extends JpaRepository<Animal, Long> {

@Procedure(named = "myAdd1Named")
int add1Named(int myInput);

@Procedure(named = "myAdd1Indexed")
int add1Indexed(int myInput);

@Procedure
int add1(int myInput);

@Procedure("add1")
int add1Alias(int myInput);

}
Loading

0 comments on commit 3ec6680

Please sign in to comment.