Как преобразовать результаты jdbc в неизменяемую коллекцию

У меня есть небольшое приложение, написанное на scala, которое отправляет запрос в mysql, получает результат, затем конвертирует его в json и отправляет на какой-то http-сервер. Я использую java jdbc и соединитель mysql для подключения к базе данных и спрей-json для преобразования коллекции scala в json. Итак, я создаю соединение с БД, выполняю запрос и затем получаю результат с помощью getResultSet(). Затем я перебираю его и копирую результат на изменяемую карту:

while(result.next()) {
    val SomeExtractor(one, two) = result
    map.update(one, map.getOrElse(one, List()) ::: List(two))
}

Это отлично работает, но затем мне нужно преобразовать результат в неизменяемую карту, потому что spray-json не может преобразовать изменяемые коллекции в json, насколько мне известно. Есть ли хороший способ преобразовать результат jdbc здесь в неизменяемую коллекцию, не копируя его во временную изменяемую карту? Может быть, можно как-то использовать потоки? Я спрашиваю, потому что похоже, что для этого должен быть какой-то классный функциональный шаблон, о котором я понятия не имею.

p.s. Кстати, я не могу просто использовать Slick, потому что он не поддерживает хранимые процедуры, насколько мне известно.


person Pavel Davydov    schedule 07.02.2014    source источник
comment
Могу я указать вам на интерфейс JDBC Scala, который позволяет вам использовать toList при возврате из запросов и поддерживает хранимые процедуры? github.com/novus/novus-jdbc (я написал это, когда работал там.)   -  person wheaties    schedule 07.02.2014
comment
@wheaties Спасибо, могу ли я получить эту библиотеку из репозитория maven с помощью sbt?   -  person Pavel Davydov    schedule 07.02.2014
comment
Вы знаете, это могло бы помочь, если бы я сделал его доступным для скачивания, но я дошел до этого момента и никогда не публиковал его за пределами компании Maven.   -  person wheaties    schedule 07.02.2014


Ответы (2)


Короткий ответ: вы не можете добиться значительно большего успеха, чем то, что у вас есть. Под капотом функциональной хитрости Scala находится код, очень похожий на ваш. Кроме того, не забывайте, что у изменяемых Map есть метод toMap, который возвращает неизменяемый Map.

Длинный ответ: вы хотите создать интерфейс кода JDBC с кодом Scala. API JDBC не предназначен для использования с функциональными языками, поэтому вам обязательно понадобится какой-нибудь изменяемый/императивный код, чтобы заполнить пробел. На самом деле это просто вопрос пути наименьшего сопротивления.

Если бы вы просто строили карту один к одному, вам бы очень подошла карта MapBuilder. Scala включает Builder классов для большинства своих структур данных, которые используют временные, частные, изменяемые структуры для максимально эффективного построения неизменяемой структуры. Код будет выглядеть примерно так:

val builder = Map.newBuilder[Int, Int]
while(result.next()) {
  val SomeExtractor(one, two) = result
  builder += one -> two
}
return builder.result

Однако на самом деле вы создаете MultiMap — карту от ключей к нескольким значениям. В стандартной библиотеке Scala есть трейт MultiMap, но он не идеален для вашего варианта использования. Он изменчив и хранит значения в изменяемых Sets, а не Lists, поэтому мы пока его проигнорируем.

В стандартной библиотеке Scala есть метод groupBy для свойства Traversable, который делает более или менее то, что вы ищете. У нас есть ResultSet, а не Traversable, но в принципе мы могли бы написать какой-нибудь связующий код, чтобы обернуть ResultSet в Traversable, и воспользоваться этим существующим кодом. Что-то вроде следующего:

// strm has side effects, caused by rs.next - only ever call it once, and re-use result if needed.
def strm: Stream[(Int, Int)] = if (rs.next) SomeExtractor.unapply(rs).get #:: strm else Stream.empty
return strm.groupBy(_._1)

Это сработает, но мы получили пугающее предупреждение о побочных эффектах, и на самом деле мы не увеличили производительность. Если вы посмотрите на исходный код для Traversable.groupBy (см. код на GitHub), на самом деле он делает то же самое, что и вы - создаете изменяемый Map с нашими данными, а затем в конце преобразуете его в неизменяемый Map.

Я думаю, что подход, который у вас уже есть, близок к оптимальному - просто верните map.toMap.

О, и я предположил, что SomeExtractor извлекает пару Ints.

