Dustin Getz
Dustin Getz

Reputation: 21811

scala chaining Trys with managed resource that needs finally/close()

For practice, I've gotten some straightforward JDBC stuff from like 100 lines down to this, but it won't typecheck. Any ideas? Better approaches?

def withResultSet[T](sql: String, f: ResultSet => T)(implicit info: ConnectionInfo): Try[T] = {
    for {
      conn <- Try(connect(info))
      stmt <- Try(conn.createStatement()) orElse { case err: SQLException => {conn.close(); err} }
      results <- Try(stmt.executeQuery(sql)) orElse { case err: SQLException => { conn.close(); stmt.close(); err }}
    } yield f(results)
  }

and my error is

 missing parameter type for expanded function
The argument types of an anonymous function must be fully known. (SLS 8.5)
Expected type was: scala.util.Try[?]
      stmt <- Try(conn.createStatement()) orElse { case err: SQLException => {conn.close(); err} }
                                                 ^

Upvotes: 2

Views: 804

Answers (3)

Beryllium
Beryllium

Reputation: 13008

You can split your function into different parts, one for each resource type, and use it together with a separate resource manager trait.

The generalized resource manager (can be used with files etc. as well):

trait ResourceManager {
  def withResource[T <: {def close()}, R](resource: T)(code: (T) => R): R = {
    try {
      code(resource)
    } finally {
      import scala.language.reflectiveCalls
      resource.close()
    }
  }

is mixed into something like this

class Db extends ResourceManager {
  private def getConnection = ...

  def withConnection[T](f: (Connection) => T): T = {
    withResource(getConnection) {
      conn =>
        f(conn)
    }
  }

  def withStatement[T](f: (Statement) => T): T = {
    withConnection {
      conn =>
        withResource(conn.createStatement()) {
          stmnt =>
            f(stmnt)
        }
    }
  }

  def withResultSet[T](selectSqlCmd: String)(f: (ResultSet) => T): T = {
    withStatement {
      stmnt => {
        withResource(stmnt.executeQuery(selectSqlCmd)) {
          rs =>
            f(rs)
        }
      }
    }
  }
}

which stacks the resources, and gives entry points at each level.

On top of this another level

  def mapResultSet[T, C <: Iterable[T]](selectSqlCmd: String)
                              (f: (ResultSet) => T)
                              (implicit cbf: CanBuildFrom[Nothing, T, C]): C = {
    withResultSet(selectSqlCmd) {
      rs =>
        val builder = cbf()
        while (rs.next()) {
          builder += f(rs)
        }
        builder.result()
    }
  }

Each method goes only one step further down, the for comprehension is decomposed into separate functions, and the Try does not interfere.

Use it like this:

val db = new Db()
val result = db.mapResultSet("select * from pg_user")(rs => rs.getString(1))

to read from the database in a single line.

Upvotes: 2

huynhjl
huynhjl

Reputation: 41646

I don't know that Try is the right tool to dispose of resources once they are no longer needed. At least I cannot see an obvious way to do it. You may want to look at https://github.com/jsuereth/scala-arm. Then your code may look like this:

def withResultSet[T](sql: String, f: ResultSet => T)(
                     implicit info: ConnectionInfo): ManagedResource[T] = for {
  conn <- managed(connect(info))
  stmt <- managed(conn.createStatement()) 
  results <- managed(stmt.executeQuery(sql))
} yield f(results)

You can keep using map or flatMap (or for comprehension) to work with possibly other resources and at the end you can get an Option or an Either out of it and at the same time close everything that needs to be closed.

Upvotes: 3

Dustin Getz
Dustin Getz

Reputation: 21811

recoverWith is what i needed; you don't actually need to recover you can just rethrow.

def withResultSet[T](sql: String, f: ResultSet => T)(implicit info: ConnectionInfo): Try[T] = {
    for {
      conn <- Try(connect(info))
      stmt <- Try(conn.createStatement()) recoverWith { case err => {conn.close(); Failure(err)} }
      results <- Try(stmt.executeQuery(sql)) recoverWith { case err => { conn.close(); stmt.close(); Failure(err) }}
    } yield f(results)
  }

Upvotes: 1

Related Questions