From f54d272744e7b41bc2d3c258d7792a7e1e1a446b Mon Sep 17 00:00:00 2001 From: Stefan Zeiger Date: Fri, 8 Aug 2014 17:43:22 +0200 Subject: [PATCH] Add BoneCP support to Database.forConfig. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The configuration is very similar to Play’s. The new JdbcDataSource abstraction allows 3rd-party connection pools to be supported easily. JdbcBackend.Database gets a new method `close` for shutting down a connection pool. --- project/Build.scala | 3 +- .../scala/scala/slick/jdbc/JdbcBackend.scala | 184 +++++++++++------- .../scala/slick/jdbc/JdbcDataSource.scala | 181 +++++++++++++++++ .../scala/scala/slick/util/GlobalConfig.scala | 30 +++ 4 files changed, 326 insertions(+), 72 deletions(-) create mode 100644 src/main/scala/scala/slick/jdbc/JdbcDataSource.scala diff --git a/project/Build.scala b/project/Build.scala index b0dd5f4dbd..e4b5c7790a 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -25,7 +25,8 @@ object SlickBuild extends Build { val slf4j = "org.slf4j" % "slf4j-api" % "1.6.4" val logback = "ch.qos.logback" % "logback-classic" % "0.9.28" val typesafeConfig = "com.typesafe" % "config" % "1.2.1" - val mainDependencies = Seq(slf4j, typesafeConfig) + val boneCP = "com.jolbox" % "bonecp" % "0.8.0.RELEASE" + val mainDependencies = Seq(slf4j, typesafeConfig, boneCP % "optional") val h2 = "com.h2database" % "h2" % "1.3.170" val testDBs = Seq( h2, diff --git a/src/main/scala/scala/slick/jdbc/JdbcBackend.scala b/src/main/scala/scala/slick/jdbc/JdbcBackend.scala index 4bdd3b6d4f..251e7b792d 100644 --- a/src/main/scala/scala/slick/jdbc/JdbcBackend.scala +++ b/src/main/scala/scala/slick/jdbc/JdbcBackend.scala @@ -4,7 +4,6 @@ import scala.language.reflectiveCalls import scala.slick.backend.DatabaseComponent import scala.slick.SlickException import slick.util.SlickLogger -import scala.collection.JavaConverters._ import java.util.Properties import java.sql.{Array => _, _} import javax.sql.DataSource @@ -13,7 +12,7 @@ import org.slf4j.LoggerFactory import com.typesafe.config.{ConfigFactory, Config} /** A JDBC-based database back-end which can be used for Plain SQL queries - * and with all `JdbcProfile`-based drivers. */ + * and with all [[scala.slick.driver.JdbcProfile]]-based drivers. */ trait JdbcBackend extends DatabaseComponent { protected[this] lazy val statementLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".statement")) protected[this] lazy val benchmarkLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".benchmark")) @@ -25,7 +24,7 @@ trait JdbcBackend extends DatabaseComponent { val Database = new DatabaseFactoryDef {} val backend: JdbcBackend = this - trait DatabaseDef extends super.DatabaseDef { + class DatabaseDef(val source: JdbcDataSource) extends super.DatabaseDef { /** The DatabaseCapabilities, accessed through a Session and created by the * first Session that needs them. Access does not need to be synchronized * because, in the worst case, capabilities will be determined multiple @@ -36,64 +35,29 @@ trait JdbcBackend extends DatabaseComponent { def createSession(): Session = new BaseSession(this) - def createConnection(): Connection - } + /** If this object represents a connection pool managed directly by Slick, close it. + * Otherwise no action is taken. */ + def close(): Unit = source.close() + } trait DatabaseFactoryDef extends super.DatabaseFactoryDef { - /** - * Create a Database based on a DataSource. - */ - def forDataSource(ds: DataSource): DatabaseDef = new DatabaseDef { - def createConnection(): Connection = ds.getConnection - } + /** Create a Database based on a [[JdbcDataSource]]. */ + def forSource(source: JdbcDataSource) = new DatabaseDef(source) - /** - * Create a Database based on the JNDI name of a DataSource. - */ + /** Create a Database based on a DataSource. */ + def forDataSource(ds: DataSource): DatabaseDef = forSource(new DataSourceJdbcDataSource(ds)) + + /** Create a Database based on the JNDI name of a DataSource. */ def forName(name: String) = new InitialContext().lookup(name) match { case ds: DataSource => forDataSource(ds) case x => throw new SlickException("Expected a DataSource for JNDI name "+name+", but got "+x) } - /** - * Create a Database that uses the DriverManager to open new connections. - */ - def forURL(url:String, user:String = null, password:String = null, prop: Properties = null, driver:String = null): DatabaseDef = new DatabaseDef { - if(driver ne null) Class.forName(driver) - val cprop = if(prop.ne(null) && user.eq(null) && password.eq(null)) prop else { - val p = new Properties(prop) - if(user ne null) p.setProperty("user", user) - if(password ne null) p.setProperty("password", password) - p - } - - def createConnection(): Connection = DriverManager.getConnection(url, cprop) - } - - /** - * Create a Database that directly uses a Driver to open new connections. - * This is needed to open a JDBC URL with a driver that was not loaded by - * the system ClassLoader. - */ - def forDriver(driver:Driver, url:String, user:String = null, password:String = null, prop: Properties = null): DatabaseDef = new DatabaseDef { - val cprop = if(prop.ne(null) && user.eq(null) && password.eq(null)) prop else { - val p = new Properties(prop) - if(user ne null) p.setProperty("user", user) - if(password ne null) p.setProperty("password", password) - p - } - - def createConnection(): Connection = { - val conn = driver.connect(url, cprop) - if(conn eq null) - throw new SQLException("Driver "+driver+" does not know how to handle URL "+url, "08001") - conn - } - } + /** Create a Database that uses the DriverManager to open new connections. */ + def forURL(url:String, user:String = null, password:String = null, prop: Properties = null, driver:String = null): DatabaseDef = + forSource(new DriverJdbcDataSource(url, user, password, prop, driverName = driver)) - /** - * Create a Database that uses the DriverManager to open new connections. - */ + /** Create a Database that uses the DriverManager to open new connections. */ def forURL(url:String, prop: Map[String, String]): Database = { val p = new Properties if(prop ne null) @@ -101,14 +65,100 @@ trait JdbcBackend extends DatabaseComponent { forURL(url, prop = p, driver = null) } + /** Create a Database that directly uses a Driver to open new connections. + * This is needed to open a JDBC URL with a driver that was not loaded by the system ClassLoader. */ + def forDriver(driver:Driver, url:String, user:String = null, password:String = null, prop: Properties = null): DatabaseDef = + forSource(new DriverJdbcDataSource(url, user, password, prop, driver = driver)) + /** Load a database configuration through [[https://github.com/typesafehub/config Typesafe Config]]. * - * The following keys are supported: - * - `url`: JDBC URL (String, must be set) - * - `driver`: JDBC driver class to load (String, optional) - * - `user`: User name (String, optional) - * - `password`: Password (String, optional) - * - `properties`: Properties to pass to the driver (Map, optional) + * The main config key to set is `pool`. It determines the connection pool implementation to + * use (if any). The default is undefined/null (no pool, use the DriverManager directly). + * Slick comes with support for [[http://jolbox.com/ BoneCP]] which can be selected by setting + * `pool=BoneCP` (or the full object name `scala.slick.jdbc.BoneCPJdbcDataSource`). + * 3rd-party connection pool implementations have to be specified with the fully qualified + * name of an object implementing [[JdbcDataSourceFactory]]. + * + * The following config keys are supported for pool settings `null` and `BoneCP`: + * + * + * The following config keys are only supported for pool setting `BoneCP`: + * + * + * Unknown keys are ignored. Invalid values or missing mandatory keys will trigger a + * [[SlickException]]. + * + * The configuration settings are very similar to the ones supported by + * [[http://www.playframework.com/documentation/2.4.x/SettingsJDBC Play 2.4]], with a few + * notable differences: + * * * @param path The path in the configuration file for the database configuration (e.g. `foo.bar` * would find a database URL at config key `foo.bar.url`) @@ -116,19 +166,11 @@ trait JdbcBackend extends DatabaseComponent { * (e.g. in `application.conf` at the root of the class path) if not specified. * @param driver An optional JDBC driver to call directly. If this is set to a non-null value, * the `driver` key from the configuration is ignored. The default is to use the - * standard lookup mechanism. + * standard lookup mechanism. The explicit driver may not be supported by all + * connection pools (in particular, the default [[BoneCPJdbcDataSource]]). */ - def forConfig(path: String, config: Config = ConfigFactory.load(), driver: Driver = null): Database = { - val c = if(path.isEmpty) config else config.getConfig(path) - def str(p: String) = if(!c.hasPath(p)) null else c.getString(p) - def props(p: String) = if(!c.hasPath(p)) null else { - val props = new Properties(null) - c.getObject(p).asScala.foreach { case (k, v) => props.put(k, v.unwrapped.toString) } - props - } - if(driver ne null) forDriver(driver, c.getString("url"), str("user"), str("password"), props("properties")) - else forURL(c.getString("url"), str("user"), str("password"), props("properties"), str("driver")) - } + def forConfig(path: String, config: Config = ConfigFactory.load(), driver: Driver = null): Database = + forSource(JdbcDataSource.forConfig(if(path.isEmpty) config else config.getConfig(path), driver)) } trait SessionDef extends super.SessionDef { self => @@ -394,7 +436,7 @@ trait JdbcBackend extends DatabaseComponent { def isOpen = open def isInTransaction = inTransaction - lazy val conn = { open = true; database.createConnection() } + lazy val conn = { open = true; database.source.createConnection } lazy val metaData = conn.getMetaData() def capabilities = { diff --git a/src/main/scala/scala/slick/jdbc/JdbcDataSource.scala b/src/main/scala/scala/slick/jdbc/JdbcDataSource.scala new file mode 100644 index 0000000000..eb42376894 --- /dev/null +++ b/src/main/scala/scala/slick/jdbc/JdbcDataSource.scala @@ -0,0 +1,181 @@ +package scala.slick.jdbc + +import java.io.Closeable +import java.util.Properties +import java.util.concurrent.TimeUnit +import java.sql.{SQLException, DriverManager, Driver, Connection} +import javax.sql.DataSource +import com.typesafe.config.Config +import scala.slick.util.ConfigExtensionMethods._ +import scala.slick.SlickException + +/** A `JdbcDataSource` provides a way to create a `Connection` object for a database. It is + * similar to a `javax.sql.DataSource` but simpler. Unlike [[JdbcBackend.DatabaseDef]] it is not a + * part of the backend cake. This trait defines the SPI for 3rd-party connection pool support. */ +trait JdbcDataSource extends Closeable { + /** Create a new Connection or get one from the pool */ + def createConnection(): Connection + + /** If this object represents a connection pool managed directly by Slick, close it. + * Otherwise no action is taken. */ + def close(): Unit +} + +object JdbcDataSource { + /** Create a JdbcDataSource from a `Config`. See [[JdbcBackend.DatabaseFactoryDef.forConfig]] + * for documentation of the supported configuration parameters. */ + def forConfig(c: Config, driver: Driver): JdbcDataSource = { + val pf: JdbcDataSourceFactory = c.getStringOr("pool") match { + case null => DriverJdbcDataSource + case "BoneCP" => BoneCPJdbcDataSource + case name => + val clazz = Class.forName(name) + clazz.getField("MODULE$").get(clazz).asInstanceOf[JdbcDataSourceFactory] + } + pf.forConfig(c, driver) + } +} + +/** Create a [[JdbcDataSource]] from a `Config` object and an optional JDBC `Driver`. + * This is used with the "pool" configuration option in [[JdbcBackend.DatabaseFactoryDef.forConfig]]. */ +trait JdbcDataSourceFactory { + def forConfig(c: Config, driver: Driver): JdbcDataSource +} + +/** A JdbcDataSource for a `DataSource` */ +class DataSourceJdbcDataSource(val ds: DataSource) extends JdbcDataSource { + def createConnection(): Connection = ds.getConnection + def close(): Unit = () +} + +/** A JdbcDataSource which can load a JDBC `Driver` from a class name */ +trait DriverBasedJdbcDataSource extends JdbcDataSource { + private[this] var registeredDriver: Driver = null + + protected[this] def registerDriver(driverName: String, url: String): Unit = if(driverName ne null) { + val oldDriver = try DriverManager.getDriver(url) catch { case ex: SQLException if "08001" == ex.getSQLState => null } + if(oldDriver eq null) { + Class.forName(driverName) + registeredDriver = DriverManager.getDriver(url) + } + } + + /** Deregister the JDBC driver if it was registered by this JdbcDataSource. + * Returns true if an attempt was made to deregister a driver. */ + def deregisterDriver(): Boolean = + if(registeredDriver ne null) { DriverManager.deregisterDriver(registeredDriver); true } + else false +} + +/** A JdbcDataSource for lookup via a `Driver` or the `DriverManager` */ +class DriverJdbcDataSource(url: String, user: String, password: String, prop: Properties, + driverName: String = null, driver: Driver = null, + connectionPreparer: ConnectionPreparer = null) extends DriverBasedJdbcDataSource { + registerDriver(driverName, url) + + val connectionProps = if(prop.ne(null) && user.eq(null) && password.eq(null)) prop else { + val p = new Properties(prop) + if(user ne null) p.setProperty("user", user) + if(password ne null) p.setProperty("password", password) + p + } + + def createConnection(): Connection = { + val conn = (if(driver eq null) DriverManager.getConnection(url, connectionProps) + else { + val conn = driver.connect(url, connectionProps) + if(conn eq null) + throw new SQLException("Driver " + driver + " does not know how to handle URL " + url, "08001") + conn + }) + if(connectionPreparer ne null) connectionPreparer(conn) + conn + } + + def close(): Unit = () +} + +object DriverJdbcDataSource extends JdbcDataSourceFactory { + def forConfig(c: Config, driver: Driver): DriverJdbcDataSource = { + val cp = new ConnectionPreparer(c) + new DriverJdbcDataSource(c.getString("url"), c.getStringOr("user"), c.getStringOr("password"), + c.getPropertiesOr("properties"), c.getStringOr("driver"), driver, if(cp.isLive) cp else null) + } +} + +/** A JdbcDataSource for a BoneCP connection pool */ +class BoneCPJdbcDataSource(val ds: com.jolbox.bonecp.BoneCPDataSource, driverName: String) extends DriverBasedJdbcDataSource { + registerDriver(driverName, ds.getJdbcUrl) + + def createConnection(): Connection = ds.getConnection() + def close(): Unit = ds.close() +} + +object BoneCPJdbcDataSource extends JdbcDataSourceFactory { + import com.jolbox.bonecp._ + import com.jolbox.bonecp.hooks._ + + def forConfig(c: Config, driver: Driver): BoneCPJdbcDataSource = { + if(driver ne null) + throw new SlickException("An explicit Driver object is not supported by BoneCPJdbcDataSource") + val ds = new BoneCPDataSource + + // Connection settings + ds.setJdbcUrl(c.getString("url")) + c.getStringOpt("user").foreach(ds.setUsername) + c.getStringOpt("password").foreach(ds.setPassword) + c.getPropertiesOpt("properties").foreach(ds.setDriverProperties) + + // Pool configuration + ds.setPartitionCount(c.getIntOr("partitionCount", 1)) + ds.setMaxConnectionsPerPartition(c.getIntOr("maxConnectionsPerPartition", 30)) + ds.setMinConnectionsPerPartition(c.getIntOr("minConnectionsPerPartition", 5)) + ds.setAcquireIncrement(c.getIntOr("acquireIncrement", 1)) + ds.setAcquireRetryAttempts(c.getIntOr("acquireRetryAttempts", 10)) + ds.setAcquireRetryDelayInMs(c.getMillisecondsOr("acquireRetryDelay", 1000)) + ds.setConnectionTimeoutInMs(c.getMillisecondsOr("connectionTimeout", 1000)) + ds.setIdleMaxAge(c.getMillisecondsOr("idleMaxAge", 1000 * 60 * 10), TimeUnit.MILLISECONDS) + ds.setMaxConnectionAge(c.getMillisecondsOr("maxConnectionAge", 1000 * 60 * 60), TimeUnit.MILLISECONDS) + ds.setDisableJMX(c.getBooleanOr("disableJMX", true)) + ds.setStatisticsEnabled(c.getBooleanOr("statisticsEnabled")) + ds.setIdleConnectionTestPeriod(c.getMillisecondsOr("idleConnectionTestPeriod", 1000 * 60), TimeUnit.MILLISECONDS) + ds.setDisableConnectionTracking(c.getBooleanOr("disableConnectionTracking", true)) + ds.setQueryExecuteTimeLimitInMs(c.getMillisecondsOr("queryExecuteTimeLimit")) + c.getStringOpt("initSQL").foreach(ds.setInitSQL) + ds.setLogStatementsEnabled(c.getBooleanOr("logStatements")) + c.getStringOpt("connectionTestStatement").foreach(ds.setConnectionTestStatement) + + // Connection preparer + val cp = new ConnectionPreparer(c) + if(cp.isLive) ds.setConnectionHook(new AbstractConnectionHook { + override def onCheckOut(conn: ConnectionHandle) = cp(conn) + }) + + new BoneCPJdbcDataSource(ds, c.getStringOr("driver")) + } +} + +/** Set parameters on a new Connection. This is used by both, + * [[DriverJdbcDataSource]] and [[BoneCPJdbcDataSource]]. */ +class ConnectionPreparer(c: Config) extends (Connection => Unit) { + val autocommit = c.getBooleanOpt("autocommit") + val isolation = c.getStringOpt("isolation").map { + case "NONE" => Connection.TRANSACTION_NONE + case "READ_COMMITTED" => Connection.TRANSACTION_READ_COMMITTED + case "READ_UNCOMMITTED" => Connection.TRANSACTION_READ_UNCOMMITTED + case "REPEATABLE_READ" => Connection.TRANSACTION_REPEATABLE_READ + case "SERIALIZABLE" => Connection.TRANSACTION_SERIALIZABLE + case unknown => throw new SlickException(s"Unknown isolation level [$unknown]") + } + val catalog = c.getStringOpt("defaultCatalog") + val readOnly = c.getBooleanOpt("readOnly") + + val isLive = autocommit.isDefined || isolation.isDefined || catalog.isDefined || readOnly.isDefined + + def apply(c: Connection): Unit = if(isLive) { + autocommit.foreach(c.setAutoCommit) + isolation.foreach(c.setTransactionIsolation) + readOnly.foreach(c.setReadOnly) + catalog.foreach(c.setCatalog) + } +} diff --git a/src/main/scala/scala/slick/util/GlobalConfig.scala b/src/main/scala/scala/slick/util/GlobalConfig.scala index 580d327385..6f9c20912b 100644 --- a/src/main/scala/scala/slick/util/GlobalConfig.scala +++ b/src/main/scala/scala/slick/util/GlobalConfig.scala @@ -1,6 +1,9 @@ package scala.slick.util +import scala.language.implicitConversions import com.typesafe.config.{Config, ConfigFactory} +import java.util.concurrent.TimeUnit +import java.util.Properties /** Singleton object with Slick's configuration, loaded from the application config. * This includes configuration for the global driver objects and settings for debug logging. */ @@ -19,3 +22,30 @@ object GlobalConfig { if(config.hasPath(path)) config.getConfig(path) else ConfigFactory.empty() } } + +/** Extension methods to make Typesafe Config easier to use */ +class ConfigExtensionMethods(val c: Config) extends AnyVal { + import scala.collection.JavaConverters._ + + def getBooleanOr(path: String, default: Boolean = false) = if(c.hasPath(path)) c.getBoolean(path) else default + def getIntOr(path: String, default: Int = 0) = if(c.hasPath(path)) c.getInt(path) else default + def getStringOr(path: String, default: String = null) = if(c.hasPath(path)) c.getString(path) else default + + def getMillisecondsOr(path: String, default: Long = 0L) = if(c.hasPath(path)) c.getDuration(path, TimeUnit.MILLISECONDS) else default + + def getPropertiesOr(path: String, default: Properties = null) = + if (!c.hasPath(path)) default + else { + val props = new Properties(null) + c.getObject(path).asScala.foreach { case (k, v) => props.put(k, v.unwrapped.toString) } + props + } + + def getBooleanOpt(path: String): Option[Boolean] = if(c.hasPath(path)) Some(c.getBoolean(path)) else None + def getStringOpt(path: String) = Option(getStringOr(path)) + def getPropertiesOpt(path: String) = Option(getPropertiesOr(path)) +} + +object ConfigExtensionMethods { + @inline implicit def configExtensionMethods(c: Config): ConfigExtensionMethods = new ConfigExtensionMethods(c) +}