Проблема в вашем подходе
В вашем подходе есть две проблемы:
- Вы записываете
Iterator
в Enumerator
/ Iteratee
. Вы должны написать содержимое Iterator
, а не весь Iterator
- 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
MyResultClass
, вам нужен экземплярWritable[MyResultClass]
, чтобы сообщить Play, как представлять ваш объект в виде потока байтов. Вам нужен кодек дляMyResultClass
в основном. - person vptheron   schedule 05.03.2015