【问题标题】:Avoiding mutable variables in Scala + JDBC在 Scala + JDBC 中避免可变变量
【发布时间】:2018-06-08 09:12:13
【问题描述】:

我在 Scala 中有以下代码来使用 JDBC 访问数据库。它工作得很好,但是它使用了几个可变变量(var 声明),因为这些变量需要在 finally 子句中可用才能关闭 JDBC 元素。可以将此代码更改为仅使用不可变变量(val 声明)吗?

var connection:Connection = null
var statement:Statement = null
var resultSet:ResultSet = null

try {
  val url = "someUrl"
  val user = "someUser"
  val password = "somePwd"
  Class.forName("mySql")
  connection = DriverManager.getConnection(url, user, password)
  statement = connection.createStatement()
  resultSet = statement.executeQuery("SELECT column FROM table")
  while ( resultSet.next() ) {
    val col = resultSet.getString(1)
    println(col)
  }

} 
catch {
  case e:Exception => e.printStackTrace
}
finally {
  if (resultSet != null) resultSet.close
  if (statement != null) statement.close
  if (connection != null) connection.close
}

【问题讨论】:

  • 在您查看了以下答案后,请考虑 Martin Odersky 的书 (lirmm.fr/~ducour/Doc-objets/scalabook.pdf) 的第 27 页; “面向对象和函数式编程的融合体现在 Scala 的许多不同方面。这两种编程风格具有互补优势”..
  • FP 在没有副作用的情况下最强,使用 jdbc 驱动程序从根本上涉及状态。这会导致您在上面的示例中观察到的那种挫败感/挑战。许多人将 jdbc 驱动程序的有状态特性封装起来,并在其顶部放置一个 FP 抽象;比如光滑的 db api (slick.lightbend.com)。就个人而言,我喜欢在这一点上进行声明,但这是另一个话题。

标签: scala functional-programming


【解决方案1】:
  • 您可以创建小函数来返回连接。
  • 当您没有变量值时,您不必在顶部声明变量(例如connectionstatementresultSet)。相反,您可以创建函数以将实际值返回为 Option[T]

  • 或者由于您需要参考connectionstatement 等来关闭它们,this answer has nice explanation 关闭资源。我正在窃取相同的代码在这里使用。请参阅函数autoClean,它获取您要处理的资源以及要清理资源的代码。

  • 另请参阅收集resultSet 数据的不可变方式。

所以,你的样子,

import java.sql.{Connection, DriverManager}

object AutoCleanJdbcConnection {

  val url = "someUrl"
  val user = "someUser"
  val password = "somePwd"

  def queryDb(): Option[Seq[String]] = {

    autoClean(dbConnection) { connection =>

      autoClean(connection.createStatement()) { statement =>

        autoClean(statement.executeQuery("SELECT column FROM table")) { result =>

          new Iterator[String] {
            def hasNext: Boolean = result.next()

            def next(): String = result.getString(1)
          }.toSeq

        }
      }
    }.flatten.flatten

  }

  def dbConnection: Connection = {
    Class.forName("mySql")
    DriverManager.getConnection(url, user, password)
  }

  def main(args: Array[String]): Unit = {
    queryDb().foreach { data =>
      println(data)
    }
  }

  def autoClean[A <: AutoCloseable, B](resource: A)(ops: A => B): Option[B] = cleanly(resource)(_.close())(ops)

  /**
    * can be returning Either[B, Throwable]
    */
  def cleanly[A, B](resource: => A)(resourceCleanupCode: A => Unit)(operationCode: A => B): Option[B] = {
    try {
      val res = resource
      try {
        Some(operationCode(res))
      }
      finally {
        resourceCleanupCode(res)
      }
    } catch {
      case e: Exception => None
    }
  }
}

注意:代码已编译,但我没有针对数据库运行此代码。

