如何将next.jdbc中的IReduceInit改编为使用cheshire将JSON流传输到使用ring的HTTP响应

时间:2019-09-23 19:22:22

标签: clojure ring transducer cheshire

tl; dr如何将IReduceInit转换为转换值的惰性序列

我有一个数据库查询,该查询产生了一个相当大的数据集,可以实时在客户端上进行数据透视(百万行或两行,25个属性-对于现代笔记本电脑来说没问题)。

我的(简化的)堆栈是调用clojure.jdbc来获得(我认为是懒惰的)结果行序列。我可以通过将它作为实体通过ring-json中间件传递出去来序列化它。 ring-json在堆上构建响应字符串时出现问题,但是从0.5.0开始,可以选择将响应流输出。

通过分析几个失败案例,我发现clojure.jdbc实际上在将整个结果集移交给内存之前将其实现。没问题!我决定不使用该库中的reducible-query,而是决定移至新的next.jdbc。

next.jdbc中的关键操作是plan,它返回一个IReduceInit,我可以使用它来运行查询并获取结果集...

(into [] (map :cc_id) (jdbc/plan ds ["select cc_id from organisation where cc_id = '675192'"]))
["675192"]

但是,这实现了整个结果集,并且在上述情况下,我可以将所有id都预先存储在内存中。对于一个人来说,这不是一个问题,但是我通常有很多。

计划IReduceInit是一个我可以给出一个初始值就可以减少的东西,因此我可以在归约函数中进行输出...(thx @amalloy)

(reduce #(println (:cc_id %2)) [] (jdbc/plan ds ["select cc_id from organisation where cc_id = '675192'"]))
675192
nil

...但是理想情况下,我想将IReduceInit应用于值后,将其转换为惰性值序列,因此我可以将它们与ring-json和cheshire一起使用。我没有发现任何明显的方法。

4 个答案:

答案 0 :(得分:2)

reduce与IReduceInit可以正常工作。 IReduceInit需要一个初始值,该值是您在调用.reduce时指定的,但在使用reduce函数时则不需要;这就解释了为什么您看到其中一个有效而看不到另一个。

但是,这不会使您懒惰。 reduce的合同的一部分是,它急切地消耗了整个输入(我们将忽略reduced,它不会改变任何有意义的内容)。您的问题是动态范围的更一般性问题的一个特定情况:JDBC产生的序列在某些上下文中仅是“有效”的,并且您需要在此上下文中进行所有处理,因此它不能偷懒。取而代之的是,您通常将程序内翻:不要将返回值用作序列,而应向查询引擎传递一个函数,然后说:“请在结果中调用此函数”。然后,引擎在调用该函数时确保数据有效,并且一旦函数返回,它将清除数据。我不了解jdbc.next,但是对于较旧的jdbc,您将使用db-query-with-resultset之类的东西。您将向其传递一些函数,该函数可以将字节添加到待处理的HTTP响应中,并且它将多次调用该函数。

这有点含糊不清,因为我不知道您使用的是哪个HTTP处理程序,或者它的功能是如何懒惰地处理流式响应,但这是您必须遵循的总体思路想要处理动态范围的资源:懒惰不是一种选择。

答案 1 :(得分:1)

令人沮丧。

为什么您不能使用JDBC?没有任何Clojure图层?

(let [resultset (.executeQuery connection "select ...")]
  (loop 
   (when (.next resultset)
     (let [row [(.getString resultset 1)
                (.getString resultset 2)
                ...]])
     (json/send row)
     (recur)))
  (json/end))

当然,有了ResultSetMetaData,您可以将行的生成自动化为一个函数,该函数可以处理返回的任何内容。

答案 2 :(得分:1)

IReduceInit允许在reduce函数退出时结束JDBC资源。 这比LazySeq方法更具可预测性,后者可能永远不会释放JDBC资源。

您使用BlockingQueue和将来的任务来像这样填充该队列

 (defn lazywalk-reducible
  "walks the reducible in chunks of size n,
  returns an iterable that permits access"
  [n reducible]
  (reify java.lang.Iterable
    (iterator [_]
      (let [bq (java.util.concurrent.ArrayBlockingQueue. n)
            finished? (volatile! false)
            traverser (future (reduce (fn [_ v] (.put bq v)) nil reducible)
                              (vreset! finished? true))]
        (reify java.util.Iterator
          (hasNext [_] (or (false? @finished?) (false? (.isEmpty bq))))
          (next [_] (.take bq)))))))

如果产生了迭代器但没有遵循其结论,那么这当然会造成泄漏。

我还没有对它进行彻底的测试,它可能还会有其他问题。但是这种方法应该行得通。

如果Java Iterable对您的用例而言不够好,您可以选择使其clojure.lang.ISeq成为具体版本。但是随后您开始涉及HeadRetention问题;以及如何处理对Object first()的调用是可行的,但我不想对此太过思索

答案 3 :(得分:1)

我的lazy-seq并不是一个好主意,有很多原因-即使我保证不抱头,结果流中出现的特殊问题无疑也会使ResultSet停滞不前-序列化会发生在可以清除的调用堆栈。

对懒惰的需求是由不希望在内存中实现整个结果的欲望所驱动,对seq或其他col的需求?以便中间件将其序列化...

因此,直接使IReduceInit JSONable,然后绕过中间件。如果序列化过程中发生异常,则控件将通过next.jdbc的IReduceInit进行传递,然后可以进行有意义的清除。

;; reuse this body generator from my patch to ring.middleware.json directly, as the coll? check will fail
(defrecord JsonStreamingResponseBody [body options]
  ring-protocols/StreamableResponseBody
  (write-body-to-stream [_ _ output-stream]
    (json/generate-stream body (io/writer output-stream) options)))
 
;; the year long yak is shaved in 8 lines by providing a custom serialiser for IReduceInits…
(extend-type IReduceInit
  cheshire.generate/JSONable
  (to-json [^IReduceInit results ^JsonGenerator jg]
    (.writeStartArray jg)
    (let [rf (fn [_ ^IPersistentMap m]
               (cheshire.generate/encode-map m jg))]
      (reduce rf nil results))
    (.writeEndArray jg)))

;; at this point I can wrap the result from next.jdbc/plan with ->JsonStreamingResponseBody into the :body of the ring response and it will stream

组合这些功能仍然需要大量工作,适配器代码总是让我担心我缺少一种简单的惯用方法。