Как один _model_ данных из реляционных баз данных в clojure?

Я задал этот вопрос на твиттере, а также канал # clojure IRC, но не получил ответов.

Было опубликовано несколько статей о программистах Clojure -for-Ruby, Clojure -for- lisp -programmers.. но какая недостающая часть Clojure для программистов ActiveRecord.

Были статьи о взаимодействии с 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 - который является настолько строго анти-OO? Возможно, вопрос был бы лучше, если бы он касался всех языков функционального программирования, но меня больше интересует точка зрения Clojure (и Clojure)

Ответ 1

В настоящее время существует несколько ORM-подобных библиотек.

  • clj-record
  • Carte
  • Oyako (Отказ от ответственности, я написал это.)
  • ClojureQL - это скорее генерация SQL-генерации из того, что я могу видеть, но это заслуживает упоминания.

В списке рассылки некоторые (умные) люди недавно описали некоторые другие модели, как это может работать. Каждая из этих библиотек принимает совершенно иной подход к проблеме, поэтому обязательно взгляните на них.

Вот пример расширенный пример с использованием Oyako. Эта библиотека не готова к производству и все еще находится в тяжелом развитии, поэтому пример может быть недействительным через неделю, но он попадает туда. Это доказательство концепции в любом случае. Дайте ему некоторое время, и кто-то придумает хорошую библиотеку.

Обратите внимание, что clojure.contrib.sql уже позволяет извлекать записи из БД (через JDBC) и заканчивать неизменяемыми хэш-картами, представляющими записи. Поскольку данные заканчиваются на нормальных картах, все функции myriad 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)))

Сохранить/обновить/удалить записи

Опять же, зачем вам нужны объекты стиля OO или мутация или наследование реализации? Сначала извлеките запись (в качестве неизменяемой карты), затем проведите ее через кучу функций, assoc добавив к ней новые значения по мере необходимости, затем загрузите ее обратно в базу данных или удалите ее, вызвав на ней функцию.

Умная библиотека могла бы использовать метаданные, чтобы отслеживать, какие поля были изменены, чтобы уменьшить количество запросов, необходимых для обновления. Или для отметки записи, чтобы функции БД знали, в какую таблицу следует вставлять ее обратно. Я думаю, что Carte даже делает каскадные обновления (обновление подзаписей при изменении родительской записи).

Проверки, перехватчики

Большая часть этого я вижу как принадлежность к базе данных, а не в библиотеке ORM. Например, каскадное удаление (удаление дочерних записей при удалении родительских записей): у AR есть способ сделать это, но вы можете просто добавить предложение в таблицу в БД, а затем позволить вашей БД обрабатывать его и никогда больше не волноваться. То же самое со многими типами ограничений и валидаций.

Но если вы хотите использовать крючки, они могут быть реализованы очень легким способом, используя простые старые функции или мультиметоды. В какой-то момент в прошлом у меня была библиотека базы данных, которая вызывала крючки в разное время цикла CRUD, например after-save или before-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))

Легко и расширяемо, и потребовалось пару секунд, чтобы написать. Нет OO в поле зрения. Не так сложно, как AR, но иногда просто достаточно хорошо.

Посмотрите эту библиотеку для другого примера определения перехватчиков.

Миграция

У этого есть. Я не очень много думал о них, но управление версиями базы данных и вклеивание данных в нее не выходит за рамки возможностей для Clojure.

Польский

Много преимуществ AR происходит из всех соглашений об именах таблиц и названии столбцов, а также всех удобных функций для заглавных букв и дат форматирования. Это не имеет ничего общего с OO против non-OO; У 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.

Ответ 2

На этот вопрос не было задано какое-то время, но Korma http://sqlkorma.com - еще один проект, который также помогает уменьшить диссонанс между SQL и Clojure. Он работал совсем недавно и должен работать с более поздними версиями Clojure.

Ответ 3

Запущен новый микрозадачный DSL, построенный на clojure.contrib.sql. Он находится на Github, взвешивая на легком 76 loc.

Там много примеров и теории за DSL в блоге автора, связанного с его профилем Github (новый гиперссылка SO для пользователей). Его дизайн позволяет SQL быть абстрагированным в гораздо более идиоматический Clojure, я чувствую.