【讨论】:

  • 不错的解决方案!值得注意的是,可以利用常见的Autocloseable 接口来专门化cleanly 函数,如下例所示:def autoclosing[A &lt;: Autoclosable, B](a: A)(f: A =&gt; B): Option[B] = cleanly(a)(_.close())(f)
  • @stefanobaghino 很酷,更好,因为无论如何都需要关闭所有资源,为什么不定义autoCleaner :)
  • 您的解决方案也可以,但我只能选择一个答案
【解决方案2】:

这是一个相对直截了当的翻译,将您所掌握的内容翻译成更多的 FP。为清晰起见添加了类型注释。 (我没有运行它,但它可以编译。)

import scala.util.Try

val connection:Try[Connection]= Try(DriverManager.getConnection(url, user, password))
val statement: Try[Statement] = connection.map(_.createStatement())
val resultSet: Try[ResultSet] = statement.map(_.executeQuery("SELECT column FROM table"))

resultSet.map(rs => while (rs.next()) println(rs.getString(1)))  
         .recover{case e => e.printStackTrace()}

resultSet.foreach(_.close())
statement.foreach(_.close())
connection.foreach(_.close())

这个想法是通过将失败条件作为变量类型的一部分来避免vars。

在这种情况下,如果getConnection() 失败,您将不会尝试createStatement(),如果createStatement() 失败,您将不会尝试executeQuery(),并且您将仅close() 那些没有不要失败。

【讨论】:

  • 当我们从 rs.next() 或 executeQuery() 获得异常时,资源会发生什么?
  • @ChrisK;关于rs.next() 和/或rs.getString() 的好点。更新。 executeQuery() 故障已解决并存储在 resultSet val 中。
【解决方案3】:

这是一种基于 Scala 的 Try 和 Stream 类的方法。它建立在 jwvh 的答案之上,通过添加异常处理来确保即使在错误情况下也能关闭所有资源。

这种方法通过将异常捕获到 Scala 的 Try 抽象中来避免 var,并通过将 JDBC 行处理转换为 Scala 集合来避免 while 循环。

Try 可以保持两种状态,一个成功的值或者一个异常。 只有当 tryInstance 的值持有成功的值时,才会调用 tryInstance.map。

Scala Stream 是惰性的,即它们一次计算一个值并延迟计算下一个值。这让我们可以将 while 循环移动到 Scala 集合库中,但代价是有一点额外的逻辑来检测流的结束,从而在正确的时间关闭资源。

注意此代码尚未经过测试,但我已在生产代码中成功使用此策略。出于说明目的,我在此处包含以下代码。

import Stream._

Class.forName("mySql")

def doQuery( url:String, user:String, password:String ): List[String] = {
  val conn      = Try(DriverManager.getConnection(url, user, password))
  val statement = conn.map( _.createStatement() )
  val rs        = statement.map( _.executeQuery("SQL") )

  toStream(rs,statement,conn).toList
}

private def toStream(rs:Try[ResultSet], s:Try[Statement], c:Try[Connection]) : Stream[String] = {
  try {
    val resultSet = rs.get // will throw an exception if any previous problems had occurred while opening the connection and parsing the sql

    if ( resultSet.next() ) {
      resultSet.getString(1) #:: toStream(rs,s,c)
    } else {
      close(rs,s,c)

      Stream.empty
    }
  } catch {
    case ex =>
      close(rs,s,c)
      throw ex
  }
}

【讨论】:

    【解决方案4】:

    我看到有很多样板代码,例如获取连接、关闭连接、处理故障!这就是 Java 应该做的,但是你想写 Scala。因此,如果不直接使用 JDBC 库是一个选项,您可以尝试使用一些映射库来为您完成这个样板。

    例如,您可以看看 Slick,它可以帮助您编写数据库抽象,同时保持功能范式不变!

    【讨论】:

    • 我使用 slick,但这个特殊用例应该是 JDBC,因为我在运行时构建 SQL。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-02-20
    • 2012-02-21
    • 2017-01-18
    • 1970-01-01
    • 2012-11-28
    • 1970-01-01
    • 2012-12-19
    相关资源
    最近更新 更多