Implement JdbcMapper.DatabaseType.BIND support

This commit is contained in:
Travis Burtrum 2018-05-22 00:08:14 -04:00
parent 2f8f293ed2
commit 5522014f8e
10 changed files with 166 additions and 47 deletions

View File

@ -128,9 +128,11 @@ public interface JdbcMapper extends Closeable {
public enum DatabaseType {
DEFAULT(null, null),
BIND(null, null),
STANDARD("numeric", "text"),
UNNEST("NUMERIC", "VARCHAR"),
ORACLE("ARRAY_NUM_TYPE", "ARRAY_STR_TYPE");
ORACLE("ARRAY_NUM_TYPE", "ARRAY_STR_TYPE"),
;
public final String arrayNumberTypeName, arrayStringTypeName;

View File

@ -1,11 +1,14 @@
package com.moparisthebest.jdbc.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class InListUtil {
public static final int defaultMaxSize = Integer.parseInt(System.getProperty("QueryMapper.BindInList.defaultMaxSize", "999"));
private static <T> List<List<T>> split(final List<T> list, final int maxLength) {
final int listSize = list.size();
final List<List<T>> ret = new ArrayList<List<T>>();
@ -49,11 +52,35 @@ public class InListUtil {
return sb.append(")").toString();
}
public static <T> String toInList(final String fieldName, final Collection<T> items, final int maxSize) {
private static <T> String toInListNonEmpty(final String fieldName, final Collection<T> items, final int maxSize) {
return toInList(fieldName, items, maxSize, " IN (", " OR ");
}
public static <T> String toNotInList(final String fieldName, final Collection<T> items, final int maxSize) {
private static <T> String toNotInListNonEmpty(final String fieldName, final Collection<T> items, final int maxSize) {
return toInList(fieldName, items, maxSize, " NOT IN (", " AND ");
}
public static <T> String toInList(final String fieldName, final Collection<T> items, final int maxSize) {
return items == null || items.isEmpty() ? "(0=1)" : toInListNonEmpty(fieldName, items, maxSize);
}
public static <T> String toNotInList(final String fieldName, final Collection<T> items, final int maxSize) {
return items == null || items.isEmpty() ? "(1=1)" : toNotInListNonEmpty(fieldName, items, maxSize);
}
public static <T> String toInList(final String fieldName, final Collection<T> items) {
return toInList(fieldName, items, defaultMaxSize);
}
public static <T> String toNotInList(final String fieldName, final Collection<T> items) {
return toInList(fieldName, items, defaultMaxSize);
}
public static <T> String toInList(final String fieldName, final T[] items) {
return toInList(fieldName, Arrays.asList(items), defaultMaxSize);
}
public static <T> String toNotInList(final String fieldName, final T[] items) {
return toInList(fieldName, Arrays.asList(items), defaultMaxSize);
}
}

View File

@ -126,7 +126,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
enumType = types.getDeclaredType(elements.getTypeElement(Enum.class.getCanonicalName()), types.getWildcardType(null, null));
final String databaseType = processingEnv.getOptions().get("jdbcMapper.databaseType");
defaultDatabaseType = databaseType == null || databaseType.isEmpty() ? JdbcMapper.DatabaseType.STANDARD : JdbcMapper.DatabaseType.valueOf(databaseType);
defaultDatabaseType = databaseType == null || databaseType.isEmpty() ? JdbcMapper.DatabaseType.STANDARD : JdbcMapper.DatabaseType.valueOf(databaseType.toUpperCase());
defaultArrayNumberTypeName = processingEnv.getOptions().get("jdbcMapper.arrayNumberTypeName");
if (defaultArrayNumberTypeName == null || defaultArrayNumberTypeName.isEmpty())
defaultArrayNumberTypeName = defaultDatabaseType.arrayNumberTypeName;
@ -194,9 +194,13 @@ public class JdbcMapperProcessor extends AbstractProcessor {
case UNNEST:
arrayInList = new UnNestArrayInList(arrayNumberTypeName, arrayStringTypeName);
break;
case BIND:
//arrayInList = BindInList.instance();
arrayInList = null; // todo: do something here
break;
default:
// no support
arrayInList = null;
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.Mapper.databaseType unsupported", element);
continue;
}
} else {
arrayInList = null;
@ -386,7 +390,16 @@ public class JdbcMapperProcessor extends AbstractProcessor {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "cannot combine in/not in and clob/blob", bindParam);
SpecialVariableElement inListBindParam = inListBindParams.get(paramName);
if(inListBindParam == null) {
inListBindParam = new SpecialVariableElement(bindParam, SpecialVariableElement.SpecialType.IN_LIST, ++inListBindParamsIdx);
inListBindParam = new SpecialVariableElement(bindParam,
databaseType == JdbcMapper.DatabaseType.BIND ? SpecialVariableElement.SpecialType.BIND_IN_LIST : SpecialVariableElement.SpecialType.IN_LIST,
++inListBindParamsIdx);
if(databaseType == JdbcMapper.DatabaseType.BIND) {
final TypeMirror o = bindParam.asType();
if(o.getKind() == TypeKind.DECLARED && types.isAssignable(o, streamType)) {
// todo: this allows them to name underscore names and create collisions, I'm ok with it
inListBindParam.setName("_" + inListBindParam.getSimpleName() + "StreamAsBindArray");
}
}
inListBindParams.put(paramName, inListBindParam);
}
bindParams.add(inListBindParam);
@ -408,6 +421,11 @@ public class JdbcMapperProcessor extends AbstractProcessor {
"(" + inColumnName + " NOT IN(UNNEST(?)))" :
"(" + inColumnName + " IN(UNNEST(?)))";
break;
case BIND:
replacement = "REPLACEMEWITHUNQUOTEDQUOTEPLZ + com.moparisthebest.jdbc.util.InListUtil.to" +
(not ? "Not" : "") + "InList(REPLACEMEWITHUNQUOTEDQUOTEPLZ"
+ inColumnName + "REPLACEMEWITHUNQUOTEDQUOTEPLZ, " + inListBindParam.getName() + ") + REPLACEMEWITHUNQUOTEDQUOTEPLZ";
break;
default:
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "default DatabaseType? should never happen!!", bindParam);
return false;
@ -441,13 +459,14 @@ public class JdbcMapperProcessor extends AbstractProcessor {
processingEnv.getMessager().printMessage(warnOnUnusedParams != null ? Diagnostic.Kind.MANDATORY_WARNING : Diagnostic.Kind.ERROR, String.format("@JdbcMapper.SQL method has unused parameter '%s'", unusedParam.getKey()), unusedParam.getValue());
}
}
final boolean notBindInList = !inListBindParams.isEmpty() && databaseType != JdbcMapper.DatabaseType.BIND;
final SQLParser parsedSQl = ManualSQLParser.getSQLParser(sql, parser, sqlStatement);
// now implementation
w.write("\t\tPreparedStatement ps = null;\n");
if (parsedSQl.isSelect())
w.write("\t\tResultSet rs = null;\n");
if(!inListBindParams.isEmpty())
if(notBindInList)
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())
@ -463,13 +482,21 @@ public class JdbcMapperProcessor extends AbstractProcessor {
w.write("conn.prepareStatement(");
}
w.write('"');
w.write(sqlStatement.replace("\"", "\\\"").replace("\n", "\\n\" +\n\t\t\t \""));
w.write(sqlStatement.replace("\"", "\\\"")
.replace("\n", "\\n\" +\n\t\t\t \"")
.replace("REPLACEMEWITHUNQUOTEDQUOTEPLZ", "\""));
w.write("\");\n");
// now bind parameters
if(!inListBindParams.isEmpty() && databaseType == JdbcMapper.DatabaseType.BIND) {
w.write("\t\t\tint psParamCount = 0;\n");
for (final VariableElement param : bindParams)
setObject(w, "++psParamCount", param);
} else {
int count = 0;
for (final VariableElement param : bindParams)
setObject(w, ++count, param);
setObject(w, Integer.toString(++count), param);
}
boolean closeRs = true;
if (!parsedSQl.isSelect()) {
@ -503,7 +530,7 @@ 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())
if(notBindInList)
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");
@ -520,7 +547,7 @@ 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()) {
if(notBindInList) {
w.write("\t\t} finally {\n");
w.append("\t\t\tfor(final Array _bindArray : _bindArrays)\n\t\t\t\ttryClose(_bindArray);\n");
}
@ -690,7 +717,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
}
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 String variableName = specialParam.getName();
final TypeMirror o = specialParam.asType();
final InListArgType inListArgType;
@ -726,6 +753,16 @@ public class JdbcMapperProcessor extends AbstractProcessor {
return;
}
IFJAVA6_END*/
if(databaseType == JdbcMapper.DatabaseType.BIND) {
specialParam.setComponentTypeString(componentType.toString());
// only need to do something special for streams here...
if(inListArgType == InListArgType.STREAM) {
// todo: is array or collection better here, doesn't matter to us...
w.append("\t\t\tfinal ").append(specialParam.getComponentTypeString()).append("[] ").append(specialParam.getName()).append(" = ");
w.append(specialParam.getSimpleName()).append(".toArray(").append(specialParam.getComponentTypeString()).append("[]::new);\n");
}
return; // we don't want any of the following
}
w.append("\t\t\t_bindArrays[").append(Integer.toString(specialParam.index)).append("] = ");
final String type = types.isAssignable(componentType, numberType) ? arrayNumberTypeName : arrayStringTypeName;
switch (databaseType) {
@ -774,7 +811,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
w.write(");\n");
}
private void setObject(final Writer w, final int index, final VariableElement param) throws IOException {
private void setObject(final Writer w, final String index, final VariableElement param) throws IOException {
String variableName = param.getSimpleName().toString();
final TypeMirror o = param.asType();
w.write("\t\t\t");
@ -784,16 +821,21 @@ public class JdbcMapperProcessor extends AbstractProcessor {
if (param instanceof SpecialVariableElement) {
final SpecialVariableElement specialParam = (SpecialVariableElement) param;
switch (specialParam.specialType) {
case BIND_IN_LIST: {
w.append("for(final ").append(specialParam.getComponentTypeString()).append(" _bindInListParam : ").append(specialParam.getName()).append(")\n");
w.append("\t\t\t\tps.setObject(").append(index).append(", _bindInListParam);\n");
return;
}
case IN_LIST: {
w.write("ps.setArray(");
w.write(Integer.toString(index));
w.write(index);
w.append(", _bindArrays[").append(Integer.toString(specialParam.index)).append("]);\n");
return;
}
case BLOB: {
if (types.isAssignable(o, stringType)) {
w.write("try {\n\t\t\t\tps.setBlob(");
w.write(Integer.toString(index));
w.write(index);
w.write(", ");
w.write(variableName);
w.write(" == null ? null : new java.io.ByteArrayInputStream(");
@ -884,7 +926,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
w.write("ps.set");
w.write(method);
w.write('(');
w.write(Integer.toString(index));
w.write(index);
w.write(", ");
w.write(variableName);
w.write(");\n");

View File

@ -12,6 +12,7 @@ import java.util.Set;
class SpecialVariableElement implements VariableElement {
enum SpecialType {
BIND_IN_LIST,
IN_LIST,
CLOB,
BLOB,
@ -22,6 +23,8 @@ class SpecialVariableElement implements VariableElement {
final String blobStringCharset;
final int index;
String name, componentTypeString;
SpecialVariableElement(final VariableElement delegate, final SpecialType specialType) {
this(delegate, specialType, null, 0);
}
@ -39,6 +42,23 @@ class SpecialVariableElement implements VariableElement {
this.specialType = specialType;
this.blobStringCharset = blobStringCharset;
this.index = index;
this.name = getSimpleName().toString();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getComponentTypeString() {
return componentTypeString;
}
public void setComponentTypeString(String componentTypeString) {
this.componentTypeString = componentTypeString;
}
@Override

View File

@ -3,6 +3,7 @@ package com.moparisthebest.jdbc;
import java.sql.Connection;
import java.util.*;
import static com.moparisthebest.jdbc.util.InListUtil.defaultMaxSize;
import static com.moparisthebest.jdbc.util.InListUtil.toInList;
/**
@ -10,8 +11,6 @@ import static com.moparisthebest.jdbc.util.InListUtil.toInList;
*/
public class BindInList implements InList {
private static final int defaultMaxSize = Integer.parseInt(System.getProperty("QueryMapper.BindInList.defaultMaxSize", "999"));
private static final InList instance = new BindInList();
public static InList instance() {

View File

@ -194,5 +194,21 @@
</plugins>
</build>
</profile>
<profile>
<id>bind</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<compilerArg>-AjdbcMapper.databaseType=BIND</compilerArg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -245,6 +245,9 @@ public interface QmDao extends JdbcMapper {
@SQL(selectStrVal)
ZoneOffset getZoneOffsetString(long valNo) throws SQLException;
@SQL("SELECT person_no, first_name, last_name, birth_date from person WHERE {person_no IN personNos} ORDER BY person_no")
List<FieldPerson> getFieldPeopleStream(Stream<Long> personNos) throws SQLException;
//IFJAVA8_END
@SQL("SELECT person_no, first_name, last_name, birth_date from person WHERE {person_no IN personNos} ORDER BY person_no")

View File

@ -10,6 +10,7 @@ import java.sql.SQLException;
import java.util.*;
//IFJAVA8_START
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.time.*;
//IFJAVA8_END
@ -453,6 +454,11 @@ public class QueryMapperQmDao implements QmDao {
return qm.toObject(selectStrVal, ZoneOffset.class, valNo);
}
@Override
public List<FieldPerson> getFieldPeopleStream(final Stream<Long> personNos) throws SQLException {
return lqm.toList("SELECT * from person WHERE " + inListReplace + " ORDER BY person_no", FieldPerson.class, lqm.inList("person_no", personNos.collect(Collectors.toList())));
}
//IFJAVA8_END
@Override

View File

@ -13,6 +13,7 @@ import java.util.List;
import java.util.Map;
//IFJAVA8_START
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.time.*;
//IFJAVA8_END
@ -332,6 +333,11 @@ public class QueryMapperTypeQmDao extends QueryMapperQmDao {
return qm.toType(selectStrVal, new TypeReference<ZoneOffset>() {}, valNo);
}
@Override
public List<FieldPerson> getFieldPeopleStream(final Stream<Long> personNos) throws SQLException {
return lqm.toType("SELECT * from person WHERE " + inListReplace + " ORDER BY person_no", new TypeReference<List<FieldPerson>>() {}, lqm.inList("person_no", personNos.collect(Collectors.toList())));
}
//IFJAVA8_END
@Override

View File

@ -75,16 +75,16 @@ public class QueryMapperTest {
static {
final Collection<String> jUrls = new ArrayList<String>();
final String jdbcUrl = System.getProperty("jdbcUrl", "all");
if(jdbcUrl.equals("all")) {
//jUrls.add("jdbc:hsqldb:mem:testDB"); // remove this from all since it supports custom InList, until generic inlist supported in JdbcMapper
if(jdbcUrl.equals("all") || jdbcUrl.equals("bind")) {
jUrls.add("jdbc:hsqldb:mem:testDB");
jUrls.add("jdbc:derby:memory:testDB;create=true");
jUrls.add("jdbc:h2:mem:testDB");
jUrls.add("jdbc:sqlite::memory:");
} else if(jdbcUrl.equals("hsql") || jdbcUrl.equals("hsqldb")) {
} else if(jdbcUrl.equals("hsql") || jdbcUrl.equals("hsqldb") || jdbcUrl.equals("unnest")) {
jUrls.add("jdbc:hsqldb:mem:testDB");
} else if(jdbcUrl.equals("derby")) {
jUrls.add("jdbc:derby:memory:testDB;create=true");
} else if(jdbcUrl.equals("h2")) {
} else if(jdbcUrl.equals("h2") || jdbcUrl.equals("any")) {
jUrls.add("jdbc:h2:mem:testDB");
} else if(jdbcUrl.equals("sqlite")) {
jUrls.add("jdbc:sqlite::memory:");
@ -521,28 +521,6 @@ public class QueryMapperTest {
assertArrayEquals(people, fromDb.toArray());
}
private static boolean supportsInList(final QmDao qm) {
return qm instanceof QueryMapperQmDao || supportsArrayInList(qm.getConnection());
}
@Test
public void testListQueryMapperList() throws SQLException {
if(!supportsInList(qm))
return;
final List<FieldPerson> fromDb = qm.getFieldPeople(Arrays.asList(people[0].getPersonNo(), people[1].getPersonNo(), people[2].getPersonNo()));
assertArrayEquals(people, fromDb.toArray());
}
@Test
public void testListQueryMapperListMultiple() throws SQLException {
if(!supportsInList(qm))
return;
final List<FieldPerson> fromDb = qm.getFieldPeopleByName(
Arrays.asList(people[0].getPersonNo(), people[1].getPersonNo(), people[2].getPersonNo()),
Arrays.asList(people[0].getFirstName(), people[1].getFirstName(), people[2].getFirstName()));
assertArrayEquals(people, fromDb.toArray());
}
@Test
public void testResultSetIterable() throws SQLException {
final ResultSetIterable<FieldPerson> rsi = qm.getThreePeopleResultSetIterable(people[0].getPersonNo(), people[1].getPersonNo(), people[2].getPersonNo());
@ -683,5 +661,25 @@ public class QueryMapperTest {
);
}
//IFJAVA8_END
@Test
public void testListQueryMapperStream() throws SQLException {
final List<FieldPerson> fromDb = qm.getFieldPeopleStream(Stream.of(people[0].getPersonNo(), people[1].getPersonNo(), people[2].getPersonNo()));
assertArrayEquals(people, fromDb.toArray());
}
//IFJAVA8_END
@Test
public void testListQueryMapperList() throws SQLException {
final List<FieldPerson> fromDb = qm.getFieldPeople(Arrays.asList(people[0].getPersonNo(), people[1].getPersonNo(), people[2].getPersonNo()));
assertArrayEquals(people, fromDb.toArray());
}
@Test
public void testListQueryMapperListMultiple() throws SQLException {
final List<FieldPerson> fromDb = qm.getFieldPeopleByName(
Arrays.asList(people[0].getPersonNo(), people[1].getPersonNo(), people[2].getPersonNo()),
Arrays.asList(people[0].getFirstName(), people[1].getFirstName(), people[2].getFirstName()));
assertArrayEquals(people, fromDb.toArray());
}
}