如何在Clojure中对关系型数据库进行数据建模?

21

我在Twitter和#clojure IRC频道上询问了这个问题,但未得到回应。

已经有几篇关于Clojure面向Ruby程序员、Clojure面向Lisp程序员的文章,但缺失的部分是面向ActiveRecord程序员的Clojure

已经有一些与MongoDB、Redis等交互的文章了- 但这些最终还是键值存储。然而,来自Rails背景的我们习惯于从继承的角度思考数据库 - has_many、polymorphic、belongs_to等。

关于Clojure/Compojure + MySQL的几篇文章(ffclassic)- 直接涉及SQL。当然,可能ORM会导致阻抗不匹配,但事实仍然是,在像ActiveRecord那样思考之后,很难再用其他方式思考。

我相信,由于它们本质上是集合,关系型数据库非常适合面向对象的范例,例如activerecord非常适合对这些数据进行建模。例如,一个博客 - 简单地说

class Post < ActiveRecord::Base
  has_many :comments
 end


 class Comment < ActiveRecord::Base
   belongs_to :post
 end

在Clojure中如何对此进行建模 - 它是如此严格地反面向对象?也许如果问题涉及所有函数式编程语言,那么问题可能会更好,但我更感兴趣的是从Clojure的角度来看(以及Clojure的例子)


一个相关的问题:https://dev59.com/sHE85IYBdhLWcg3wgDzd -- 我已经提供了一个答案,也许可以作为这个问题的答案。 - Michał Marczyk
谢谢Michal - 我特别喜欢你用哈希表来描述表建模的方式。我想知道你是否可以详细说明如何处理外键、单表继承、连接等问题。 - Sandeep
3个回答

19

现在有几个类似ORM的库正在开发中。

在邮件列表上,一些(聪明的)人最近描述了一些其他模型如何工作。每个库都采用了不同的方法来解决问题,所以一定要查看它们所有的内容。

这里有一个扩展示例,例如使用Oyako。 这个库还没有准备好投入生产,并且仍在积极开发中,因此示例可能在一周内无效,但它正在逐步完善。 无论如何,这只是一个概念验证。 给它一些时间,某人将会推出一个好的库。

请注意,clojure.contrib.sql已经让您可以通过JDBC从数据库获取记录,并以不可变的哈希映射表示记录。由于数据最终以普通映射的形式出现,所有适用于映射的Clojure核心函数都可以适用于这些数据。
ActiveRecord还能提供什么?我能想到几件事情。

简洁的SQL查询DSL

我的理解是:首先定义表之间的关系。这不需要突变或对象。这是一个静态描述。AR将这些信息分散在一堆类中,但我将其视为单独的(静态)实体。
使用定义好的关系,您可以非常简洁地编写查询。例如,使用Oyako:
(def my-data (make-datamap db [:foo [has-one :bar]]
                              [:bar [belongs-to :foo]]))

(with-datamap my-data (fetch-all :foo includes :bar))

然后你将会有一些foo对象,每个对象都有一个列出栏的:bar键。
在Oyako中,“数据映射”只是一个映射。查询本身是一个映射。返回的数据是映射向量(映射向量的向量)。因此,您最终将拥有一种标准、简单的方法来构建、操作和迭代所有这些内容,这非常方便。添加一些语法糖(宏和普通函数),让您更轻松地简洁地创建和操作这些映射,就可以获得相当强大的功能。这只是其中一种方法,有很多种方法。
如果您看一下Sequel等库的另一个例子,您将会看到以下内容:
Artist.order(:name).last

但是为什么这些函数必须是存在于对象内部的方法呢?在Oyako中,等效的写法可能是:

(last (-> (query :artist) 
          (order :name)))

保存/更新/删除记录

为什么需要面向对象的风格、变异或实现继承呢?首先获取记录(作为不可变映射),然后将其通过一系列函数进行处理,根据需要assoc新值,最后通过调用函数将其放回数据库或删除它。

