mirror of
https://github.com/moparisthebest/JdbcMapper
synced 2024-12-21 23:08:52 -05:00
Implement optional compile-time SQL checking with explain plan
This commit is contained in:
parent
1fa08bc74b
commit
a6c5fc7b52
@ -1,7 +1,9 @@
|
||||
package com.moparisthebest.jdbc.codegen;
|
||||
|
||||
import com.moparisthebest.jdbc.ArrayInList;
|
||||
import com.moparisthebest.jdbc.Cleaner;
|
||||
import com.moparisthebest.jdbc.MapperException;
|
||||
import com.moparisthebest.jdbc.OracleArrayInList;
|
||||
|
||||
import javax.annotation.processing.*;
|
||||
import javax.lang.model.SourceVersion;
|
||||
@ -28,7 +30,7 @@ import static com.moparisthebest.jdbc.codegen.JdbcMapperFactory.SUFFIX;
|
||||
* Created by mopar on 5/24/17.
|
||||
*/
|
||||
@SupportedAnnotationTypes("com.moparisthebest.jdbc.codegen.JdbcMapper.Mapper")
|
||||
@SupportedOptions({"jdbcMapper.databaseType", "jdbcMapper.arrayNumberTypeName", "jdbcMapper.arrayStringTypeName", "JdbcMapper.allowedMaxRowParamNames"})
|
||||
@SupportedOptions({"jdbcMapper.databaseType", "jdbcMapper.arrayNumberTypeName", "jdbcMapper.arrayStringTypeName", "jdbcMapper.allowedMaxRowParamNames", "jdbcMapper.sqlCheckerClass"})
|
||||
@SupportedSourceVersion(SourceVersion.RELEASE_5)
|
||||
public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
|
||||
@ -47,8 +49,8 @@ public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
RELEASE_8 = rl8;
|
||||
}
|
||||
|
||||
private static Types types;
|
||||
private static Messager messager;
|
||||
static Types types;
|
||||
static Messager messager;
|
||||
|
||||
public static Types getTypes() {
|
||||
return types;
|
||||
@ -58,14 +60,15 @@ public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
return messager;
|
||||
}
|
||||
|
||||
private TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, jdbcMapperType,
|
||||
byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, calendarType, cleanerType;
|
||||
static TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, jdbcMapperType,
|
||||
byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, calendarType, cleanerType, enumType;
|
||||
//IFJAVA8_START
|
||||
private TypeMirror instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType;
|
||||
static TypeMirror instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType;
|
||||
//IFJAVA8_END
|
||||
private TypeElement cleanerElement;
|
||||
private JdbcMapper.DatabaseType defaultDatabaseType;
|
||||
private String defaultArrayNumberTypeName, defaultArrayStringTypeName;
|
||||
private SQLChecker sqlChecker;
|
||||
private Set<String> allowedMaxRowParamNames;
|
||||
private CompileTimeResultSetMapper rsm;
|
||||
|
||||
@ -114,14 +117,25 @@ public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
cleanerElement = elements.getTypeElement(Cleaner.class.getCanonicalName());
|
||||
cleanerType = types.getDeclaredType(cleanerElement, types.getWildcardType(null, null));
|
||||
|
||||
final String databaseType = processingEnv.getOptions().get("JdbcMapper.databaseType");
|
||||
enumType = types.getDeclaredType(elements.getTypeElement(Enum.class.getCanonicalName()), types.getWildcardType(null, null));
|
||||
|
||||
final String databaseType = processingEnv.getOptions().get("jdbcMapper.databaseType");
|
||||
defaultDatabaseType = databaseType == null ? JdbcMapper.DatabaseType.STANDARD : JdbcMapper.DatabaseType.valueOf(databaseType);
|
||||
defaultArrayNumberTypeName = processingEnv.getOptions().get("JdbcMapper.arrayNumberTypeName");
|
||||
defaultArrayNumberTypeName = processingEnv.getOptions().get("jdbcMapper.arrayNumberTypeName");
|
||||
if (defaultArrayNumberTypeName == null || defaultArrayNumberTypeName.isEmpty())
|
||||
defaultArrayNumberTypeName = defaultDatabaseType.arrayNumberTypeName;
|
||||
defaultArrayStringTypeName = processingEnv.getOptions().get("JdbcMapper.arrayStringTypeName");
|
||||
defaultArrayStringTypeName = processingEnv.getOptions().get("jdbcMapper.arrayStringTypeName");
|
||||
if (defaultArrayStringTypeName == null || defaultArrayStringTypeName.isEmpty())
|
||||
defaultArrayStringTypeName = defaultDatabaseType.arrayStringTypeName;
|
||||
final String sqlCheckerClass = processingEnv.getOptions().get("jdbcMapper.sqlCheckerClass");
|
||||
if(sqlCheckerClass != null) {
|
||||
try {
|
||||
sqlChecker = (SQLChecker) Class.forName(sqlCheckerClass).newInstance();
|
||||
} catch (Throwable e) {
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
|
||||
"Error instantiating class specified by jdbcMapper.sqlCheckerClass, needs to implement SQLChecker and have a public no-arg constructor:" + toString(e));
|
||||
}
|
||||
}
|
||||
|
||||
String allowedMaxRowParamNames = processingEnv.getOptions().get("JdbcMapper.allowedMaxRowParamNames");
|
||||
if (allowedMaxRowParamNames == null || allowedMaxRowParamNames.isEmpty())
|
||||
@ -162,6 +176,22 @@ public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
arrayStringTypeName = !mapper.arrayStringTypeName().isEmpty() ? mapper.arrayStringTypeName() :
|
||||
(mapper.databaseType() == defaultDatabaseType ? defaultArrayStringTypeName : mapper.databaseType().arrayStringTypeName);
|
||||
}
|
||||
final ArrayInList arrayInList;
|
||||
if(sqlChecker != null) {
|
||||
switch(databaseType) {
|
||||
case ORACLE:
|
||||
arrayInList = new OracleArrayInList(arrayNumberTypeName, arrayStringTypeName);
|
||||
break;
|
||||
case STANDARD:
|
||||
arrayInList = new ArrayInList(arrayNumberTypeName, arrayStringTypeName);
|
||||
break;
|
||||
default:
|
||||
// no support
|
||||
arrayInList = null;
|
||||
}
|
||||
} else {
|
||||
arrayInList = null;
|
||||
}
|
||||
final String sqlParserMirror = getSqlParser(mapper).toString();
|
||||
//final SQLParser parser = new SimpleSQLParser();//(SQLParser)Class.forName(mapper.sqlParser().getCanonicalName()).newInstance();
|
||||
//final SQLParser parser = mapper.sqlParser().equals(SQLParser.class) ? new SimpleSQLParser() : mapper.sqlParser().newInstance();
|
||||
@ -380,7 +410,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
w.write("conn.prepareStatement(");
|
||||
}
|
||||
w.write('"');
|
||||
w.write(sqlStatement.replace("\n", "\\n").replace("\"", "\\\""));
|
||||
w.write(sqlStatement.replace("\"", "\\\"").replace("\n", "\\n\" +\n\t\t\t \""));
|
||||
w.write("\");\n");
|
||||
|
||||
// now bind parameters
|
||||
@ -439,6 +469,9 @@ public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
w.write("\t\t}\n");
|
||||
|
||||
w.write("\t}\n");
|
||||
|
||||
if(sqlChecker != null)
|
||||
sqlChecker.checkSql(processingEnv, genClass, mapper, databaseType, eeMethod, sqlStatement, bindParams, arrayInList);
|
||||
}
|
||||
|
||||
// look on super classes and interfaces recursively
|
||||
@ -501,10 +534,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
tryClose(w);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
final StringWriter sw = new StringWriter();
|
||||
sw.write('\n');
|
||||
e.printStackTrace(new PrintWriter(sw));
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage() + sw.toString(), element);
|
||||
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage() + toString(e), element);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -876,4 +906,20 @@ public class JdbcMapperProcessor extends AbstractProcessor {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static String toString(final Throwable e) {
|
||||
StringWriter sw = null;
|
||||
PrintWriter pw = null;
|
||||
try {
|
||||
sw = new StringWriter();
|
||||
sw.write(String.format("%n"));
|
||||
pw = new PrintWriter(sw);
|
||||
e.printStackTrace(new PrintWriter(pw));
|
||||
pw.flush();
|
||||
return sw.toString();
|
||||
} finally {
|
||||
tryClose(sw);
|
||||
tryClose(pw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
package com.moparisthebest.jdbc.codegen;
|
||||
|
||||
import com.moparisthebest.jdbc.ArrayInList;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
public interface SQLChecker {
|
||||
void checkSql(final ProcessingEnvironment processingEnv, final TypeElement classElement, final JdbcMapper.Mapper mapper, final JdbcMapper.DatabaseType databaseType, final ExecutableElement method, final String sqlStatement, final List<VariableElement> bindParams, final ArrayInList arrayInList) throws SQLException;
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
package com.moparisthebest.jdbc.codegen;
|
||||
|
||||
import com.moparisthebest.jdbc.ArrayInList;
|
||||
import com.moparisthebest.jdbc.QueryMapper;
|
||||
|
||||
import javax.annotation.processing.Messager;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.ArrayType;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.tools.Diagnostic;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.StringReader;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static com.moparisthebest.jdbc.TryClose.tryClose;
|
||||
import static com.moparisthebest.jdbc.codegen.JdbcMapperProcessor.*;
|
||||
|
||||
public class SimpleSQLChecker implements SQLChecker {
|
||||
@Override
|
||||
public void checkSql(final ProcessingEnvironment processingEnv, final TypeElement classElement, final JdbcMapper.Mapper mapper, final JdbcMapper.DatabaseType databaseType, final ExecutableElement method, final String sqlStatement, final List<VariableElement> bindParams, final ArrayInList arrayInList) throws SQLException {
|
||||
Connection conn = null;
|
||||
QueryMapper qm = null;
|
||||
try {
|
||||
conn = getConnection(classElement, mapper, databaseType);
|
||||
if (conn == null) {
|
||||
getMessager().printMessage(Diagnostic.Kind.ERROR, "connection must be non-null for testing", classElement);
|
||||
return;
|
||||
}
|
||||
conn.setAutoCommit(false);
|
||||
qm = new QueryMapper(conn);
|
||||
qm.executeUpdate(getSqlToExecute(classElement, conn, databaseType, sqlStatement), getFakeBindParams(bindParams, conn, arrayInList));
|
||||
} catch (Exception e) {
|
||||
handleException(getMessager(), e, classElement, method);
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
try {
|
||||
conn.rollback();
|
||||
conn.setAutoCommit(true);
|
||||
} catch (Throwable e) {
|
||||
// ignore
|
||||
}
|
||||
tryClose(conn);
|
||||
}
|
||||
tryClose(qm);
|
||||
}
|
||||
}
|
||||
|
||||
public Connection getConnection(final TypeElement classElement, final JdbcMapper.Mapper mapper, final JdbcMapper.DatabaseType databaseType) throws SQLException {
|
||||
if (mapper.jndiName().isEmpty()) {
|
||||
throw new RuntimeException("@JdbcMapper.jndiName must be non-null for testing");
|
||||
}
|
||||
return JdbcMapperFactory.connectionFactory(mapper.jndiName()).create();
|
||||
}
|
||||
|
||||
public String getSqlToExecute(final TypeElement classElement, final Connection conn, final JdbcMapper.DatabaseType databaseType, final String sqlStatement) {
|
||||
switch (databaseType) {
|
||||
case ORACLE:
|
||||
// oracle, being terrible, gives this error message for explain plans on MERGES
|
||||
// so we will just execute those directly instead...
|
||||
// ORA-00600: internal error code, arguments: [qctfrc : bfc], [22], [0], [5], [1], [1], [2], [371], [], [], [], []
|
||||
// we are assuming length is at least 5 after being trimmed, if anyone can come up with a valid SQL statement that is shorter let me know...
|
||||
if (sqlStatement.trim().substring(0, 5).toUpperCase().equals("MERGE"))
|
||||
return sqlStatement;
|
||||
return "EXPLAIN PLAN FOR " + sqlStatement;
|
||||
case STANDARD:
|
||||
return "EXPLAIN " + sqlStatement;
|
||||
}
|
||||
return sqlStatement;
|
||||
}
|
||||
|
||||
public void handleException(final Messager messager, final Exception e, final TypeElement classElement, final ExecutableElement method) {
|
||||
if (e.getMessage().startsWith("ORA-02291: integrity constraint"))
|
||||
return; // since we make up values and rollback we can ignore this, it means SQL is correct at least
|
||||
getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage(), method);
|
||||
}
|
||||
|
||||
public Collection<Object> getFakeBindParams(final List<VariableElement> bindParams, final Connection conn, final ArrayInList arrayInList) throws SQLException {
|
||||
final Collection<Object> ret = new ArrayList<Object>(bindParams.size());
|
||||
for (final VariableElement bindParam : bindParams) {
|
||||
ret.add(getFakeBindParam(bindParam, conn, arrayInList));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static final String defaultString = "n"; // oracle considers empty strings to be null...
|
||||
|
||||
public Object getFakeBindParam(final VariableElement param, final Connection conn, final ArrayInList arrayInList) throws SQLException {
|
||||
final TypeMirror o = param.asType();
|
||||
// special behavior
|
||||
if (param instanceof InListVariableElement) {
|
||||
final TypeMirror componentType;
|
||||
if (o.getKind() == TypeKind.ARRAY) {
|
||||
componentType = ((ArrayType) o).getComponentType();
|
||||
} else if (o.getKind() == TypeKind.DECLARED && types.isAssignable(o, collectionType)) {
|
||||
final DeclaredType dt = (DeclaredType) o;
|
||||
componentType = dt.getTypeArguments().get(0);
|
||||
} else {
|
||||
getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "invalid in-list-variable type, returning null", ((InListVariableElement) param).delegate);
|
||||
return null;
|
||||
}
|
||||
if (types.isAssignable(componentType, numberType)) {
|
||||
return arrayInList.toArray(conn, true, new Long[]{0L}); // todo: not quite right, oh well for now
|
||||
} else if (types.isAssignable(componentType, stringType) || types.isAssignable(componentType, enumType)) {
|
||||
return arrayInList.toArray(conn, false, new String[]{defaultString});
|
||||
} else {
|
||||
getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "invalid in-list-variable type, returning null", ((InListVariableElement) param).delegate);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
final JdbcMapper.Blob blob = param.getAnnotation(JdbcMapper.Blob.class);
|
||||
if (blob != null) {
|
||||
return new ByteArrayInputStream(new byte[1]);
|
||||
} else {
|
||||
final JdbcMapper.Clob clob = param.getAnnotation(JdbcMapper.Clob.class);
|
||||
if (clob != null) {
|
||||
return new StringReader(defaultString);
|
||||
}
|
||||
}
|
||||
// end special behavior
|
||||
// we are going to put most common ones up top so it should execute faster normally
|
||||
if (types.isAssignable(o, stringType)) {
|
||||
return defaultString;
|
||||
} else if (o.getKind().isPrimitive() || types.isAssignable(o, numberType)) {
|
||||
switch (o.getKind()) {
|
||||
case BOOLEAN:
|
||||
return true;
|
||||
case BYTE:
|
||||
case SHORT:
|
||||
case INT:
|
||||
return 0;
|
||||
case LONG:
|
||||
return 0L;
|
||||
case CHAR:
|
||||
return ' ';
|
||||
case FLOAT:
|
||||
return 0F;
|
||||
case DOUBLE:
|
||||
return 0D;
|
||||
}
|
||||
return null;//new BigDecimal(0D); // todo: not quite right, maybe good enough
|
||||
// java.util.Date support, put it in a Timestamp
|
||||
} else if (types.isAssignable(o, utilDateType)) {
|
||||
return new java.sql.Timestamp(0);
|
||||
}
|
||||
//IFJAVA8_START
|
||||
else if (types.isAssignable(o, instantType) || types.isAssignable(o, localDateTimeType) || types.isAssignable(o, zonedDateTimeType) || types.isAssignable(o, offsetDateTimeType)) {
|
||||
return new java.sql.Timestamp(0);
|
||||
} else if (types.isAssignable(o, localDateType)) {
|
||||
return new java.sql.Date(0);
|
||||
} else if (types.isAssignable(o, localTimeType) || types.isAssignable(o, offsetTimeType)) {
|
||||
return new java.sql.Time(0);
|
||||
}
|
||||
//IFJAVA8_END
|
||||
// CLOB support
|
||||
else if (types.isAssignable(o, readerType) || types.isAssignable(o, clobType)) {
|
||||
return new StringReader(defaultString);
|
||||
} else if (types.isAssignable(o, inputStreamType) || types.isAssignable(o, blobType) || types.isAssignable(o, fileType)) {
|
||||
return new ByteArrayInputStream(new byte[1]);
|
||||
} else if (types.isAssignable(o, byteArrayType)) {
|
||||
return new byte[1];
|
||||
} else if (types.isAssignable(o, sqlArrayType)) {
|
||||
getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "java.sql.Array not yet supported for getFakeBindParam, returning null", param);
|
||||
return null;
|
||||
} else {
|
||||
// shouldn't get here ever, if we do the types should be more specific
|
||||
getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING, "unsupported type for getFakeBindParam, returning null", param);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -16,18 +16,31 @@ public class ArrayInList implements InList {
|
||||
return instance;
|
||||
}
|
||||
|
||||
protected ArrayInList() {
|
||||
protected final String numericType, otherType;
|
||||
|
||||
public ArrayInList(final String numericType, final String otherType) {
|
||||
this.numericType = numericType;
|
||||
this.otherType = otherType;
|
||||
}
|
||||
|
||||
public ArrayInList() {
|
||||
this("number", "text");
|
||||
}
|
||||
|
||||
protected String columnAppend(final String columnName) {
|
||||
return "(" + columnName + " = ANY(?))";
|
||||
}
|
||||
|
||||
public Array toArray(final Connection conn, final String typeName, final Object[] elements) throws SQLException {
|
||||
return conn.createArrayOf(typeName, elements);
|
||||
}
|
||||
|
||||
public Array toArray(final Connection conn, final boolean numeric, final Object[] elements) throws SQLException {
|
||||
return toArray(conn, numeric ? numericType : otherType, elements);
|
||||
}
|
||||
|
||||
protected <T> Array toArray(final Connection conn, final Collection<T> values) throws SQLException {
|
||||
return conn.createArrayOf(
|
||||
values.iterator().next() instanceof Number ? "number" : "text",
|
||||
values.toArray()
|
||||
);
|
||||
return toArray(conn, values.iterator().next() instanceof Number, values.toArray());
|
||||
}
|
||||
|
||||
public <T> InListObject inList(final Connection conn, final String columnName, final Collection<T> values) throws SQLException {
|
||||
|
@ -34,25 +34,22 @@ public class OracleArrayInList extends ArrayInList {
|
||||
createArray = ca;
|
||||
}
|
||||
|
||||
protected OracleArrayInList() {
|
||||
public OracleArrayInList(final String numberType, final String otherType) {
|
||||
super(numberType, otherType);
|
||||
}
|
||||
|
||||
public OracleArrayInList() {
|
||||
this("ARRAY_NUM_TYPE", "ARRAY_STR_TYPE");
|
||||
}
|
||||
|
||||
protected String columnAppend(final String columnName) {
|
||||
return "(" + columnName + " IN(select column_value from table(?)))";
|
||||
}
|
||||
|
||||
protected <T> Array toArray(final Connection conn, final Collection<T> values) throws SQLException {
|
||||
/*
|
||||
return conn.unwrap(oracle.jdbc.OracleConnection.class).createOracleArray(
|
||||
values.iterator().next() instanceof Number ? "ARRAY_NUM_TYPE" : "ARRAY_STR_TYPE",
|
||||
values.toArray()
|
||||
);
|
||||
*/
|
||||
public Array toArray(final Connection conn, final String typeName, final Object[] elements) throws SQLException {
|
||||
//return conn.unwrap(oracle.jdbc.OracleConnection.class).createOracleArray(typeName, elements);
|
||||
try {
|
||||
return (Array) createArray.invoke(conn.unwrap(oracleConnection),
|
||||
values.iterator().next() instanceof Number ? "ARRAY_NUM_TYPE" : "ARRAY_STR_TYPE",
|
||||
values.toArray()
|
||||
);
|
||||
return (Array) createArray.invoke(conn.unwrap(oracleConnection), typeName, elements);
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
|
@ -179,6 +179,8 @@ public class QueryMapper implements JdbcMapper {
|
||||
ps.setBlob(index, (java.sql.Blob) o);
|
||||
else if (o instanceof ArrayInList.ArrayListObject)
|
||||
ps.setArray(index, ((ArrayInList.ArrayListObject) o).getArray());
|
||||
else if (o instanceof java.sql.Array)
|
||||
ps.setArray(index, (java.sql.Array) o);
|
||||
else
|
||||
ps.setObject(index, o); // probably won't get here ever, but just in case...
|
||||
/*
|
||||
|
Loading…
Reference in New Issue
Block a user