From 72906cf3c5057ac55213facae8bc1be773f1d17d Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Wed, 29 Nov 2017 23:51:27 -0500 Subject: [PATCH] Add @JdbcMapper.RunInTransaction and static support methods to QueryRunner --- .../com/moparisthebest/jdbc/QueryRunner.java | 103 ++++++++++++--- .../jdbc/codegen/JdbcMapper.java | 7 + .../codegen/CompileTimeResultSetMapper.java | 17 +-- .../jdbc/codegen/JdbcMapperProcessor.java | 123 +++++++++++++++++- .../jdbc/codegen/AbstractDao.java | 28 ++++ .../jdbc/codegen/PersonDAO.java | 5 + 6 files changed, 250 insertions(+), 33 deletions(-) create mode 100644 jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/AbstractDao.java diff --git a/common/src/main/java/com/moparisthebest/jdbc/QueryRunner.java b/common/src/main/java/com/moparisthebest/jdbc/QueryRunner.java index e13ddee..dc0cb0e 100644 --- a/common/src/main/java/com/moparisthebest/jdbc/QueryRunner.java +++ b/common/src/main/java/com/moparisthebest/jdbc/QueryRunner.java @@ -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 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 + * @param + * @return + * @throws SQLException + */ + public static E runInTransaction(final T dao, final Runner 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 + * @param + * @return + * @throws SQLException + */ + public static E runConnectionInTransaction(final T dao, final Runner 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 + } } } } diff --git a/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java b/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java index 909dba9..d959bf4 100644 --- a/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java +++ b/common/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapper.java @@ -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 { diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/CompileTimeResultSetMapper.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/CompileTimeResultSetMapper.java index 7c9b5e6..728e2fe 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/CompileTimeResultSetMapper.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/CompileTimeResultSetMapper.java @@ -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)); diff --git a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java index d2012e6..ba6bc5d 100644 --- a/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java +++ b/jdbcmapper/src/main/java/com/moparisthebest/jdbc/codegen/JdbcMapperProcessor.java @@ -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 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 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(); diff --git a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/AbstractDao.java b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/AbstractDao.java new file mode 100644 index 0000000..d3dbb46 --- /dev/null +++ b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/AbstractDao.java @@ -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)); + } +} diff --git a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java index bb59db0..fd14558 100644 --- a/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java +++ b/jdbcmapper/src/test/java/com/moparisthebest/jdbc/codegen/PersonDAO.java @@ -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