person James_pic    schedule 07.02.2014
comment
Спасибо за ответ! Но нельзя ли как-то здесь использовать поток? Чем вызвать его метод takeWhile(result.next()) для извлечения всех результатов в поток? А потом использовать как сборник. Хотя я не знаю, поддерживается ли он в spray-json. - person Pavel Davydov; 07.02.2014
comment
Да, я думал о чем-то подобном. Я думаю, что делать это таким образом вполне безопасно, если к нему обращаются из одного потока, потому что результат будет скопирован в поток, который неизменен. Я ошибся? - person Pavel Davydov; 07.02.2014
comment
Если вам нужен поток, вам понадобится связующий код. @Aleksey's getStreamOfResults, вероятно, лучший способ сделать это. Получив это, вы можете позвонить groupBy по телефону Stream. Но под обложками он все равно создаст изменяемую карту. - person James_pic; 07.02.2014
comment
Извините, я удалил комментарий, на который вы отвечаете. Код, на который вы ссылаетесь, был def strm: Stream[(Int, Int)] = if (rs.next) SomeExtractor.unapply(rs).get #:: strm else Stream.empty, что эквивалентно коду @Aleksey, но немного более удобно (реализация Iterator.toStream использует тот же прием рекурсивного вызова самого себя). Пока вы гарантируете, что код используется из одного и того же потока, поток не переживает сеанс JDBC, и вы никогда не пытаетесь повторно использовать ResultSet (например, вызывая strm несколько раз. Это должно быть хорошо. - person James_pic; 07.02.2014

Возможно, что-то вроде Slick сделает именно то, что вам нужно.

В качестве альтернативы, вот код, который я когда-то написал. Он предоставляет поток документов и метаинформации JSON и основан на библиотеке Lift JSON, но вы можете легко изменить ее на другие реализации JSON. Это работает очень хорошо.

case class ColumnMeta(index: Int, label: String, datatype: String)

def runQuery(dbConnection: Connection, query: String): (List[ColumnMeta], Stream[JObject]) = {
    val rs = dbConnection.prepareStatement(query).executeQuery
    implicit val cols = getColumnMeta(rs.getMetaData)
    (cols, getStreamOfResults(rs))
  }

  /**
   * Returns a list of columns for specified ResultSet which describes column properties we are interested in.
   */
  def getColumnMeta(rsMeta: ResultSetMetaData): List[ColumnMeta] =
    (for {
      idx <- (1 to rsMeta.getColumnCount)
      colName = rsMeta.getColumnLabel(idx).toLowerCase
      colType = rsMeta.getColumnClassName(idx)
    } yield ColumnMeta(idx, colName, colType)).toList

  /**
   * Creates a stream of results on top of a ResultSet.
   */
  def getStreamOfResults(rs: ResultSet)(implicit cols: List[ColumnMeta]): Stream[JObject] =
    new Iterator[JObject] {
      def hasNext = rs.next
      def next() = rowToObj(rs)
    }.toStream

  /**
   * Given a row from a ResultSet produces a JSON document.
   */
  def rowToObj(rs: ResultSet)(implicit cols: List[ColumnMeta]): JObject = {
    val fields = for {
      ColumnMeta(index, label, datatype) <- cols
      clazz = Class.forName(datatype)
      value = columnValueGetter(datatype, index, rs)
    } yield (label -> value)
    JObject(fields map { case (n, v) => JField(n, v) })
  }

  /**
   * Takes a fully qualified Java type as String and returns one of the subtypes of JValue by fetching a value
   * from result set and converting it to proper type.
   * It supports only the most common types and everything else that does not match this conversion is converted
   * to String automatically. If you see that you results should contain more specific type instead of String
   * add conversion cases to {{{resultsetGetters}}} map.
   */
  def columnValueGetter(datatype: String, columnIdx: Int, rs: ResultSet): JValue = {
    val obj = rs.getObject(columnIdx)
    if (obj == null)
      JNull
    else {
      val converter = resultsetGetters getOrElse (datatype, (obj: Object) => JString(obj.toString))
      converter(obj)
    }
  }

  val resultsetGetters: Map[String, Object => JValue] = Map(
    "java.lang.Integer" -> ((obj: Object) => JInt(obj.asInstanceOf[Int])),
    "java.lang.Long" -> ((obj: Object) => JInt(obj.asInstanceOf[Long])),
    "java.lang.Double" -> ((obj: Object) => JDouble(obj.asInstanceOf[Double])),
    "java.lang.Float" -> ((obj: Object) => JDouble(obj.asInstanceOf[Float])),
    "java.lang.Boolean" -> ((obj: Object) => JBool(obj.asInstanceOf[Boolean])),
    "java.sql.Clob" -> ((obj: Object) => {
      val clob = obj.asInstanceOf[Clob]
      JString(clob.getSubString(1, clob.length.toInt))
    }),
    "java.lang.String" -> ((obj: Object) => JString(obj.asInstanceOf[String])))
person yǝsʞǝla    schedule 07.02.2014
comment
К сожалению, я не могу использовать Slick, так как он не поддерживает хранимые процедуры. Спасибо за ваш ответ и за пример потока! - person Pavel Davydov; 07.02.2014