Skip to content

Commit

Permalink
airframe-jdbc: Refine JDBC connection pool (#858)
Browse files Browse the repository at this point in the history
* airframe-jdbc: Redesign connection pool config/interface

* Update doc

* Fix multiple exceptions

* Fix doc
  • Loading branch information
xerial committed Dec 17, 2019
1 parent 5af0887 commit 405e3e0
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 290 deletions.
Expand Up @@ -12,6 +12,7 @@
* limitations under the License.
*/
package wvlet.airframe.control
import scala.util.control.NonFatal

/**
*
Expand All @@ -33,15 +34,26 @@ object Control {
try {
body(resource1, resource2)
} finally {
try {
if (resource1 != null) {
resource1.close()
}
} finally {
if (resource2 != null) {
resource2.close()
closeResources(resource1, resource2)
}
}

def closeResources[R <: AutoCloseable](resources: R*): Unit = {
if (resources != null) {
var exceptionList = List.empty[Throwable]
resources.map { x =>
try {
if (x != null) {
x.close()
}
} catch {
case NonFatal(e) =>
exceptionList = e :: exceptionList
}
}
if (exceptionList.nonEmpty) {
throw MultipleExceptions(exceptionList)
}
}
}
}
Expand Up @@ -66,7 +66,7 @@ class ControlTest extends AirSpec {
}
}

intercept[SecondException] {
val m = intercept[MultipleExceptions] {
Control.withResources(
new AutoCloseable {
override def close(): Unit = throw new FirstException()
Expand All @@ -78,5 +78,7 @@ class ControlTest extends AirSpec {
// do nothing
}
}
m.causes.exists(_.getClass == classOf[FirstException]) shouldBe true
m.causes.exists(_.getClass == classOf[SecondException]) shouldBe true
}
}
125 changes: 2 additions & 123 deletions airframe-jdbc/README.md
@@ -1,127 +1,6 @@
airframe-jdbc
====

airframe-jdbc is a reusable JDBC connection pool implementation with Airframe.
airframe-jdbc is a reusable JDBC connection pool implementation.

Currently we are supporting these databases:

- **sqlite**: SQLite
- **postgres**: PostgreSQL (e.g., [AWS RDS](https://aws.amazon.com/rds/))

Adding a new connection pool type would be easy. Your contributions are welcome.


## Usage
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.wvlet.airframe/airframe-jdbc_2.12/badge.svg)](http://central.maven.org/maven2/org/wvlet/airframe/airframe-jdbc_2.12/)

**build.sbt**

```scala
libraryDependencies += "org.wvlet.airframe" %% "airframe-jdbc" % "(version)"
```


```scala
import wvlet.airframe._
import wvlet.airframe.jdbc._

// Import ConnectionPoolFactoryService
trait MyDbTest extends ConnectionPoolFactoryService {
// Create a new connection pool. The created pool will be closed automatically
// after the Airframe session is terminated.
val connectionPool = bind{ config:DbConfig => connectionPoolFactory.newConnectionPool(config) }

// Create a new database
connectionPool.executeUpdate("craete table if not exists test(id int, name text)")
// Update the database with prepared statement
connectionPool.updateWith("insert into test values(?, ?)") { ps =>
ps.setInt(1, 1)
ps.setString(2, "name")
}
// Read ResultSet
connectionPoo.executeQuery("select * from test") { rs =>
// Traverse the query ResultSet here
while (rs.next()) {
val id = rs.getInt("id")
val name = rs.getString("name")
println(s"read (${id}, ${name})")
}
}
}

...

// Configuring database
val d = newDesign
// ConnectionPoolFactory should be a singleton so as not to create duplicated pools
.bind[ConnectionPoolFactory].toSingleton
.bind[DbConfig].toInstance(DbConfig.ofSQLite(path="mydb.sqlite"))

d.withSession { session =>
val t = session.build[MyDbTest]

// You can make queries using the connection pool
t.connectionPool.executeQuery("select ...")
}
// Connection pools will be closed here

```

## Using PostgreSQL

For using RDS, configure DbConfig as follows:

```scala
val d = newDesign
.bind[ConnectionPoolFactory].toSingleton
.bind[DbConfig].toInstance(
DbConfig.ofPostgreSQL(
host="(your RDS address, e.g., mypostgres.xxxxxx.us-east-1.rds.amazonaws.com)",
database="mydatabase"
)
.withUser("postgres")
.withPassword("xxxxx")
)
```

For accessing a local PostgreSQL without SSL support, disable SSL access like this:
```scala
val d = newDesign
.bind[ConnectionPoolFactory].toSingleton
.bind[DbConfig].toInstance(
DbConfig.ofPostgreSQL(
host="(your RDS address, e.g., mypostgres.xxxxxx.us-east-1.rds.amazonaws.com)",
database="mydatabase"
)
.withUser("postgres")
.withPassword("xxxxx"),
PostgreSQLConfig(useSSL=false)
)
```

## Creating multiple connection pools

You can create multiple connection pools with different configurations by using type aliases to DbConfig:

```scala
import wvlet.airframe._
import wvlet.airframe.jdbc._

object MultipleConnection {
type MyDb1Config = DbConfig
type MyDb2Config = DbConfig
}

import MultipleConnection._

trait MultipleConnection extends ConnectionPoolFactoryService {
val pool1 = bind{ c:MyDB1Config => connectionPoolFactory.newConnectionPool(c) }
val pool2 = bind{ c:MyDB2Config => connectionPoolFactory.newConnectionPool(c) }
}

val d = newDesign
.bind[ConnectionPoolFactory].toSingleton
.bind[MyDb1Config].toInstance(DbConfig.ofSQLite(path="mydb.sqlite"))
.bind[MyDb2Config].toInstance(DbConfig.ofPostgreSQL(database="mydatabase"))

```
- [Documentaiton](https://wvlet.org/airframe/docs/airframe-jdbc)
Expand Up @@ -15,18 +15,30 @@ package wvlet.airframe.jdbc

import java.sql.{Connection, PreparedStatement, ResultSet}

import wvlet.airframe._
import wvlet.log.LogSupport
import wvlet.log.io.IOUtil.withResource
import wvlet.log.{Guard, LogSupport}

import scala.util.{Failure, Success, Try}
object ConnectionPool {
def apply(config: DbConfig): ConnectionPool = {
val pool: ConnectionPool = config.`type` match {
case "sqlite" => new SQLiteConnectionPool(config)
case other =>
new GenericConnectionPool(config)
}
pool
}

def newFactory: ConnectionPoolFactory = new ConnectionPoolFactory()
}

trait ConnectionPool extends LogSupport {
trait ConnectionPool extends LogSupport with AutoCloseable {
def config: DbConfig

def withConnection[U](body: Connection => U): U
def stop: Unit

override def close(): Unit = stop

def executeQuery[U](sql: String)(handler: ResultSet => U): U = {
withConnection { conn =>
withResource(conn.createStatement()) { stmt =>
Expand Down Expand Up @@ -68,52 +80,3 @@ trait ConnectionPool extends LogSupport {
}
}
}

trait ConnectionPoolFactoryService {
val connectionPoolFactory = bind[ConnectionPoolFactory]
}

trait ConnectionPoolFactory extends Guard with AutoCloseable with LogSupport {
private var createdPools = List.empty[ConnectionPool]

/**
* Use this method to add a precisely configured connection pool
* @param pool
* @return
*/
def addConnectionPool(pool: ConnectionPool): ConnectionPool = {
guard {
// Register the generated pool to the list
createdPools = pool :: createdPools
}
pool
}

/**
*
* @param config
* @param pgConfig
* @return
*/
def newConnectionPool(config: DbConfig, pgConfig: PostgreSQLConfig = PostgreSQLConfig()): ConnectionPool = {
val pool: ConnectionPool = config.`type` match {
case "postgresql" => new PostgreSQLConnectionPool(config, pgConfig)
case "sqlite" => new SQLiteConnectionPool(config)
case other =>
throw new IllegalArgumentException(s"Unsupported database type ${other}")
}
addConnectionPool(pool)
}

override def close: Unit = {
guard {
createdPools.foreach { x =>
Try(x.stop) match {
case Success(u) => // OK
case Failure(e) => warn(e)
}
}
createdPools = List.empty
}
}
}
@@ -0,0 +1,52 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package wvlet.airframe.jdbc
import wvlet.airframe.control.Control
import wvlet.log.{Guard, LogSupport}

/**
* A factory for managing multiple connection pools and properly closes these pools upon shutdown
*/
class ConnectionPoolFactory extends Guard with AutoCloseable with LogSupport {
private var createdPools = List.empty[ConnectionPool]

/**
* Use this method to add a precisely configured connection pool
* @param pool
* @return
*/
def addConnectionPool(pool: ConnectionPool): ConnectionPool = {
guard {
// Register the generated pool to the list
createdPools = pool :: createdPools
}
pool
}

/**
* Add a new connection pool for a specific database
* @param config
* @return
*/
def newConnectionPool(config: DbConfig): ConnectionPool = {
addConnectionPool(ConnectionPool(config))
}

override def close: Unit = {
guard {
Control.closeResources(createdPools: _*)
createdPools = List.empty
}
}
}

0 comments on commit 405e3e0

Please sign in to comment.