1

我有一个用 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 结果转换为不可变集合而不将其处理为临时可变映射?也许有可能以某种方式使用流?我在问,因为看起来必须有一些很酷的功能模式来做到这一点,我不知道。

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

4

2 回答 2

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])))
于 2014-02-07T14:24:58.287 回答
3

简短的回答:你不能比你所拥有的做得更好。在 Scala 的功能聪明的背后是看起来很像你的代码。另外,不要忘记 mutableMap有一个toMap返回 immutable 的方法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 - 从键到多个值的映射。ScalaMultiMap在其标准库中确实有一个特点,但它并不适合您的用例。它是可变的,并且将值存储在 mutable Sets 中而不是Lists 中,所以我们现在将忽略它。

Scala 的标准库groupBy在它的 trait 上确实有一个方法,它Traversable或多或少地完成了你正在寻找的事情。我们有 aResultSet而不是 a Traversable,但原则上我们可以编写一些胶水代码来包装ResultSeta 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在最后将其转换为不可变的。

我认为你已经得到的方法接近最优 - 只是 return map.toMap

哦,我假设 SomeExtractor 提取了一对Ints。

于 2014-02-07T14:06:31.240 回答