聪明的库可以利用元数据来跟踪哪些字段已被更改,以减少执行更新所需的查询量。 或者标记记录,使DB函数知道将其放回哪个表中。 我认为Carte甚至可以进行级联更新(在更改父记录时更新子记录)。

验证、钩子

我认为这些大部分应该属于数据库而不是ORM库。例如,级联删除(删除父记录时删除子记录):AR有一种方法可以做到这一点,但您可以将一个子句添加到DB中的表中,然后让您的DB处理它,从而永远不必担心。许多类型的约束和验证也是如此。

但是,如果您需要钩子,可以使用普通的函数或多态轻松地实现它们。在过去的某个时刻,我有一个数据库库,在CRUD周期的不同时间调用钩子,例如after-savebefore-delete。它们是在表名上分派的简单多方法。这使您可以根据需要扩展它们到自己的表中。

(defmulti before-delete (fn [x] (table-for x)))
(defmethod before-delete :default [& _]) ;; do nothing
(defn delete [x] (when (before-delete x) (db-delete! x) (after-delete x)))

作为最终用户,我之后可以编写:

(defmethod before-delete ::my_table [x] 
  (if (= (:id x) 1)
    (throw (Exception. "OH NO! ABORT!"))
    x))

简单易扩展,只需花费几秒钟即可编写。看不到任何面向对象的东西。也许没有AR那么复杂,但有时候简单就足够了。

查看此库,以获取另一个定义钩子的示例。

迁移

Carte有这些功能。我还没有仔细思考过它们,但是对于Clojure来说,将数据库版本化并将数据导入其中似乎并不超出可能性的范围。

优雅

AR的好处在于命名表和列的所有约定,以及大量便捷函数用于大写单词,格式化日期等等。这与面向对象或非面向对象无关; AR之所以如此优雅,是因为已经投入了很多时间。也许Clojure尚未拥有像AR类库一样用于处理数据库数据的库,但请给它点时间。

所以...

与其拥有一个知道如何销毁自身、突变自身、保存自身、将自身与其他数据关联、提取自身等对象,不如拥有只是数据的数据,并定义适用于该数据的函数:保存它,销毁它,在数据库中更新它,提取它,将其与其他数据关联。这就是Clojure通常在数据上操作的方式,来自数据库的数据也不例外。

Foo.find(1).update_attributes(:bar => "quux").save!

=> (with-db (-> (fetch-one :foo :where {:id 1})
                (assoc :bar "quux")
                (save!)))

Foo.create!(:id => 1)

=> (with-db (save (in-table :foo {:id 1})))

类似这样的东西。它与对象工作方式相反,但提供了相同的功能。但在Clojure中,您还可以获得以FP方式编写代码的所有好处。


太好了!这基本上就是我在寻找的东西 - 不知道您是否能为oyako添加示例。问题在于我们(在这种情况下是我)太习惯于思考对象 - 大多数框架都鼓励我们这样做。不可变性的概念与诸如MVC之类的概念相比,很难理解 - 在命令式MVC和函数式MVC之间存在哪些概念上的差异。这就是为什么我找不到任何关于使用关系型数据库编写Web应用程序的Clojure / Compojure示例(除了您自己编写的一个示例),这与Scala(最终是面向对象的)不同。 - Sandeep

5

这个问题已经有一段时间没有得到回答了,但是Korma http://sqlkorma.com 是另一个项目,它也有助于减少SQL和Clojure之间的不协调。近期它一直在进行维护,并且应该能够适用于更新的Clojure版本。


0
一个基于clojure.contrib.sql的新微型SQL DSL已经开始了。它在Github上,只有轻量级的76行代码。
作者的博客中有很多DSL的例子和理论,可以在他的Github个人资料中找到链接(新用户SO超链接限制)。我认为它的设计使得SQL可以更加符合Clojure的习惯用法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接