如何将jdbc结果转换为不可变集合

时间:2014-02-07 11:14:55

标签: scala immutability spray-json

我有一个用scala编写的小应用程序,它向mysql发送请求,接收结果,然后将其转换为json并发送到某个http服务器。我使用java jdbc和mysql连接器连接到数据库,并使用spray-json进行scala集合到json转换。因此,我创建了与db的连接,执行查询,然后使用getResultSet()获得结果。然后我遍历它,并将结果复制到一个可变的地图:

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

这个工作正常,但后来我必须将结果转换为不可变映射,导致spray-json无法将可变集合转换为json,AFAIK。有没有一种很好的方法将jdbc结果转换为不可变集合而不将其复制到临时可变映射?也许有可能以某种方式使用流?我问,因为它看起来似乎必须有一些很酷的功能模式,我不知道。

P.S。顺便说一句,我不能只使用Slick,因为它不支持存储过程,AFAIK。

2 个答案:

答案 0 :(得分:4)

类似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])))

答案 1 :(得分:3)

简短回答:你做得比你所做的要好得多。在Scala的引擎盖下,功能聪明的代码看起来很像你的代码。另外,不要忘记mutable Map有一个toMap方法,它返回一个不可变的Map

长答案:您正在寻求使用Scala代码创建JDBC代码接口。 JDBC的API并非设计用于函数式语言,因此您肯定需要一些可变/命令式代码来帮助缩小差距。这实际上只是阻力最小的问题。

如果您只是制作一对一地图,那么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特性,但它并不适合您的用例。它是可变的,并且将值存储在可变Set s而不是List中,因此我们暂时忽略它。

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 (see code on GitHub)的源代码,它实际上与您的内容完全相同 - 使用我们的数据构建可变Map,然后将其转换为不可变Map 1}}在最后。

我认为您已经获得的方法接近最优 - 只需返回map.toMap

哦,我假设SomeExtractor提取了一对Int