HHH-9576 - Use JDBC bind variables for handling JPA Criteria query numeric literals

This commit is contained in:
Vlad Mihalcea 2017-05-29 15:32:41 +03:00
parent 81f4ab8f06
commit 56947f28dc
18 changed files with 497 additions and 9 deletions

View File

@ -44,7 +44,6 @@
import org.hibernate.dialect.function.SQLFunction;
import org.hibernate.engine.config.internal.ConfigurationServiceImpl;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.config.spi.StandardConverters;
import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
@ -53,6 +52,7 @@
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.loader.BatchFetchStyle;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.hibernate.resource.jdbc.spi.StatementInspector;
import org.hibernate.resource.transaction.spi.TransactionCoordinatorBuilder;
@ -564,6 +564,7 @@ public static class SessionFactoryOptionsStateStandardImpl implements SessionFac
private boolean wrapResultSetsEnabled;
private TimeZone jdbcTimeZone;
private boolean queryParametersValidationEnabled;
private LiteralHandlingMode criteriaLiteralHandlingMode;
private Map<String, SQLFunction> sqlFunctions;
@ -776,6 +777,13 @@ else if ( jdbcTimeZoneValue != null ) {
configurationSettings,
true
);
this.criteriaLiteralHandlingMode = strategySelector.resolveStrategy(
LiteralHandlingMode.class,
configurationSettings.get( CRITERIA_LITERAL_HANDLING_MODE ),
LiteralHandlingMode.AUTO,
(clazz) -> LiteralHandlingMode.AUTO
);
}
private static Interceptor determineInterceptor(Map configurationSettings, StrategySelector strategySelector) {
@ -1216,6 +1224,11 @@ public TimeZone getJdbcTimeZone() {
public boolean isQueryParametersValidationEnabled() {
return this.queryParametersValidationEnabled;
}
@Override
public LiteralHandlingMode getCriteriaLiteralHandlingMode() {
return this.criteriaLiteralHandlingMode;
}
}
@Override
@ -1544,4 +1557,9 @@ public TimeZone getJdbcTimeZone() {
public boolean isQueryParametersValidationEnabled() {
return options.isQueryParametersValidationEnabled();
}
@Override
public LiteralHandlingMode getCriteriaLiteralHandlingMode() {
return options.getCriteriaLiteralHandlingMode();
}
}

View File

@ -28,6 +28,7 @@
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.loader.BatchFetchStyle;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.hibernate.resource.jdbc.spi.StatementInspector;
import org.hibernate.tuple.entity.EntityTuplizerFactory;
@ -127,6 +128,7 @@ public class SessionFactoryOptionsImpl implements SessionFactoryOptions {
private final Map<String, SQLFunction> sqlFunctions;
private boolean queryParametersValidationEnabled;
private LiteralHandlingMode criteriaLiteralHandlingMode;
public SessionFactoryOptionsImpl(SessionFactoryOptionsState state) {
this.serviceRegistry = state.getServiceRegistry();
@ -207,6 +209,7 @@ public SessionFactoryOptionsImpl(SessionFactoryOptionsState state) {
this.jdbcTimeZone = state.getJdbcTimeZone();
this.queryParametersValidationEnabled = state.isQueryParametersValidationEnabled();
this.criteriaLiteralHandlingMode = state.getCriteriaLiteralHandlingMode();
}
@Override
@ -542,4 +545,9 @@ public TimeZone getJdbcTimeZone() {
public boolean isQueryParametersValidationEnabled() {
return queryParametersValidationEnabled;
}
@Override
public LiteralHandlingMode getCriteriaLiteralHandlingMode() {
return criteriaLiteralHandlingMode;
}
}

View File

@ -28,6 +28,7 @@
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.loader.BatchFetchStyle;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.hibernate.resource.jdbc.spi.StatementInspector;
import org.hibernate.tuple.entity.EntityTuplizerFactory;
@ -178,4 +179,8 @@ public interface SessionFactoryOptionsState {
TimeZone getJdbcTimeZone();
boolean isQueryParametersValidationEnabled();
default LiteralHandlingMode getCriteriaLiteralHandlingMode() {
return LiteralHandlingMode.AUTO;
}
}

View File

@ -27,6 +27,7 @@
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.loader.BatchFetchStyle;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.hibernate.resource.jdbc.spi.StatementInspector;
import org.hibernate.tuple.entity.EntityTuplizerFactory;
@ -383,4 +384,9 @@ public TimeZone getJdbcTimeZone() {
public boolean isQueryParametersValidationEnabled() {
return delegate.isQueryParametersValidationEnabled();
}
@Override
public LiteralHandlingMode getCriteriaLiteralHandlingMode() {
return delegate.getCriteriaLiteralHandlingMode();
}
}

View File

@ -27,6 +27,7 @@
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.loader.BatchFetchStyle;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode;
import org.hibernate.resource.jdbc.spi.StatementInspector;
import org.hibernate.tuple.entity.EntityTuplizerFactory;
@ -223,4 +224,8 @@ default boolean doesConnectionProviderDisableAutoCommit() {
default boolean isQueryParametersValidationEnabled(){
return isJpaBootstrap();
}
default LiteralHandlingMode getCriteriaLiteralHandlingMode() {
return LiteralHandlingMode.AUTO;
}
}

View File

@ -1690,4 +1690,24 @@ public interface AvailableSettings {
*
*/
String VALIDATE_QUERY_PARAMETERS = "hibernate.query.validate_parameters";
/**
* By default, Criteria queries uses bind parameters for any literal that is not a numeric value.
*
* However, to increase the likelihood of JDBC statement caching,
* you might want to use bind parameters for numeric values too.
* The {@link org.hibernate.query.criteria.LiteralHandlingMode#BIND} mode will use bind variables for any literal value.
*
* The {@link org.hibernate.query.criteria.LiteralHandlingMode#INLINE} mode will inline literal values as-is.
* To prevent SQL injection, never use {@link org.hibernate.query.criteria.LiteralHandlingMode#INLINE} with String variables.
* Always use constants with the {@link org.hibernate.query.criteria.LiteralHandlingMode#INLINE} mode.
* </p>
* Valid options are defined by the {@link org.hibernate.query.criteria.LiteralHandlingMode} enum.
* </p>
* The default value is {@link org.hibernate.query.criteria.LiteralHandlingMode#AUTO}
*
* @since 5.2.12
* @see org.hibernate.query.criteria.LiteralHandlingMode
*/
String CRITERIA_LITERAL_HANDLING_MODE = "hibernate.criteria.literal_handling_mode";
}

View File

@ -2921,6 +2921,24 @@ public boolean isLegacyLimitHandlerBehaviorEnabled() {
return legacyLimitHandlerBehavior;
}
/**
* Inline String literal.
*
* @return escaped String
*/
public String inlineLiteral(String literal) {
return String.format( "\'%s\'", escapeLiteral( literal ) );
}
/**
* Escape String literal.
*
* @return escaped String
*/
protected String escapeLiteral(String literal) {
return literal.replace("'", "''");
}
private void resolveLegacyLimitHandlerBehavior(ServiceRegistry serviceRegistry) {
// HHH-11194
// Temporary solution to set whether legacy limit handler behavior should be used.

View File

@ -583,4 +583,9 @@ public boolean dropConstraints() {
protected MySQLStorageEngine getDefaultMySQLStorageEngine() {
return MyISAMStorageEngine.INSTANCE;
}
@Override
protected String escapeLiteral(String literal) {
return super.escapeLiteral( literal ).replace("\\", "\\\\");
}
}

View File

@ -0,0 +1,29 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.query.criteria;
/**
* This enum defines how literals are handled by JPA Criteria.
*
* By default ({@code AUTO}), Criteria queries uses bind parameters for any literal that is not a numeric value.
*
* However, to increase the likelihood of JDBC statement caching,
* you might want to use bind parameters for numeric values too.
* The {@code BIND} mode will use bind variables for any literal value.
*
* The {@code INLINE} mode will inline literal values as-is.
* To prevent SQL injection, never use {@code INLINE} with String variables.
* Always use constants with the {@code INLINE} mode.
*
* @author Vlad Mihalcea
*/
public enum LiteralHandlingMode {
AUTO,
BIND,
INLINE
}

View File

@ -14,9 +14,13 @@
import javax.persistence.TypedQuery;
import javax.persistence.criteria.ParameterExpression;
import org.hibernate.SessionFactory;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.query.spi.QueryImplementor;
import org.hibernate.type.Type;
@ -47,6 +51,14 @@ public QueryImplementor compile(CompilableCriteria criteria) {
final Map<ParameterExpression<?>, ExplicitParameterInfo<?>> explicitParameterInfoMap = new HashMap<>();
final List<ImplicitParameterBinding> implicitParameterBindings = new ArrayList<>();
final SessionFactoryImplementor sessionFactory = entityManager.getSessionFactory();
final LiteralHandlingMode criteriaLiteralHandlingMode = sessionFactory
.getSessionFactoryOptions()
.getCriteriaLiteralHandlingMode();
final Dialect dialect = sessionFactory.getServiceRegistry().getService( JdbcServices.class ).getDialect();
RenderingContext renderingContext = new RenderingContext() {
private int aliasCount;
private int explicitParameterCount;
@ -122,6 +134,16 @@ public String getCastType(Class javaType) {
}
return hibernateType.getName();
}
@Override
public Dialect getDialect() {
return dialect;
}
@Override
public LiteralHandlingMode getCriteriaLiteralHandlingMode() {
return criteriaLiteralHandlingMode;
}
};
return criteria.interpret( renderingContext ).buildCompiledQuery(

View File

@ -8,6 +8,9 @@
import javax.persistence.criteria.ParameterExpression;
import org.hibernate.dialect.Dialect;
import org.hibernate.query.criteria.LiteralHandlingMode;
/**
* Used to provide a context and services to the rendering.
*
@ -19,7 +22,7 @@ public interface RenderingContext {
*
* @return The generated correlation name
*/
public String generateAlias();
String generateAlias();
/**
* Register parameters explicitly encountered in the criteria query.
@ -28,7 +31,7 @@ public interface RenderingContext {
*
* @return The JPA-QL parameter name
*/
public ExplicitParameterInfo registerExplicitParameter(ParameterExpression<?> criteriaQueryParameter);
ExplicitParameterInfo registerExplicitParameter(ParameterExpression<?> criteriaQueryParameter);
/**
* Register a parameter that was not part of the criteria query (at least not as a parameter).
@ -38,7 +41,7 @@ public interface RenderingContext {
*
* @return The JPA-QL parameter name
*/
public String registerLiteralParameterBinding(Object literal, Class javaType);
String registerLiteralParameterBinding(Object literal, Class javaType);
/**
* Given a java type, determine the proper cast type name.
@ -47,5 +50,21 @@ public interface RenderingContext {
*
* @return The cast type name.
*/
public String getCastType(Class javaType);
String getCastType(Class javaType);
/**
* Current Dialect.
*
* @return Dialect
*/
Dialect getDialect();
/**
* How literals are going to be handled.
*
* @return literal handling strategy
*/
default LiteralHandlingMode getCriteriaLiteralHandlingMode() {
return LiteralHandlingMode.AUTO;
}
}

View File

@ -8,6 +8,7 @@
import java.io.Serializable;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
import org.hibernate.query.criteria.internal.ParameterRegistry;
import org.hibernate.query.criteria.internal.ValueHandlerFactory;
@ -46,11 +47,31 @@ public void registerParameters(ParameterRegistry registry) {
@SuppressWarnings({ "unchecked" })
public String render(RenderingContext renderingContext) {
if ( ValueHandlerFactory.isNumeric( literal ) ) {
return ValueHandlerFactory.determineAppropriateHandler( (Class) literal.getClass() ).render( literal );
}
// else...
LiteralHandlingMode literalHandlingMode = renderingContext.getCriteriaLiteralHandlingMode();
switch ( literalHandlingMode ) {
case AUTO:
if ( ValueHandlerFactory.isNumeric( literal ) ) {
return ValueHandlerFactory.determineAppropriateHandler( (Class) literal.getClass() ).render( literal );
}
else {
return bindLiteral( renderingContext );
}
case BIND:
return bindLiteral( renderingContext );
case INLINE:
Object literalValue = literal;
if ( String.class.equals( literal.getClass() ) ) {
literalValue = renderingContext.getDialect().inlineLiteral( (String) literal );
}
return ValueHandlerFactory.determineAppropriateHandler( (Class) literal.getClass() ).render( literalValue );
default:
throw new IllegalArgumentException( "Unexpected LiteralHandlingMode: " + literalHandlingMode );
}
}
private String bindLiteral(RenderingContext renderingContext) {
final String parameterName = renderingContext.registerLiteralParameterBinding( getLiteral(), getJavaType() );
return ':' + parameterName;
}

View File

@ -0,0 +1,127 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpa.test.criteria.literal;
import java.util.List;
import java.util.Map;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Tuple;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.testing.RequiresDialect;
import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider;
import org.junit.Before;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* @author Vlad Mihalcea
*/
public abstract class AbstractCriteriaLiteralHandlingModeTest extends BaseEntityManagerFunctionalTestCase {
private PreparedStatementSpyConnectionProvider connectionProvider;
@Override
protected Map getConfig() {
Map config = super.getConfig();
config.put(
org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER,
connectionProvider
);
return config;
}
@Override
public void buildEntityManagerFactory() throws Exception {
connectionProvider = new PreparedStatementSpyConnectionProvider();
super.buildEntityManagerFactory();
}
@Override
public void releaseResources() {
super.releaseResources();
connectionProvider.stop();
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] {
Book.class
};
}
@Before
public void init() {
doInJPA( this::entityManagerFactory, entityManager -> {
Book book = new Book();
book.id = 1;
book.name = bookName();
entityManager.persist( book );
} );
}
@Test
public void testLiteralHandlingMode() throws Exception {
doInJPA( this::entityManagerFactory, entityManager -> {
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaQuery<Tuple> query = cb.createQuery( Tuple.class );
final Root<Book> entity = query.from( Book.class );
query.where(
cb.and(
cb.equal(
entity.get( "id" ),
cb.literal( 1 )
),
cb.equal(
entity.get( "name" ),
cb.literal( bookName() )
)
)
);
query.multiselect(
cb.literal( "abc" ),
entity.get( "name" )
);
connectionProvider.clear();
List<Tuple> tuples = entityManager.createQuery( query )
.getResultList();
assertEquals( 1, tuples.size() );
assertNotNull( connectionProvider.getPreparedStatement( expectedSQL() ) );
} );
}
protected abstract String expectedSQL();
@Entity(name = "Book")
public static class Book {
@Id
private Integer id;
private String name;
}
protected String bookName() {
return "Vlad's High-Performance Java Persistence";
}
}

View File

@ -0,0 +1,26 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpa.test.criteria.literal;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.testing.RequiresDialect;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* @author Vlad Mihalcea
*/
@RequiresDialect(H2Dialect.class)
public class CriteriaLiteralHandlingModeAutoTest extends AbstractCriteriaLiteralHandlingModeTest {
protected String expectedSQL() {
return "select 'abc' as col_0_0_, abstractcr0_.name as col_1_0_ from Book abstractcr0_ where abstractcr0_.id=1 and abstractcr0_.name=?";
}
}

View File

@ -0,0 +1,36 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpa.test.criteria.literal;
import java.util.Map;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.testing.RequiresDialect;
/**
* @author Vlad Mihalcea
*/
@RequiresDialect(H2Dialect.class)
public class CriteriaLiteralHandlingModeBindTest extends AbstractCriteriaLiteralHandlingModeTest {
@Override
protected Map getConfig() {
Map config = super.getConfig();
config.put(
AvailableSettings.CRITERIA_LITERAL_HANDLING_MODE,
LiteralHandlingMode.BIND
);
return config;
}
protected String expectedSQL() {
return "select 'abc' as col_0_0_, abstractcr0_.name as col_1_0_ from Book abstractcr0_ where abstractcr0_.id=? and abstractcr0_.name=?";
}
}

View File

@ -0,0 +1,36 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpa.test.criteria.literal;
import java.util.Map;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.testing.RequiresDialect;
/**
* @author Vlad Mihalcea
*/
@RequiresDialect(H2Dialect.class)
public class CriteriaLiteralHandlingModeInlineTest extends AbstractCriteriaLiteralHandlingModeTest {
@Override
protected Map getConfig() {
Map config = super.getConfig();
config.put(
AvailableSettings.CRITERIA_LITERAL_HANDLING_MODE,
LiteralHandlingMode.INLINE
);
return config;
}
protected String expectedSQL() {
return "select 'abc' as col_0_0_, abstractcr0_.name as col_1_0_ from Book abstractcr0_ where abstractcr0_.id=1 and abstractcr0_.name='Vlad''s High-Performance Java Persistence'";
}
}

View File

@ -0,0 +1,42 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.jpa.test.criteria.literal;
import java.util.Map;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.MySQLDialect;
import org.hibernate.query.criteria.LiteralHandlingMode;
import org.hibernate.testing.RequiresDialect;
/**
* @author Vlad Mihalcea
*/
@RequiresDialect(MySQLDialect.class)
public class MySQLCriteriaLiteralHandlingModeInlineTest extends AbstractCriteriaLiteralHandlingModeTest {
@Override
protected Map getConfig() {
Map config = super.getConfig();
config.put(
AvailableSettings.CRITERIA_LITERAL_HANDLING_MODE,
LiteralHandlingMode.INLINE
);
return config;
}
protected String expectedSQL() {
return "select 'abc' as col_0_0_, abstractcr0_.name as col_1_0_ from Book abstractcr0_ where abstractcr0_.id=1 and abstractcr0_.name='Vlad\\\\''s High-Performance Java Persistence'";
}
@Override
protected String bookName() {
return "Vlad\\'s High-Performance Java Persistence";
}
}

View File

@ -11,6 +11,7 @@
import java.util.Map;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@ -141,6 +142,50 @@ public void testLiteralsInWhereClause() throws Exception {
} );
}
@Test
public void testNumericLiteralsInWhereClause() throws Exception {
doInJPA( this::entityManagerFactory, entityManager -> {
testNumericLiterals(
entityManager,
"select 'abc' as col_0_0_, criteriali0_.name as col_1_0_ from Book criteriali0_ where criteriali0_.id=1"
);
} );
}
@Test
public void testNumericLiteralsInWhereClauseUsingBindParameters() throws Exception {
doInJPA( this::entityManagerFactory, entityManager -> {
testNumericLiterals(
entityManager,
"select 'abc' as col_0_0_, criteriali0_.name as col_1_0_ from Book criteriali0_ where criteriali0_.id=1"
);
} );
}
private void testNumericLiterals(EntityManager entityManager, String expectedSQL) {
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaQuery<Tuple> query = cb.createQuery( Tuple.class );
final Root<Book> entity = query.from( Book.class );
query.where( cb.equal(
entity.get( "id" ),
cb.literal( 1 )
) );
query.multiselect(
cb.literal( "abc" ),
entity.get( "name" )
);
connectionProvider.clear();
List<Tuple> tuples = entityManager.createQuery( query )
.getResultList();
assertEquals( 1, tuples.size() );
assertNotNull( connectionProvider.getPreparedStatement(expectedSQL) );
}
@Test
public void testCriteriaParameters() throws Exception {
doInJPA( this::entityManagerFactory, entityManager -> {