Add @JdbcMapper.RunInTransaction and static support methods to QueryRunner

This commit is contained in:
Travis Burtrum 2017-11-29 23:51:27 -05:00
parent 3675ae0f73
commit 72906cf3c5
6 changed files with 250 additions and 33 deletions

View File

@ -2,6 +2,7 @@ package com.moparisthebest.jdbc;
import com.moparisthebest.jdbc.codegen.JdbcMapper;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.*;
@ -97,31 +98,103 @@ public class QueryRunner<T extends JdbcMapper> {
T dao = null;
try {
dao = factory.create();
dao.getConnection().setAutoCommit(false);
final E ret = query.run(dao);
dao.getConnection().commit();
return ret;
} catch (final Throwable e) {
if (dao != null) {
return runInTransaction(dao, query);
} finally {
if (dao != null)
tryClose(dao);
}
}
/**
* For running existing JdbcMapper in transaction
* @param dao
* @param query
* @param <T>
* @param <E>
* @return
* @throws SQLException
*/
public static <T extends JdbcMapper, E> E runInTransaction(final T dao, final Runner<T, E> query) throws SQLException {
if (query == null)
throw new NullPointerException("query must be non-null");
if (dao == null)
throw new NullPointerException("dao must be non-null");
if(!dao.getConnection().getAutoCommit()) {
// if we are already in a transaction, the calling code will do the right thing
// we don't want to change autoCommit, commit, or rollback
return query.run(dao);
} else {
try {
dao.getConnection().setAutoCommit(false);
final E ret = query.run(dao);
dao.getConnection().commit();
return ret;
} catch (final Throwable e) {
try {
dao.getConnection().rollback();
} catch (SQLException excep) {
// ignore to throw original
}
}
if (e instanceof SQLException)
throw (SQLException) e;
if (e instanceof RuntimeException)
throw (RuntimeException) e;
throw new RuntimeException("odd error should never happen", e);
} finally {
if (dao != null) {
if (e instanceof SQLException)
throw (SQLException) e;
if (e instanceof RuntimeException)
throw (RuntimeException) e;
throw new RuntimeException("odd error should never happen", e);
} finally {
try {
dao.getConnection().setAutoCommit(true);
} catch (SQLException excep) {
// ignore
}
tryClose(dao);
}
}
}
/**
* For running against an existing raw connection for things not implementing JdbcMapper
*
* this could construct a JdbcMapper instance with the Connection and re-use the method above,
* with a bit of a performance/allocation hit, we'll skip for now
*
* @param dao
* @param query
* @param <T>
* @param <E>
* @return
* @throws SQLException
*/
public static <T extends Connection, E> E runConnectionInTransaction(final T dao, final Runner<T, E> query) throws SQLException {
if (query == null)
throw new NullPointerException("query must be non-null");
if (dao == null)
throw new NullPointerException("dao must be non-null");
if(!dao.getAutoCommit()) {
// if we are already in a transaction, the calling code will do the right thing
// we don't want to change autoCommit, commit, or rollback
return query.run(dao);
} else {
try {
dao.setAutoCommit(false);
final E ret = query.run(dao);
dao.commit();
return ret;
} catch (final Throwable e) {
try {
dao.rollback();
} catch (SQLException excep) {
// ignore to throw original
}
if (e instanceof SQLException)
throw (SQLException) e;
if (e instanceof RuntimeException)
throw (RuntimeException) e;
throw new RuntimeException("odd error should never happen", e);
} finally {
try {
dao.setAutoCommit(true);
} catch (SQLException excep) {
// ignore
}
}
}
}

View File

@ -57,6 +57,13 @@ public interface JdbcMapper extends Closeable {
*/
public @interface WarnOnUnusedParams {}
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD})
/**
* Run this method in a transaction, useless on @SQL methods because they only run single statements, helpful on default or abstract methods that chain calls
*/
public @interface RunInTransaction {}
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD})
public @interface SQL {

View File

@ -25,6 +25,7 @@ import java.util.*;
import java.util.stream.Stream;
//IFJAVA8_END
import static com.moparisthebest.jdbc.codegen.JdbcMapperProcessor.java8;
import static com.moparisthebest.jdbc.codegen.JdbcMapperProcessor.typeMirrorStringNoGenerics;
import static com.moparisthebest.jdbc.codegen.JdbcMapperProcessor.typeMirrorToClass;
@ -33,32 +34,16 @@ import static com.moparisthebest.jdbc.codegen.JdbcMapperProcessor.typeMirrorToCl
*/
public class CompileTimeResultSetMapper {
public static final SourceVersion RELEASE_8;
static {
SourceVersion rl8 = null;
try {
rl8 = SourceVersion.valueOf("RELEASE_8");
} catch(Throwable e) {
// ignore
}
RELEASE_8 = rl8;
}
public final Types types;
public final TypeMirror collectionType, mapType, mapCollectionType, iteratorType, listIteratorType, finishableType, resultSetType, resultSetIterableType, byteArrayType;
//IFJAVA8_START
public final TypeMirror streamType;
//IFJAVA8_END
private final boolean java8;
public CompileTimeResultSetMapper(final ProcessingEnvironment processingEnv) {
types = processingEnv.getTypeUtils();
final Elements elements = processingEnv.getElementUtils();
// is this the proper way to do this?
java8 = RELEASE_8 != null && processingEnv.getSourceVersion().ordinal() >= RELEASE_8.ordinal();
collectionType = types.getDeclaredType(elements.getTypeElement(Collection.class.getCanonicalName()), types.getWildcardType(null, null));
mapType = types.getDeclaredType(elements.getTypeElement(Map.class.getCanonicalName()), types.getWildcardType(null, null), types.getWildcardType(null, null));
mapCollectionType = types.getDeclaredType(elements.getTypeElement(Map.class.getCanonicalName()), types.getWildcardType(null, null), types.getWildcardType(collectionType, null));

View File

@ -33,6 +33,19 @@ public class JdbcMapperProcessor extends AbstractProcessor {
private static final Pattern paramPattern = Pattern.compile("\\{(([^\\s]+)\\s+(([Nn][Oo][Tt]\\s+)?[Ii][Nn]\\s+))?([^}]+)\\}");
public static final SourceVersion RELEASE_8;
public static boolean java8;
static {
SourceVersion rl8 = null;
try {
rl8 = SourceVersion.valueOf("RELEASE_8");
} catch(Throwable e) {
// ignore
}
RELEASE_8 = rl8;
}
private static Types types;
private static Messager messager;
@ -44,7 +57,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
return messager;
}
private TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType,
private TypeMirror sqlExceptionType, stringType, numberType, utilDateType, readerType, clobType, jdbcMapperType,
byteArrayType, inputStreamType, fileType, blobType, sqlArrayType, collectionType, calendarType, cleanerType;
//IFJAVA8_START
private TypeMirror instantType, localDateTimeType, localDateType, localTimeType, zonedDateTimeType, offsetDateTimeType, offsetTimeType;
@ -62,6 +75,10 @@ public class JdbcMapperProcessor extends AbstractProcessor {
@Override
public synchronized void init(final ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// is this the proper way to do this?
java8 = RELEASE_8 != null && processingEnv.getSourceVersion().ordinal() >= RELEASE_8.ordinal();
types = processingEnv.getTypeUtils();
messager = processingEnv.getMessager();
final Elements elements = processingEnv.getElementUtils();
@ -71,6 +88,7 @@ public class JdbcMapperProcessor extends AbstractProcessor {
utilDateType = elements.getTypeElement(java.util.Date.class.getCanonicalName()).asType();
readerType = elements.getTypeElement(Reader.class.getCanonicalName()).asType();
clobType = elements.getTypeElement(Clob.class.getCanonicalName()).asType();
jdbcMapperType = elements.getTypeElement(JdbcMapper.class.getCanonicalName()).asType();
inputStreamType = elements.getTypeElement(InputStream.class.getCanonicalName()).asType();
fileType = elements.getTypeElement(File.class.getCanonicalName()).asType();
blobType = elements.getTypeElement(Blob.class.getCanonicalName()).asType();
@ -209,8 +227,14 @@ public class JdbcMapperProcessor extends AbstractProcessor {
for (final Element methodElement : genClass.getEnclosedElements()) {
// can only implement abstract methods
if (methodElement.getKind() != ElementKind.METHOD || !methodElement.getModifiers().contains(Modifier.ABSTRACT))
if (methodElement.getKind() != ElementKind.METHOD) {
continue;
}
if (!methodElement.getModifiers().contains(Modifier.ABSTRACT)) {
if (methodElement.getAnnotation(JdbcMapper.RunInTransaction.class) != null)
outputRunInTransaction((ExecutableElement) methodElement, w);
continue;
}
final ExecutableElement eeMethod = (ExecutableElement) methodElement;
if (lookupCloseMethod)
if ((closeMethod = getCloseMethod(eeMethod)) != null) {
@ -223,6 +247,10 @@ public class JdbcMapperProcessor extends AbstractProcessor {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL with non-empty query is required on abstract or interface methods", methodElement);
continue;
}
if (eeMethod.getAnnotation(JdbcMapper.RunInTransaction.class) != null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.SQL is incompatible with @JdbcMapper.RunInTransaction", methodElement);
continue;
}
final boolean allowReflection = sql.allowReflection().combine(defaultAllowReflection);
w.write("\n\t@Override\n\tpublic ");
final String returnType = eeMethod.getReturnType().toString();
@ -481,6 +509,97 @@ public class JdbcMapperProcessor extends AbstractProcessor {
return true;
}
private void outputRunInTransaction(final ExecutableElement eeMethod, final Writer w) throws IOException {
w.write("\n\t@Override\n\tpublic ");
final String returnType = eeMethod.getReturnType().toString();
w.write(returnType);
w.write(" ");
w.write(eeMethod.getSimpleName().toString());
w.write('(');
boolean sqlExceptionThrown = false;
final List<? extends VariableElement> params = eeMethod.getParameters();
final int numParams = params.size();
{
// now parameters
int count = 0;
for (final VariableElement param : params) {
writeAllParamAnnotations(w, param);
w.write("final ");
w.write(param.asType().toString());
w.write(' ');
final String name = param.getSimpleName().toString();
w.write(name);
if (++count != numParams)
w.write(", ");
}
// throws?
w.write(")");
final List<? extends TypeMirror> thrownTypes = eeMethod.getThrownTypes();
final int numThrownTypes = thrownTypes.size();
if (numThrownTypes > 0) {
count = 0;
w.write(" throws ");
}
for (final TypeMirror thrownType : thrownTypes) {
sqlExceptionThrown |= types.isSameType(thrownType, sqlExceptionType);
w.write(thrownType.toString());
if (++count != numThrownTypes)
w.write(", ");
}
w.write(" {\n");
}
final Element thisDao = eeMethod.getEnclosingElement();
final boolean thisDaoImplementsJdbcMapper = types.isAssignable(thisDao.asType(), jdbcMapperType);
final String thisDaoName = thisDao.getSimpleName().toString();
if(!java8)
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@JdbcMapper.RunInTransaction cannot be used in java6 yet, java8+ only", eeMethod);
// todo: *can* this be done in java6 without reflection or something? we need to call super, not this, which causes infinite recursion, bail for now
if(!java8)
w.append("\t\tfinal ").append(thisDaoName).append(" jdbcMapperGeneratedTransactionThis = this;\n");
w.write("\t\treturn com.moparisthebest.jdbc.QueryRunner.run");
if(thisDaoImplementsJdbcMapper)
w.write("InTransaction(this, ");
else
w.write("ConnectionInTransaction(this.conn, ");
if(!java8) {
w.append("new com.moparisthebest.jdbc.QueryRunner.Runner<").append(thisDaoName).append(", ").append(returnType).append(">() {\n" +
"\t\t\t@Override\n" +
"\t\t\tpublic ").append(returnType).append(" run(").append(eeMethod.getEnclosingElement().getSimpleName()).append(" dao) throws SQLException {\n" +
"\t\t\t\treturn jdbcMapperGeneratedTransactionThis");
} else {
w.append("dao -> ");
//IFJAVA8_START
if (eeMethod.getModifiers().contains(Modifier.DEFAULT))
w.append(thisDaoName).append(".");
//IFJAVA8_END
w.append("super");
}
w.append(".").append(eeMethod.getSimpleName().toString()).append('(');
int count = 0;
for (final VariableElement param : params) {
final String name = param.getSimpleName().toString();
w.write(name);
if (++count != numParams)
w.write(", ");
}
w.append(')');
if(!java8)
w.append(";\n\t\t\t}\n" +
"\t\t}");
w.write(");\n");
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();

View File

@ -0,0 +1,28 @@
package com.moparisthebest.jdbc.codegen;
import com.moparisthebest.jdbc.dto.FieldPerson;
import com.moparisthebest.jdbc.dto.Person;
import java.sql.SQLException;
@JdbcMapper.Mapper(
// databaseType = JdbcMapper.DatabaseType.ORACLE
cachePreparedStatements = JdbcMapper.OptionalBool.FALSE
// , sqlParser = SimpleSQLParser.class
, allowReflection = JdbcMapper.OptionalBool.TRUE
)
public abstract class AbstractDao {
@JdbcMapper.SQL(value = "SELECT person_no, first_name, last_name, birth_date FROM person WHERE person_no = {personNo}")
abstract FieldPerson getPerson(long personNo) throws SQLException;
@JdbcMapper.SQL("SELECT person_no FROM person WHERE last_name = {lastName}")
abstract long getPersonNo(String lastName) throws SQLException;
//IFJAVA8_START
@JdbcMapper.RunInTransaction
//IFJAVA8_END
protected Person getPersonInTransaction(final String lastName) throws SQLException {
return getPerson(getPersonNo(lastName));
}
}

View File

@ -237,6 +237,11 @@ public interface PersonDAO extends JdbcMapper {
@JdbcMapper.SQL("SELECT str_val FROM val WHERE val_no = {valNo}")
ZoneOffset getZoneOffsetStr(long valNo);
@JdbcMapper.RunInTransaction
default Person getPersonInTransaction(final String lastName) throws SQLException {
return getPerson(getPersonNo(lastName));
}
//IFJAVA8_END
// test blob