The issue fixed here is that SavepointTransaction was effectively using the
TransactionEvent of the underlying 'parent' transaction. The fix is for
SavepointTransaction to have its own TransactionEvent.
Typically useful for handling DuplicateKeyException where we expect
DuplicateKeyException to be thrown and catch it with the intention of
continuing processing using the same transaction.
Note that some databases like Oracle do not require this explicit
rollback() and would work without the rollbackAndContinue(). Postgres
in particular requires the rollback() call on the underlying connection
such that we can continue using that transaction/java.sql.Connection.
Note that in the existing test we can see that rollbackAndContinue()
is pretty close to being syntactic sugar. I think adding rollbackAndContinue()
is justified and complements the existing commitAndContinue().
The reason for this is that as part of DefaultOrmQuery.copy() it uses
DefaultExpressionList.copy() and that assumed that expressions were
safe to share which is NOT the case for IN and EXISTS sub-query expressions
so InQueryExpression and ExistsQueryExpression
The effective fix for this is that DefaultExpressionList.copy() changes
to call SpiExpression.copy() and for InQueryExpression and ExistsQueryExpression
to implement that copy() by creating a copy of the sub-query.
A "side-fix" is that in DefaultOrmQuery.createExtraJoinsToSupportManyWhereClause()
it was creating an instance of ManyWhereJoins, then mutating it ... and if we
change that to only doing the assignment at the end (object assignment is atomic)
then racy access reading ManyWhereJoins would always get a fully completed non-mutating
instance of ManyWhereJoins. Noting this because it kind of points to where I think
the race condition is (in createExtraJoinsToSupportManyWhereClause()) but noting that
with the change to DefaultExpressionList.copy() this "side-fix" isn't required per say.
The reason for this is that ScopedTransaction holds a stack of ScopeTrans and Transaction, but
the actual transaction in this stack is often the same instance / shared when nesting. The issue
is that currently setNestedUseSavepoint() sets this flag on the *_underlying transaction_* and
this instance is the same / shared when 'nesting'.
This change moves the nestedUseSavepoint flag for ScopedTransaction to be on ScopeTrans
(from the underlying transaction).