将SQL结果集视为Scala流

将SQL结果集视为Scala流,scala,stream,resultset,Scala,Stream,Resultset,当我查询一个数据库并收到一个(仅转发、只读)结果集时,结果集的作用就像一个数据库行列表 我正试图找到某种方法,将此结果集视为Scala流。这将允许诸如过滤器、映射等操作,同时不消耗大量RAM 我实现了一个尾部递归方法来提取单个项,但这需要所有项同时在内存中,如果结果集非常大,这是一个问题: // Iterate through the result set and gather all of the String values into a list // then return that li

当我查询一个数据库并收到一个(仅转发、只读)结果集时,结果集的作用就像一个数据库行列表

我正试图找到某种方法,将此结果集视为Scala
。这将允许诸如
过滤器
映射
等操作,同时不消耗大量RAM

我实现了一个尾部递归方法来提取单个项,但这需要所有项同时在内存中,如果结果集非常大,这是一个问题:

// Iterate through the result set and gather all of the String values into a list
// then return that list
@tailrec
def loop(resultSet: ResultSet,
         accumulator: List[String] = List()): List[String] = {
  if (!resultSet.next) accumulator.reverse
  else {
    val value = resultSet.getString(1)
    loop(resultSet, value +: accumulator)
  }
}

我没有测试它,但为什么它不起作用

new Iterator[String] {
  def hasNext = resultSet.next()
  def next() = resultSet.getString(1)
}.toStream

我需要类似的东西。在elbowich非常酷的答案的基础上,我对它进行了一点包装,并返回结果,而不是字符串(这样您就可以得到任何列)

我需要访问表元数据,但这将适用于表行(可以执行stmt.executeQuery(sql)而不是md.getColumns):


@elbowich答案的实用函数:

def results[T](resultSet: ResultSet)(f: ResultSet => T) = {
  new Iterator[T] {
    def hasNext = resultSet.next()
    def next() = f(resultSet)
  }
}
允许您使用类型推断。例如:

stmt.execute("SELECT mystr, myint FROM mytable")

// Example 1:
val it = results(stmt.resultSet) {
  case rs => rs.getString(1) -> 100 * rs.getInt(2)
}
val m = it.toMap // Map[String, Int]

// Example 2:
val it = results(stmt.resultSet)(_.getString(1))

因为ResultSet只是一个由next导航的可变对象,所以我们需要定义自己的下一行概念。我们可以通过如下输入函数来实现:

class ResultSetIterator[T](rs: ResultSet, nextRowFunc: ResultSet => T) 
extends Iterator[T] {

  private var nextVal: Option[T] = None

  override def hasNext: Boolean = {
    val ret = rs.next()
    if(ret) {
      nextVal = Some(nextRowFunc(rs))
    } else {
      nextVal = None
    }
    ret
  }

  override def next(): T = nextVal.getOrElse { 
    hasNext 
    nextVal.getOrElse( throw new ResultSetIteratorOutOfBoundsException 
  )}

  class ResultSetIteratorOutOfBoundsException extends Exception("ResultSetIterator reached end of list and next can no longer be called. hasNext should return false.")
}
编辑:
按照上面所述,翻译成流或其他形式。

对于隐式类来说,这听起来是一个很好的机会。首先在某处定义隐式类:

import java.sql.ResultSet

object Implicits {

    implicit class ResultSetStream(resultSet: ResultSet) {

        def toStream: Stream[ResultSet] = {
            new Iterator[ResultSet] {
                def hasNext = resultSet.next()

                def next() = resultSet
            }.toStream
        }
    }
}
接下来,只要在执行查询并定义ResultSet对象的位置导入该隐式类即可:

import com.company.Implicits._
最后使用toStream方法获取数据。例如,获取所有ID,如下所示:

val allIds = resultSet.toStream.map(result => result.getInt("id"))

这种实现虽然更长、更笨拙,但与ResultSet契约的对应性更好。副作用已从hasNext(…)中移除并移动到next()


我认为上面的大多数实现都有一个不确定的
hasNext
方法。调用两次会将光标移动到第二行。我建议使用类似的方法:

  new Iterator[ResultSet] {
    def hasNext = {
      !resultSet.isLast
    }
    def next() = {
      resultSet.next()
      resultSet
    }
  }

这里有一个替代方案,类似于Sergey Alaev和thoredge的解决方案,当我们需要一个尊重
迭代器
合同的解决方案时,
hasNext
没有副作用

假设函数
f:ResultSet=>T

Iterator.unfold(resultSet.next()){hasNext=>
Option.when(hasNext)(f(resultSet),resultSet.next())
}
我发现在
ResultSet
上使用as
map
“extension method”很有用

隐式类resultsetop(resultSet:resultSet){
def-map[T](f:ResultSet=>T):迭代器[T]={
Iterator.unfold(resultSet.next()){hasNext=>
Option.when(hasNext)(f(resultSet),resultSet.next())
}
}
}

上述版本的另一个变体,适用于Scala 2.12:

implicit class ResultSetOps(resultSet: ResultSet) {
 def map[T](f: ResultSet => T): Iterator[T] =
  Iterator.continually(resultSet).takeWhile(_.next()).map(f)
}

你能用Iterable代替Stream来做你想做的事情吗?另外,Stream会将值保留在内存中,因此当你到达列表的末尾时,你不会真正节省内存。我认为,如果没有jdbc标志/选项使jdbc本身将结果流化,那么内存中仍然有一个完整的数据副本,由JDBCAPI构建。看起来很完美。我将在数据库设置好后立即测试它。我甚至不认为我需要将其转换为
流。我可以直接对它应用
map
filter
等。我想给你第二次投票。我已将此代码片段添加到Scala代码段库中。它很快成为我的最爱之一。这是一个很酷的解决方案,但我担心。我认为
Iterator
的通常约定是
hasNext
没有副作用。它可以在两次调用
next
之间被调用任意次数。有什么东西可以阻止这成为一个问题吗?mysql connector java不适用于我。不确定我是否做错了什么,但是我的
ResultSet
在第二次
next()
调用时关闭了,因此我只能检索一个结果行。在获取所有行之前,它不会自动关闭的唯一方法似乎是使用
while(rs.next()){…}
,因此我将项目单独添加到
while
中的
scala.collection.mutable.ListBuffer
。看起来不太漂亮,但想不出其他方法。@Nick使用
新迭代器[String]{…}。toList
而不是
。toStream
将立即获取整个结果集,而不仅仅是第一行。如果您不需要返回流(例如,仅向前迭代),您可以使用迭代器。这大大减少了使用流时的内存开销(返回
迭代器[ResultSet]
,并删除
到流
)是否确实有效?它在DB2上失败,结果集关闭。如果这在您的情况下起作用,可能取决于特定的数据库品牌和/或配置?确实如此,但您只能在连接保持打开的情况下使用流。如果关闭连接,流将失败,迭代器也将失败。
new Iterator[String] {
  private var available = resultSet.next()
  override def hasNext: Boolean = available
  override def next(): String = {
    val string = resultSet.getString(1)
    available = resultSet.next()
    string
  }
}
  new Iterator[ResultSet] {
    def hasNext = {
      !resultSet.isLast
    }
    def next() = {
      resultSet.next()
      resultSet
    }
  }
Iterator.continually(rs.next())
  .takeWhile(identity)
  .map(_ => Model(
      id = rs.getInt("id"),
      text = rs.getString("text")
   ))
implicit class ResultSetOps(resultSet: ResultSet) {
 def map[T](f: ResultSet => T): Iterator[T] =
  Iterator.continually(resultSet).takeWhile(_.next()).map(f)
}