All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dbobjekts.api.TransactionManager.kt Maven / Gradle / Ivy

There is a newer version: 0.6.0-RC2
Show newest version
package com.dbobjekts.api

import com.dbobjekts.api.exception.DBObjektsException
import com.dbobjekts.jdbc.ConnectionAdapter
import com.dbobjekts.jdbc.DataSourceAdapter
import com.dbobjekts.jdbc.DataSourceAdapterImpl
import com.dbobjekts.metadata.Catalog
import com.dbobjekts.metadata.DefaultNoVendorCatalog
import com.dbobjekts.statement.TransactionResultValidator
import com.dbobjekts.util.HikariDataSourceAdapterImpl
import com.dbobjekts.util.StatementLogger
import com.dbobjekts.util.StringUtil
import com.dbobjekts.util.Version
import com.dbobjekts.vendors.Vendor
import com.dbobjekts.vendors.Vendors
import com.zaxxer.hikari.HikariDataSource
import org.slf4j.LoggerFactory
import java.sql.Connection
import javax.sql.DataSource

/**
 * The factory for [Transaction] instances to connect to a database. It wraps a [javax.sqlDataSource] and you typically need only one in your application.
 *
 * Instances are obtained with a call to [TransactionManager.builder]
 *
 * You use a TransactionManager with a call to [newTransaction] or the shorthand [invoke] method, as follows:
 * ```kotlin
 *  transactionManager.newTransaction { transaction ->
 *     // your queries here
 *  }
 *
 *  transactionManager { it.select(..) }
 *
 * ```
 */
class TransactionManager private constructor(
    ds: DataSource,
    val catalog: Catalog,
    private val customConnectionProvider: ((DataSource) -> Connection)? = null
) {
    private val log = LoggerFactory.getLogger(TransactionManager::class.java)
    private val dataSource: DataSourceAdapter
    internal val vendor: Vendor

    init {
        this.dataSource = when (ds) {
            is HikariDataSource -> HikariDataSourceAdapterImpl(ds)
            else -> DataSourceAdapterImpl(ds)
        }
        val vendorByDBMetaData = extractDBMetaData().vendor
        if (catalog.vendor.isBlank()) {
            log.warn("The catalog is not associated with any vendor.")
        } else if (catalog.vendor != vendorByDBMetaData.name)
            throw DBObjektsException(
                "You provided a Catalog implementation that is associated with vendor ${catalog.vendor}, " +
                        "but you connected to a ${vendorByDBMetaData.name} DataSource."
            )
        vendor = vendorByDBMetaData
    }

    /**
     * Shorthand for [newTransaction]
     */
    operator fun  invoke(function: (Transaction) -> T): T = newTransaction(function)

    /**
     * Creates a new [Transaction]
     * @param function a function that receives a [Transaction] on which you perform your queries
     * @return an arbitrary result of your queries. Can be Unit.
     */
    fun  newTransaction(function: (Transaction) -> T): T {

        val connection: Connection = customConnectionProvider?.invoke(dataSource.dataSource)
            ?: dataSource.createConnection()

        require(!connection.isClosed, { "Connection is closed" })

        val adapter = ConnectionAdapter(connection, StatementLogger(), catalog, vendor)
        val transaction = Transaction(adapter)
        try {
            val result: T = function(transaction)
            transaction.commit()
            return TransactionResultValidator.validate(result)
        } catch (e: Exception) {
            val info = transaction.connection.statementLog.lastStatement()
            log.error("Caught exception while executing query '${info?: ""}'. Rolling back", e)
            transaction.rollback()
            throw e
        } finally {
            transaction.semaphore.clear()
            transaction.close()
        }
    }

    private fun extractDBMetaData(): DBConnectionMetaData {
        val connection = dataSource.createConnection()
        try {
            val metaData = connection.metaData
            val vendor =
                Vendors.byProductAndVersion(
                    metaData.getDatabaseProductName(),
                    metaData.databaseMajorVersion
                )
            val rs = metaData.catalogs
            val schemas = mutableListOf()
            while (rs.next()) {
                val name = rs.getString(1)
                schemas += StringUtil.initUpperCase(name)
            }
            return DBConnectionMetaData(vendor, schemas)

        } finally {
            connection.close()
        }
    }


    override fun toString(): String = "${dataSource} ${catalog}"

    /**
     * Provides access to a [TransactionManagerBuilder]
     */
    companion object {
        fun builder() = TransactionManagerBuilder()
    }

    /**
     * Creates a new [TransactionManagerBuilder] instances for configuring a [TransactionManager]. Use it as follows:
     * ```kotlin
     *  TransactionManager.builder()
     *      .withDataSource(dataSource)
     *      .withCatalog(AcmeCatalog)
     *      .build()
     * ```
     */
    class TransactionManagerBuilder {
        private lateinit var dataSource: DataSource
        private var catalog: Catalog? = null
        private var connectionFactory: ((DataSource) -> Connection)? = null

        /**
         * Sets the underlying [DataSource] for the new [TransactionManager]. Mandatory setting
         * @param a valid [DataSource]
         */
        fun withDataSource(dataSource: DataSource): TransactionManagerBuilder {
            this.dataSource = dataSource
            return this
        }

        /**
         * Sets the [Catalog] implementation (generated by the [com.dbobjekts.codegen.CodeGenerator]) associated with a database.
         *
         * A Catalog reference is essential for creating queries with the generated metadata objects. If you omit this step, the [com.dbobjekts.metadata.DefaultNoVendorCatalog] will be used, which is only suitable for native SQL queries.
         *
         * @param the [Catalog] associated with the [DataSource]
         * @throws com.dbobjekts.api.exception.DBObjektsException if the major version of this catalog is too much behind and would cause compatability issues
         */
        fun withCatalog(catalog: Catalog): TransactionManagerBuilder {
            Version.validateCatalogVersion(catalog.version)
            this.catalog = catalog
            return this
        }

        /**
         * An optional setting if you require full control over the creation of the [Connection]. By default, db-Objekts only does:
         * ```kotlin
         * dataSource.getConnection()
         * ```
         * With this feature you can customize the Connection that db-Obekts will use.
         * @param a function that receives a [DataSource] and returns a [Connection]
         */
        fun withCustomConnectionProvider(factory: (DataSource) -> Connection): TransactionManagerBuilder {
            this.connectionFactory = factory
            return this
        }

        /**
         * @return a new [TransactionManager] instance
         */
        fun build(): TransactionManager {
            return TransactionManager(dataSource, catalog ?: DefaultNoVendorCatalog, connectionFactory)
        }
    }

}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy