Обработка набора результатов SQL как потока Scala

Когда я запрашиваю базу данных и получаю обратно набор результатов (только для чтения и только для пересылки), набор результатов действует как список строк базы данных.

Я пытаюсь найти способ обрабатывать этот ResultSet как Scala Stream. Это позволит выполнять такие операции, как filter, map и т. д., при этом не потребляя больших объемов оперативной памяти.

Я реализовал метод хвостовой рекурсии для извлечения отдельных элементов, но для этого требуется, чтобы все элементы находились в памяти одновременно, что является проблемой, если ResultSet очень большой:

// 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)
  }
}

person Ralph    schedule 09.03.2012    source источник
comment
Не могли бы вы использовать Iterable вместо Stream, чтобы делать то, что вы хотите?   -  person Leif Wickland    schedule 09.03.2012
comment
Кроме того, поток в любом случае сохранит значения в памяти, поэтому вы фактически не сохраните память к тому времени, когда достигнете конца списка.   -  person Richard Todd    schedule 30.07.2013
comment
Я думаю, что без флага/параметра jdbc, который заставляет сам jdbc передавать результаты, у вас все еще есть одна полная копия данных в памяти, созданная вашим jdbc API.   -  person matanster    schedule 08.03.2016


Ответы (9)


Я не проверял, но почему не работает?

new Iterator[String] {
  def hasNext = resultSet.next()
  def next() = resultSet.getString(1)
}.toStream
person elbowich    schedule 09.03.2012
comment
Это выглядит идеально. Я проверю это, как только настрою свою базу данных. Я даже не думаю, что мне нужно преобразовать его в Stream. Я могу применить map, filter и т. д. непосредственно к нему. - person Ralph; 10.03.2012
comment
Я хотел бы дать вам второй голос. Я добавил этот фрагмент кода в свою библиотеку фрагментов Scala. Он быстро становится одним из моих любимых. - person Ralph; 22.03.2012
comment
Это классное решение, но я беспокоюсь. Я думаю, что обычный контракт Iterator заключается в том, что hasNext не имеет побочных эффектов. Его можно вызывать любое количество раз между двумя вызовами next. Что-то мешает этому стать проблемой? - person Daniel Darabos; 25.01.2016
comment
Хороший ответ, но какова фактическая реализация? - person Yordan Georgiev; 14.10.2016
comment
У меня не работало с mysql-connector-java версии 6. Не уверен, что я сделал что-то не так, но мой ResultSet закрылся при втором вызове next(), поэтому я смог получить только одну строку результата. Единственный способ, которым он не закрывается автоматически до того, как я получил все строки, похоже, использует while (rs.next()) {...}, поэтому я добавляю элементы по отдельности в scala.collection.mutable.ListBuffer внутри while. Выглядит некрасиво, но по-другому не придумал. - person Nick; 07.04.2017
comment
@Nick Использование new Iterator[String]{ ... }.toList вместо .toStream приведет к немедленному получению всего набора результатов, а не только первой строки. - person steinar; 22.08.2017
comment
Это преобразование одного столбца из rs в поток. Есть ли способ преобразовать rs с несколькими столбцами в один массив/список с несколькими измерениями? - person davidzxc574; 29.07.2020

Вспомогательная функция для ответа @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))
person hraban    schedule 30.04.2015

Это звучит как отличная возможность для неявного класса. Сначала определите неявный класс где-нибудь:

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. Например, получите все идентификаторы, как показано ниже:

val allIds = resultSet.toStream.map(result => result.getInt("id"))
person Jeroen Minnaert    schedule 29.09.2016
comment
Вы уверены, что это работает? В DB2 происходит сбой с закрытием ResultSet. Если это сработало в вашем случае, возможно, это зависит от конкретной марки и/или конфигурации базы данных? - person Sergio Pelin; 31.08.2018
comment
Это так, но вы можете использовать поток только до тех пор, пока ваше соединение остается открытым. Если вы закроете соединение, поток завершится ошибкой, как и итератор. - person Jeroen Minnaert; 05.09.2018

мне нужно было нечто подобное. Основываясь на очень крутом ответе Лолича, я немного завернул его и вместо строки возвращаю результат (так что вы можете получить любой столбец)

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

Мне нужно было получить доступ к метаданным таблицы, но это будет работать для строк таблицы (можно сделать stmt.executeQuery(sql) вместо md.getColumns):

 val md = connection.getMetaData()
 val columnItr = resultSetItr( md.getColumns(null, null, "MyTable", null))
      val columns = columnItr.map(col => {
        val columnType = col.getString("TYPE_NAME")
        val columnName = col.getString("COLUMN_NAME")
        val columnSize = col.getString("COLUMN_SIZE")
        new Column(columnName, columnType, columnSize.toInt, false)
      })
person Greg    schedule 19.08.2014
comment
Если вам не нужно возвращаться к потоку (например, только прямая итерация), вы можете просто использовать итератор. Это значительно снижает нагрузку на память при использовании потока (возвратите Iterator[ResultSet] и отбросьте toStream). - person Greg; 15.09.2014

Поскольку 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.")
}

РЕДАКТИРОВАТЬ: перевести в поток или что-то еще, как указано выше.

person Brendan    schedule 17.05.2016

Вот альтернатива, аналогичная решениям Сергея Алаева и Тореджа, когда нам нужно решение, которое соблюдает контракт Iterator, где hasNext не имеет побочных эффектов.

Предполагая функцию f: ResultSet => T:

Iterator.unfold(resultSet.next()) { hasNext =>
  Option.when(hasNext)(f(resultSet), resultSet.next())
}

Я нашел полезным иметь в качестве map "метод расширения" на ResultSet.

implicit class ResultSetOps(resultSet: ResultSet) {
    def map[T](f: ResultSet => T): Iterator[T] = {
      Iterator.unfold(resultSet.next()) { hasNext =>
        Option.when(hasNext)(f(resultSet), resultSet.next())
      }
    }
  }
person henko    schedule 30.10.2019

Эта реализация, хотя и длиннее и неуклюже, лучше соответствует контракту ResultSet. Побочный эффект был удален из hasNext(...) и перемещен в next().

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
  }
}
person thoredge    schedule 22.05.2018

Я думаю, что большинство из вышеперечисленных реализаций имеют недетерминированный метод hasNext. Вызов его два раза переместит курсор на вторую строку. Я бы посоветовал использовать что-то вроде этого:

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

Другой вариант вышеизложенного, который работает со Scala 2.12:

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