Transactions

Talks about: , and

YoSQL generates no transaction management, opens no transaction of its own, and has no opinion about where yours come from. What it generates is the one thing a transaction needs: every statement is reachable on a connection you supply.

Two shapes for every statement

Each statement generates two methods with the same name:

Optional<Tenant> findTenant(UUID id);
Optional<Tenant> findTenant(Connection connection, UUID id);

The first takes a connection from the repository’s DataSource and closes it. The second runs on the connection it is given and leaves it open, so several statements can share one — which is all a transaction is.

Which one a call site uses is its own business, and that is why both exist. Turning them into a choice made per statement is what generateConnectionOverloads is for, and there is rarely a reason to.

Plain JDBC

Open a connection, turn off auto-commit, and pass it to every statement in the unit of work:

public void placeOrder(final UUID tenantId, final PlacedOrder order) {
    try (final var connection = dataSource.getConnection()) {
        connection.setAutoCommit(false);
        try {
            orders.insertOrder(connection, order.id(), tenantId, order.state(), order.monthlyPrice());
            ledger.insertLedgerEntry(connection, nextId(), tenantId, order.monthlyPrice());
            connection.commit();
        } catch (final RuntimeException | SQLException failure) {
            connection.rollback();
            throw failure;
        }
    }
}

Both repositories run on the same connection, so both statements are in the same transaction, and neither closes it — the try-with-resources does, once.

Nothing about that is specific to YoSQL. It is the JDBC you would have written, which is the point.

Spring

Under Spring you do not thread a Connection at all. Wrap the DataSource in a TransactionAwareDataSourceProxy and hand that to the repositories: getConnection() then returns the connection bound to the current transaction, and the DataSource-based methods join whatever @Transactional opened.

@Configuration
public class PersistenceConfiguration {

    @Bean
    public DataSource transactionAwareDataSource(final DataSource dataSource) {
        return new TransactionAwareDataSourceProxy(dataSource);
    }

    @Bean
    public TenantRepository tenantRepository(final DataSource transactionAwareDataSource) {
        return new TenantRepository(transactionAwareDataSource);
    }

}
@Service
public class OrderService {

    private final OrderRepository orders;
    private final LedgerRepository ledger;

    // constructor omitted

    @Transactional
    public void placeOrder(final UUID tenantId, final PlacedOrder order) {
        orders.insertOrder(order.id(), tenantId, order.state(), order.monthlyPrice());
        ledger.insertLedgerEntry(nextId(), tenantId, order.monthlyPrice());
    }

}

Both statements run on the transaction’s connection, rollback works, and the repositories know nothing about any of it. Use the Connection overloads under Spring only where you want a unit of work that is explicitly not the ambient one.

Give the PlatformTransactionManager the underlying DataSource, not the proxy — the proxy is for the code that consumes connections, not for the manager that binds them.

Quarkus and Jakarta EE

An injected DataSource under a JTA transaction is already transaction-aware: getConnection() returns the connection enlisted in the current transaction. Inject it, construct the repository with it, and annotate the service method with @Transactional. As with Spring, the Connection overloads are for the cases you want to keep out of the ambient transaction.

Things worth knowing

A cursor result outlives the method that returned it. A statement with returning: cursor gives you a lazy Stream, and the connection stays open until the stream is closed. The DataSource form closes it for you when the stream closes; the Connection form does not close a connection it did not open, so close the stream inside the transaction that owns it.

Batch methods take a connection too. insertTenantBatch(Connection, UUID[], String[]) exists alongside insertTenantBatch(UUID[], String[]), so a batch belongs in a transaction like anything else.

Isolation, savepoints and read-only flags are yours. They are properties of the connection, so set them on the connection before passing it in. YoSQL does not touch them.