Фрагментированный ответ от итератора с Play Framework в Scala

У меня есть большой набор результатов из вызова базы данных, который мне нужно передать обратно пользователю, поскольку он не может уместиться в памяти.

Я могу передать результаты из базы данных обратно, установив параметры

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, что должно позволить мне передать Iterator классу Enumerator, который используется для фрагментированного ответа в игровой среде в соответствии с документацией по адресу https://www.playframework.com/documentation./2.3.x/ScalaStream

При просмотре исходного кода Enumerator я обнаружил, что он имеет перегруженный метод применения для использования объекта 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, он возьмет его оттуда, но я думаю, что нет ??


person Adam Ritter    schedule 05.03.2015    source источник
comment
Скорее всего, проблема в вашем пользовательском MyResultClass, вам нужен экземпляр Writable[MyResultClass], чтобы сообщить Play, как представлять ваш объект в виде потока байтов. Вам нужен кодек для MyResultClass в основном.   -  person vptheron    schedule 05.03.2015
comment
Вот о чем я думал, но я не могу найти документацию о том, как это сделать :(   -  person Adam Ritter    schedule 06.03.2015


Ответы (1)


Проблема в вашем подходе

В вашем подходе есть две проблемы:

  1. Вы записываете Iterator в Enumerator / Iteratee. Вы должны написать содержимое Iterator, а не весь Iterator
  2. Scala не знает, как выразить объекты MyResultClass в потоке HTTP. Попробуйте преобразовать их в представление String (например, JSON) перед их записью.

Пример

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")

конф/маршруты

Всего один маршрут на /json.

GET    /json                        controllers.Application.json

Глобал.скала

Конфигурационный файл, создает и наполняет базу демо-данными при запуске приложения 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. Сначала мы читаем данные из базы данных, затем преобразуем результат в итератор, а затем в перечислитель. Этот Enumerator был бы бесполезен, потому что его содержимое — это Person объектов, а Play не знает, как записывать такие объекты по HTTP. Но с помощью personWrites мы можем конвертировать эти объекты в JSON. И Play умеет писать JSON по HTTP.

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

Обсуждение

Конфигурация базы данных

В этом примере конфигурация базы данных является хаком. Обычно мы настраиваем 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, filter, flatMap) интерфейс.

В контроллере код JSON и код SQL смешиваются вместе. Когда код становится больше, вы должны организовать его, например. на технологию или на объект модели/домен бизнеса.

Блокирующий итератор

Использование итератора приводит к блокировке. Обычно это большая проблема, но ее следует избегать для приложений, которые должны иметь большую нагрузку (сотни или тысячи обращений в секунду) или которые должны отвечать очень быстро (подумайте о торговых алгоритмах, работающих в режиме реального времени на бирже стека). В этом случае вы можете использовать базу данных NoSQL в качестве кеша (пожалуйста, не используйте ее как единственное хранилище данных) или неблокирующий JDBC (например, асинхронный postgres/mysql). Опять же: это не обязательно для больших приложений.

Внимание. При преобразовании в итератор помните, что использовать итератор можно только один раз. Для каждого запроса вам нужен новый итератор.

Заключение

Полное веб-приложение, включая доступ к базе данных полностью в (не таком коротком) ответе SO. Мне очень нравится фреймворк Play.

Этот код предназначен для образовательных целей. В некоторых местах это очень неудобно, чтобы облегчить понимание концепций для новичка. В реальном приложении вы бы исправили эти вещи, потому что вы уже знаете концепции и просто хотите увидеть назначение кода (почему он здесь? какие инструменты он использует? когда он что делает?) на Первый взгляд.

Повеселись!

person stefan.schwetschke    schedule 10.03.2015
comment
Что именно вы имеете в виду, когда говорите записать содержимое Iterator в Enumerator/Iteratee? Вы имеете в виду фактические значения, полученные из базы данных? Если это так, это будет означать, что мне сначала нужно загрузить все данные в память, что я не могу сделать, так как они слишком велики, чтобы поместиться в память. - person Adam Ritter; 11.03.2015
comment
@AdamRitter Вы создали объект типа Iterator[MyResultClass]. Play пытается отправить этот объект по сети. Что вы хотите, так это отправить объекты типа MyResultClass, к которым можно получить доступ через этот итератор. Но для этого нужно вызвать Enumerator.enumerate(...), возможно с помощью scala.collections.JavaConversions. - person stefan.schwetschke; 11.03.2015