来自Scala中使用Play框架的迭代器的响应

时间:2015-03-05 20:07:16

标签: scala playframework chunked

我有一个来自数据库调用的大型结果集,我需要流回用户,因为它不能完全适合内存。

我可以通过设置选项

来回传数据库中的结果
val statement = session.conn.prepareStatement(query, 
                java.sql.ResultSet.TYPE_FORWARD_ONLY,
                java.sql.ResultSet.CONCUR_READ_ONLY)
statement.setFetchSize(Integer.MIN_VALUE)
....
....
val res = statement.executeQuery

然后使用Iterator

val result = new Iterator[MyResultClass] {
    def hasNext = res.next
    def next = MyResultClass(someValue = res.getString("someColumn"), anotherValue = res.getInt("anotherValue"))
}

在Scala中,Iterator扩展了TraversableOnce,它允许我根据https://www.playframework.com/documentation/2.3.x/ScalaStream

中的文档将Iterator传递给用于Play框架中Chunked Response的Enumerator类。

在查看Enumerator的源代码时,我发现它有一个重载的apply方法来使用TraversableOnce对象

我尝试使用以下代码

import play.api.libs.iteratee.Enumerator
val dataContent = Enumerator(result)
Ok.chunked(dataContent)

但是这不起作用,因为它抛出以下异常

Cannot write an instance of Iterator[MyResultClass] to HTTP response. Try to define a Writeable[Iterator[MyResultClass]]

我无法在文档中的任何地方找到有关Writable是什么或做什么的内容。我以为一旦Enumerator消耗了TraversableOnce对象,它会从那里拿走它但我猜不是吗?

1 个答案:

答案 0 :(得分:8)

您的方法存在问题

