|
package ru.circumflex
package orm
import core._
import javax.sql.DataSource
import javax.naming.InitialContext
import java.util.Date
import java.sql.{Timestamp, Connection, PreparedStatement}
import com.mchange.v2.c3p0.{DataSources, ComboPooledDataSource}
import collection.mutable.HashMap
import xml._
import util.control.ControlThrowable
|
ORM Configuration Objects
Circumflex ORM needs to know a little about your environment to operate. Following objects configure different aspects of Circumflex ORM:
- connection provider is used to acquire JDBC connections, can be overriden using the
orm.connectionProvider configuration parameter;
- type converter handles convertions between JDBC and Scala data types, can be overriden using the
orm.typeConverter configuration parameter;
- dialect handles all SQL rendering throughout the framework, can be overriden using the
orm.dialect configuration parameter;
- transaction manager is responsible for allocating current transactions for execution contexts.
|
trait ORMConfiguration {
// Configuration identification
def name: String
def prefix(sym: String) = if (name == "") "" else name + sym
// Database connectivity parameters
val url = cx.get("orm.connection.url")
.map(_.toString)
.getOrElse(throw new ORMException(
"Missing mandatory configuration parameter 'orm.connection.url'."))
val username = cx.get("orm.connection.username")
.map(_.toString)
.getOrElse(throw new ORMException(
"Missing mandatory configuration parameter 'orm.connection.username'."))
val password = cx.get("orm.connection.password")
.map(_.toString)
.getOrElse(throw new ORMException(
"Missing mandatory configuration parameter 'orm.connection.password'."))
lazy val dialect = cx.instantiate[Dialect]("orm.dialect", url match {
case u if (u.startsWith("jdbc:postgresql:")) => new PostgreSQLDialect
case u if (u.startsWith("jdbc:mysql:")) => new MySQLDialect
case u if (u.startsWith("jdbc:oracle:")) => new OracleDialect
case u if (u.startsWith("jdbc:h2:")) => new H2Dialect
case u if (u.startsWith("jdbc:sqlserver:")) => new MSSQLDialect
case u if (u.startsWith("jdbc:db2:")) => new DB2Dialect
case _ => new Dialect
})
lazy val driver = cx.get("orm.connection.driver") match {
case Some(s: String) => s
case _ => dialect.driverClass
}
lazy val isolation: Int = cx.get("orm.connection.isolation") match {
case Some("none") => Connection.TRANSACTION_NONE
case Some("read_uncommitted") => Connection.TRANSACTION_READ_UNCOMMITTED
case Some("read_committed") => Connection.TRANSACTION_READ_COMMITTED
case Some("repeatable_read") => Connection.TRANSACTION_REPEATABLE_READ
case Some("serializable") => Connection.TRANSACTION_SERIALIZABLE
case _ => {
ORM_LOG.info("Using READ COMMITTED isolation, override 'orm.connection.isolation' if necesssary.")
Connection.TRANSACTION_READ_COMMITTED
}
}
// Configuration objects
lazy val typeConverter: TypeConverter = cx.instantiate[TypeConverter](
"orm.typeConverter", new TypeConverter)
lazy val transactionManager: TransactionManager = cx.instantiate[TransactionManager](
"orm.transactionManager", new DefaultTransactionManager)
lazy val defaultSchema: Schema = new Schema(
cx.get("orm.defaultSchema").map(_.toString).getOrElse("public"))
lazy val statisticsManager: StatisticsManager = cx.instantiate[StatisticsManager](
"orm.statisticsManager", new StatisticsManager)
lazy val connectionProvider: ConnectionProvider = cx.instantiate[ConnectionProvider](
"orm.connectionProvider", new SimpleConnectionProvider(driver, url, username, password, isolation))
}
class SimpleORMConfiguration(val name: String) extends ORMConfiguration
|
Connection Provider
The ConnectionProvider is a simple trait responsible for acquiring JDBC connections throughout the application.
|
trait ConnectionProvider {
def openConnection(): Connection
def close()
}
|
Circumflex ORM provides default ConnectionProvider implementation. It behaves as follows:
If c3p0 data source is used you can fine tune it's configuration with c3p0.properties file (see c3p0 documentation for more details).
Though SimpleConnectionProvider is an optimal choice for most applications, you can create your own connection provider by implementing the ConnectionProvider trait and setting the orm.connectionProvider configuration parameter.
|
class SimpleConnectionProvider(
val driverClass: String,
val url: String,
val username: String,
val password: String,
val isolation: Int)
extends ConnectionProvider {
protected def createDataSource: DataSource = cx.get("orm.connection.datasource") match {
case Some(jndiName: String) => {
val ctx = new InitialContext
val ds = ctx.lookup(jndiName).asInstanceOf[DataSource]
ORM_LOG.info("Using JNDI datasource ({}).", jndiName)
ds
}
case _ => {
ORM_LOG.info("Using c3p0 connection pool.")
val ds = new ComboPooledDataSource()
ds.setDriverClass(driverClass)
ds.setJdbcUrl(url)
ds.setUser(username)
ds.setPassword(password)
ds
}
}
protected var _ds: DataSource = null
def dataSource: DataSource = {
if (_ds == null)
_ds = createDataSource
_ds
}
def openConnection(): Connection = {
val conn = dataSource.getConnection
conn.setAutoCommit(false)
conn.setTransactionIsolation(isolation)
conn
}
def close() {
DataSources.destroy(_ds)
_ds = null
}
}
|
Type converter
The TypeConverter trait is used to set JDBC prepared statement values for execution. If you intend to use custom types, provide your own implementation.
|
class TypeConverter {
def write(st: PreparedStatement, parameter: Any, paramIndex: Int) {
parameter match {
case None | null => st.setObject(paramIndex, null)
case Some(v) => write(st, v, paramIndex)
case p: Date => st.setObject(paramIndex, new Timestamp(p.getTime))
case x: Elem => st.setString(paramIndex, x.toString())
case bd: BigDecimal => st.setBigDecimal(paramIndex, bd.bigDecimal)
case ba: Array[Byte] => st.setBytes(paramIndex, ba)
case v => st.setObject(paramIndex, v)
}
}
}
|
Transaction manager
No communication with the database can occur outside of a database transaction.
The Transaction class wraps JDBC Connection and provides simple methods for committing or rolling back underlying transaction as well as for executing various typical JDBC statements.
The TransactionManager trait is responsible for allocation current transaction for application's execution context. The default implementation uses Context , however, your application may require different approaches to transaction demarcation — in this case you may provide your own implementation.
JDBC PreparedStatement objects are also cached within Transaction for performance considerations.
|
class Transaction {
// Connections are opened lazily
protected var _connection: Connection = null
// Statements are cached by actual SQL
protected val _statementsCache = new HashMap[String, PreparedStatement]()
def isLive: Boolean =
_connection != null && !_connection.isClosed
def commit() {
if (isLive && !_connection.getAutoCommit) _connection.commit()
}
def rollback() {
if (isLive && !_connection.getAutoCommit) _connection.rollback()
this.cache.invalidate()
}
def close() {
if (isLive) try {
// close all cached statements
_statementsCache.values.foreach(_.close())
} finally {
// clear statements cache
_statementsCache.clear()
// close connection
_connection.close()
ormConf.statisticsManager.connectionsClosed.incrementAndGet()
ORM_LOG.trace("Closed a JDBC connection.")
}
}
protected def getConnection: Connection = {
if (_connection == null || _connection.isClosed) {
_connection = ormConf.connectionProvider.openConnection()
ormConf.statisticsManager.connectionsOpened.incrementAndGet()
ORM_LOG.trace("Opened a JDBC connection.")
}
_connection
}
// Cache service
val cache: CacheService = new DefaultCacheService()
// Execution methods
def execute[A](connActions: Connection => A,
errActions: Throwable => A): A =
try {
ormConf.statisticsManager.executions.incrementAndGet()
val result = connActions(getConnection)
ormConf.statisticsManager.executionsSucceeded.incrementAndGet()
result
} catch {
case e: Exception =>
ormConf.statisticsManager.executionsFailed.incrementAndGet()
errActions(e)
}
def execute[A](sql: String,
stActions: PreparedStatement => A,
errActions: Throwable => A): A = execute({ conn =>
ORM_LOG.debug(ormConf.prefix(": ") + sql)
val st =_statementsCache.get(sql).getOrElse {
val statement = ormConf.dialect.prepareStatement(conn, sql)
_statementsCache.update(sql, statement)
statement
}
stActions(st)
}, errActions)
def apply[A](block: => A): A = {
val sp = getConnection.setSavepoint()
try {
block
} catch {
case e: ControlThrowable =>
ORM_LOG.trace("Escaped nested transaction via ControlThrowable, ROLLBACK is suppressed.")
throw e
case e: Exception =>
getConnection.rollback(sp)
throw e
} finally {
getConnection.releaseSavepoint(sp)
}
}
}
trait TransactionManager {
def get: Transaction
}
class DefaultTransactionManager extends TransactionManager {
Context.addDestroyListener(c => try {
get.commit()
ORM_LOG.trace("Committed current transaction.")
} catch {
case e1: Exception =>
ORM_LOG.error("Could not commit current transaction", e1)
try {
get.rollback()
ORM_LOG.trace("Rolled back current transaction.")
} catch {
case e2: Exception =>
ORM_LOG.error("Could not roll back current transaction", e2)
}
} finally {
get.close()
})
def get: Transaction = ctx.get("orm.transaction") match {
case Some(t: Transaction) => t
case _ =>
val t = cx.instantiate[Transaction]("orm.transaction", new Transaction)
ctx.update("orm.transaction", t)
t
}
}
// Special helper for single-user REPL usage
class ConsoleTransactionManager extends TransactionManager {
var currentTransaction = new Transaction
def get = currentTransaction
}
|