Re-work IN LIST support in JdbcMapper to re-use created arrays, and free them after use

This commit is contained in:
Travis Burtrum 2018-05-12 00:30:57 -04:00
parent 17a429399c
commit c2204b4635
6 changed files with 151 additions and 56 deletions

View File

@ -2,6 +2,7 @@ package com.moparisthebest.jdbc;
import javax.naming.Context;
import java.io.Closeable;
import java.sql.Array;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
@ -48,6 +49,16 @@ public class TryClose { // tries to close certain object types
}
}
public static void tryClose(Array obj) {
if (obj == null)
return;
try {
obj.free();
} catch (Throwable e) {
// ignore...
}
}
public static void tryClose(Closeable obj) {
if (obj == null)
return;

View File

@ -22,6 +22,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
//IFJAVA8_START
import java.time.*;
import java.util.stream.Stream;
//IFJAVA8_END
import static com.moparisthebest.jdbc.TryClose.tryClose;
@ -69,7 +70,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
static TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, connectionType, jdbcMapperType,
byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, calendarType, cleanerType, enumType;
//IFJAVA8_START
static TypeMirror instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType;
static TypeMirror streamType, instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType;
//IFJAVA8_END
private TypeElement cleanerElement;
private JdbcMapper.DatabaseType defaultDatabaseType;
@ -105,6 +106,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
blobType = elements.getTypeElement(Blob.class.getCanonicalName()).asType();
calendarType = elements.getTypeElement(Calendar.class.getCanonicalName()).asType();
//IFJAVA8_START
streamType = types.getDeclaredType(elements.getTypeElement(Stream.class.getCanonicalName()), types.getWildcardType(null, null));
instantType = elements.getTypeElement(Instant.class.getCanonicalName()).asType();
localDateTimeType = elements.getTypeElement(LocalDateTime.class.getCanonicalName()).asType();
localDateType = elements.getTypeElement(LocalDate.class.getCanonicalName()).asType();
@ -308,6 +310,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
// build query and bind param order
final List<VariableElement> bindParams = new ArrayList<VariableElement>();
final Map<String, SpecialVariableElement> inListBindParams = new LinkedHashMap<>();
final String sqlStatement;
String calendarName = null, cleanerName = null;
CompileTimeResultSetMapper.MaxRows maxRows = CompileTimeResultSetMapper.MaxRows.getMaxRows(sql.maxRows());
@ -349,6 +352,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
final Matcher bindParamMatcher = paramPattern.matcher(sql.value());
final StringBuffer sb = new StringBuffer();
int inListBindParamsIdx = -1;
while (bindParamMatcher.find()) {
final String paramName = bindParamMatcher.group(7);
final VariableElement bindParam = paramMap.get(paramName);
@ -380,7 +384,12 @@ public class JdbcMapperProcessor extends AbstractProcessor {
} else {
if(clobBlob != null)
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "cannot combine in/not in and clob/blob", bindParam);
bindParams.add(new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.IN_LIST));
SpecialVariableElement inListBindParam = inListBindParams.get(paramName);
if(inListBindParam == null) {
inListBindParam = new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.IN_LIST, ++inListBindParamsIdx);
inListBindParams.put(paramName, inListBindParam);
}
bindParams.add(inListBindParam);
final boolean not = bindParamMatcher.group(4) != null;
final String replacement;
switch (databaseType) {
@ -433,7 +442,12 @@ public class JdbcMapperProcessor extends AbstractProcessor {
w.write("\t\tPreparedStatement ps = null;\n");
if (parsedSQl.isSelect())
w.write("\t\tResultSet rs = null;\n");
w.write("\t\ttry {\n\t\t\tps = ");
if(!inListBindParams.isEmpty())
w.append("\t\tfinal Array[] _bindArrays = new Array[").append(Integer.toString(inListBindParams.size())).append("];\n");
w.write("\t\ttry {\n");
for (final SpecialVariableElement param : inListBindParams.values())
setArray(w, databaseType, arrayNumberTypeName, arrayStringTypeName, param);
w.write("\t\t\tps = ");
final boolean cachePreparedStatements = sql.cachePreparedStatement().combine(defaultCachePreparedStatements);
if (cachePreparedStatements) {
w.write("this.prepareStatement(");
@ -450,7 +464,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
// now bind parameters
int count = 0;
for (final VariableElement param : bindParams)
setObject(w, ++count, databaseType, arrayNumberTypeName, arrayStringTypeName, param);
setObject(w, ++count, param);
boolean closeRs = true;
if (!parsedSQl.isSelect()) {
@ -484,6 +498,8 @@ public class JdbcMapperProcessor extends AbstractProcessor {
if (!sqlExceptionThrown)
w.write("\t\t} catch(SQLException e) {\n\t\t\tthrow new RuntimeException(e);\n");
w.write("\t\t} finally {\n");
if(!inListBindParams.isEmpty())
w.append("\t\t\tfor(final Array _bindArray : _bindArrays)\n\t\t\t\ttryClose(_bindArray);\n");
if (parsedSQl.isSelect())
w.write("\t\t\ttryClose(rs);\n");
if (!cachePreparedStatements)
@ -499,6 +515,10 @@ public class JdbcMapperProcessor extends AbstractProcessor {
w.write("\t\t\tif(e instanceof SQLException)\n\t\t\t\tthrow (SQLException)e;\n");
w.write("\t\t\tif(e instanceof RuntimeException)\n\t\t\t\tthrow (RuntimeException)e;\n");
w.write("\t\t\tthrow new RuntimeException(e);\n");
if(!inListBindParams.isEmpty()) {
w.write("\t\t} finally {\n");
w.append("\t\t\tfor(final Array _bindArray : _bindArrays)\n\t\t\t\ttryClose(_bindArray);\n");
}
}
w.write("\t\t}\n");
@ -658,44 +678,57 @@ public class JdbcMapperProcessor extends AbstractProcessor {
w.write("\t}\n");
}
private void setObject(final Writer w, final int index, final JdbcMapper.DatabaseType databaseType, final String arrayNumberTypeName, final String arrayStringTypeName, final VariableElement param) throws SQLException, IOException {
String variableName = param.getSimpleName().toString();
final TypeMirror o = param.asType();
w.write("\t\t\t");
String method = null;
private enum InListArgType {
ARRAY,
COLLECTION,
STREAM,
}
// special behavior
if (param instanceof SpecialVariableElement) {
final SpecialVariableElement specialParam = (SpecialVariableElement) param;
switch (specialParam.specialType) {
case IN_LIST: {
final boolean collection;
final TypeMirror componentType;
if (o.getKind() == TypeKind.ARRAY) {
collection = false;
componentType = ((ArrayType) o).getComponentType();
} else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) {
collection = true;
final DeclaredType dt = (DeclaredType) o;
if (dt.getTypeArguments().isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax requires a Collection with a generic type parameter" + o.toString(), specialParam.delegate);
return;
}
componentType = dt.getTypeArguments().get(0);
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax only valid on Collections or arrays" + o.toString(), specialParam.delegate);
return;
}
w.write("ps.setArray(");
w.write(Integer.toString(index));
w.write(", ");
final String type = types.isAssignable(componentType, numberType) ? arrayNumberTypeName : arrayStringTypeName;
switch (databaseType) {
case ORACLE:
w.write("conn.unwrap(oracle.jdbc.OracleConnection.class).createOracleArray(\"");
private void setArray(final Writer w, final JdbcMapper.DatabaseType databaseType, final String arrayNumberTypeName, final String arrayStringTypeName, final SpecialVariableElement specialParam) throws IOException {
final String variableName = specialParam.getSimpleName().toString();
final TypeMirror o = specialParam.asType();
// todo: if oracle driver is not on compile-time classpath, would need to do:
// we could also create a fake-oracle module that just had a oracle.jdbc.OracleConnection class implementing createOracleArray()...
final InListArgType inListArgType;
final TypeMirror componentType;
if (o.getKind() == TypeKind.ARRAY) {
inListArgType = InListArgType.ARRAY;
componentType = ((ArrayType) o).getComponentType();
} else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) {
inListArgType = InListArgType.COLLECTION;
final DeclaredType dt = (DeclaredType) o;
if (dt.getTypeArguments().isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax requires a Collection with a generic type parameter" + o.toString(), specialParam.delegate);
return;
}
componentType = dt.getTypeArguments().get(0);
//IFJAVA8_START
} else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, streamType)) {
inListArgType = InListArgType.STREAM;
final DeclaredType dt = (DeclaredType) o;
if (dt.getTypeArguments().isEmpty()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax requires a Stream with a generic type parameter" + o.toString(), specialParam.delegate);
return;
}
componentType = dt.getTypeArguments().get(0);
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax only valid on Collections or arrays" + o.toString(), specialParam.delegate);
return;
}
//IFJAVA8_END
/*IFJAVA6_START
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL in list syntax only valid on Collections or arrays" + o.toString(), specialParam.delegate);
return;
}
IFJAVA6_END*/
w.append("\t\t\t_bindArrays[").append(Integer.toString(specialParam.index)).append("] = ");
final String type = types.isAssignable(componentType, numberType) ? arrayNumberTypeName : arrayStringTypeName;
switch (databaseType) {
case ORACLE:
w.write("conn.unwrap(oracle.jdbc.OracleConnection.class).createOracleArray(\"");
// todo: if oracle driver is not on compile-time classpath, would need to do:
// we could also create a fake-oracle module that just had a oracle.jdbc.OracleConnection class implementing createOracleArray()...
/*
private static final Class<?> oracleConnection;
private static final Method createArray;
@ -713,20 +746,42 @@ public class JdbcMapperProcessor extends AbstractProcessor {
createArray = ca;
}
*/
//w.write("(Array) createArray.invoke(conn.unwrap(oracleConnection), \"");
break;
case STANDARD:
w.write("conn.createArrayOf(\"");
break;
default:
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "default DatabaseType? should never happen!!", specialParam.delegate);
}
w.write(type);
w.write("\", ");
w.write(variableName);
if (collection)
w.write(".toArray()");
w.write("));\n");
//w.write("(Array) createArray.invoke(conn.unwrap(oracleConnection), \"");
break;
case STANDARD:
w.write("conn.createArrayOf(\"");
break;
default:
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "default DatabaseType? should never happen!!", specialParam.delegate);
}
w.write(type);
w.write("\", ");
w.write(variableName);
switch (inListArgType) {
case COLLECTION:
w.append(".toArray(new ").append(componentType.toString()).append("[").append(variableName).append(".size()])");
break;
case STREAM:
w.append(".toArray(").append(componentType.toString()).append("[]::new)");
break;
}
w.write(");\n");
}
private void setObject(final Writer w, final int index, final VariableElement param) throws IOException {
String variableName = param.getSimpleName().toString();
final TypeMirror o = param.asType();
w.write("\t\t\t");
String method = null;
// special behavior
if (param instanceof SpecialVariableElement) {
final SpecialVariableElement specialParam = (SpecialVariableElement) param;
switch (specialParam.specialType) {
case IN_LIST: {
w.write("ps.setArray(");
w.write(Integer.toString(index));
w.append(", _bindArrays[").append(Integer.toString(specialParam.index)).append("]);\n");
return;
}
case BLOB: {

View File

@ -20,15 +20,25 @@ class SpecialVariableElement implements VariableElement {
final VariableElement delegate;
final SpecialType specialType;
final String blobStringCharset;
final int index;
SpecialVariableElement(final VariableElement delegate, final SpecialType specialType) {
this(delegate, specialType, null);
this(delegate, specialType, null, 0);
}
SpecialVariableElement(final VariableElement delegate, final SpecialType specialType, final int index) {
this(delegate, specialType, null, index);
}
SpecialVariableElement(final VariableElement delegate, final SpecialType specialType, final String blobStringCharset) {
this(delegate, specialType, blobStringCharset, 0);
}
SpecialVariableElement(final VariableElement delegate, final SpecialType specialType, final String blobStringCharset, final int index) {
this.delegate = delegate;
this.specialType = specialType;
this.blobStringCharset = blobStringCharset;
this.index = index;
}
@Override

View File

@ -120,4 +120,5 @@ TODO
* DOCUMENTATION!!!!!
* sql other than select return boolean, int > 0 ?
* @RunInTransaction void support
* @RunInTransaction void support
* QueryMapper mapping errors should be clearer, especially if a .finish(ResultSet) throws an error

View File

@ -226,8 +226,17 @@ public interface PersonDAO extends JdbcMapper {
@JdbcMapper.SQL("SELECT str_val FROM val WHERE val_no = 4")
SameClassPathEnumPerson.FirstName getSameClassPathEnumNull() throws SQLException;
@SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos}")
List<FieldPerson> getPeopleInListArray(Long[] personNos);
@SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos}")
List<FieldPerson> getPeopleInListCollection(Collection<Long> personNos);
//IFJAVA8_START
@SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos} OR {first_name IN names} OR {last_name IN names}")
Stream<FieldPerson> getPeopleInListStream(Stream<Long> personNos, Stream<String> names);
@JdbcMapper.SQL("SELECT birth_date FROM person WHERE person_no = {personNo}")
Instant getBirthDateInstant(long personNo);

View File

@ -226,8 +226,17 @@ public interface PrestoPersonDAO extends PersonDAO {
@JdbcMapper.SQL("SELECT str_val FROM val WHERE val_no = 4")
SameClassPathEnumPerson.FirstName getSameClassPathEnumNull() throws SQLException;
@SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos}")
List<FieldPerson> getPeopleInListArray(Long[] personNos);
@SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos}")
List<FieldPerson> getPeopleInListCollection(Collection<Long> personNos);
//IFJAVA8_START
@SQL("SELECT first_name, last_name FROM person WHERE {person_no IN personNos} OR {first_name IN names} OR {last_name IN names}")
Stream<FieldPerson> getPeopleInListStream(Stream<Long> personNos, Stream<String> names);
@JdbcMapper.SQL("SELECT birth_date FROM person WHERE person_no = {personNo}")
Instant getBirthDateInstant(long personNo);