您的方法存在两个问题:

  1. 您正在将Iterator写入Enumerator / Iteratee。您应该写出Iterator而不是整个Iterator
  2. 的内容
  3. Scala不知道如何在HTTP流上表达MyResultClass的对象。在编写它们之前,尝试将它们转换为String表示(例如JSON)。
  4. 实施例

    build.sbt

    一个简单的Play Scala项目,支持H2和SQL。

    lazy val root = (project in file(".")).enablePlugins(PlayScala)
    
    scalaVersion := "2.11.6"
    
    libraryDependencies ++= Seq(
      jdbc,
      "org.scalikejdbc" %% "scalikejdbc"       % "2.2.4",
      "com.h2database"  %  "h2"                % "1.4.185",
      "ch.qos.logback"  %  "logback-classic"   % "1.1.2"
    )
    

    项目/ plugins.sbt

    只是当前稳定版本中sbt play插件的最小配置

    resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
    
    addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.3.8")
    

    CONF /路由

    /json

    上只有一条路线
    GET    /json                        controllers.Application.json
    

    Global.scala

    配置文件,在Play应用程序启动期间使用演示数据创建并填充数据库

    import play.api.Application
    import play.api.GlobalSettings
    import scalikejdbc._
    
    object Global extends GlobalSettings {
    
      override def onStart(app : Application): Unit = {
    
        // initialize JDBC driver & connection pool
        Class.forName("org.h2.Driver")
        ConnectionPool.singleton("jdbc:h2:mem:hello", "user", "pass")
    
        // ad-hoc session provider
        implicit val session = AutoSession
    
    
        // Create table
        sql"""
          CREATE TABLE persons (
            customer_id SERIAL NOT NULL PRIMARY KEY,
            first_name VARCHAR(64),
            sure_name VARCHAR(64)
          )""".execute.apply()
    
        // Fill table with demo data
        Seq(("Alice", "Anderson"), ("Bob", "Builder"), ("Chris", "Christoph")).
          foreach { case (firstName, sureName) =>
            sql"INSERT INTO persons (first_name, sure_name) VALUES (${firstName}, ${sureName})".update.apply()
        }
      }
    }
    

    模型/ Person.scala

    这里我们定义数据库模式和数据库对象的Scala表示。这里的关键是函数personWrites。它将Person对象转换为JSON表示(实际代码可以通过宏方便地生成)。

    package models
    
    import scalikejdbc._
    import scalikejdbc.WrappedResultSet
    import play.api.libs.json._
    
    case class Person(customerId : Long, firstName: Option[String], sureName : Option[String])
    
    object PersonsTable extends SQLSyntaxSupport[Person] {
      override val tableName : String = "persons"
      def apply(rs : WrappedResultSet) : Person =
        Person(rs.long("customer_id"), rs.stringOpt("first_name"), rs.stringOpt("sure_name"))
    }
    
    package object models {
      implicit val personWrites: Writes[Person] = Json.writes[Person]
    }
    

    控制器/ Application.scala

    这里有Iteratee / Enumerator代码。首先我们从数据库中读取数据,然后将结果转换为Iterator,然后转换为Enumerator。该枚举器没有用,因为它的内容是Person个对象,而Play并不知道如何通过HTTP编写这样的对象。但是在personWrites的帮助下,我们可以将这些对象转换为JSON。 Play知道如何通过HTTP编写JSON。

    package controllers
    
    import play.api.libs.json.JsValue
    import play.api.mvc._
    import play.api.libs.iteratee._
    import scala.concurrent.ExecutionContext.Implicits.global
    import scalikejdbc._
    
    import models._
    import models.personWrites
    
    object Application extends Controller {
    
      implicit val session = AutoSession
    
      val allPersons : Traversable[Person] = sql"SELECT * FROM persons".map(rs => PersonsTable(rs)).traversable().apply()
      def personIterator(): Iterator[Person] = allPersons.toIterator
      def personEnumerator() : Enumerator[Person] = Enumerator.enumerate(personIterator)
      def personJsonEnumerator() : Enumerator[JsValue] = personEnumerator.map(personWrites.writes(_))
    
      def json = Action {
        Ok.chunked(personJsonEnumerator())
      }
    }
    

    讨论

    数据库配置

    在此示例中,数据库配置是一个hack。通常我们会配置Play,以便它提供数据源并在后台处理所有数据库内容。

    JSON转换

    在代码中我直接调用JSON转换。有更好的方法,导致更紧凑的代码(但初学者更容易理解)。

    你得到的回答并不是真正有效的JSON。例如:

    {"customerId":1,"firstName":"Alice","sureName":"Anderson"}
    {"customerId":2,"firstName":"Bob","sureName":"Builder"}
    {"customerId":3,"firstName":"Chris","sureName":"Christoph"}
    

    (注:换行符仅用于格式化。在线上看起来像:

    ...son"}{"custom...
    

    相反,你会得到一块有效的JSON块。这就是你要求的。接收端可以自己使用每个块。但是有一个问题:你必须找到一些方法将响应分成有效的块。

    请求本身确实是分块的。请考虑以下HTTP标头(采用JSON HAR格式,从Google Chrome导出):

         "status": 200,
          "statusText": "OK",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "Transfer-Encoding",
              "value": "chunked"
            },
            {
              "name": "Content-Type",
              "value": "application/json; charset=utf-8"
            }
    

    代码组织

    我在控制器中放了一些SQL代码。在这种情况下,这是完全正常的。如果代码变大,那么模型中的SQL东西可能会更好,让控制器使用更通用的(在这种情况下:" monadic plus",即map,{{1 },filter)interface。

    在控制器中JSON代码和SQL代码混合在一起。当代码变大时,你应该组织它,例如每个技术或每个模型对象/业务领域。

    阻止迭代器

    迭代器的使用会导致阻塞行为。这通常是一个大问题,但应该避免应用程序必须加载很多(每秒数百或数千次点击)或必须非常快速地回答(想想交易算法在堆栈交换机上实时工作)。在这种情况下,您可以使用NoSQL数据库作为缓存(请不要将其用作唯一的数据存储)或非阻塞的JDBC(例如async postgres / mysql)。再说一遍:对于大型应用程序来说,这不是必需的。

    注意:只要转换为迭代器,请记住只能使用一次迭代器。对于每个请求,您需要一个新的迭代器。

    结论

    完整的WebApp,包括数据库访问完全在(不那么短)的SO答案中。我非常喜欢Play框架。

    此代码用于教育目的。在某些地方,这是非常尴尬的,以便更容易理解初学者的概念。在一个真实的应用程序中,你会理顺这些东西,因为你已经知道了这些概念,你只是想看看代码的目的(为什么它在那里?它使用哪些工具?什么时候它在做什么?)乍一看。

    玩得开心!