Advertisement
Shinmera

Radiance MongoDB Interface

Jul 4th, 2013
189
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lisp 11.50 KB | None | 0 0
  1. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MONGO.LISP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  2.  
  3. #|
  4.   This file is a part of TyNETv5/Radiance
  5.   (c) 2013 TymoonNET/NexT http://tymoon.eu (shinmera@tymoon.eu)
  6.   Author: Nicolas Hafner <shinmera@tymoon.eu>
  7. |#
  8.  
  9. (defpackage radiance-mod-mongo
  10.   (:use :cl :radiance :cl-mongo)
  11.   (:export :mongodb
  12.            :mongo-data-model))
  13.  
  14. (in-package :radiance-mod-mongo)
  15.  
  16. (defmodule mongodb (database)
  17.   "Database implementation for MongoDB"
  18.   (:name "MongoDB Binding" :author "Nicolas Hafner" :version "0.0.1" :license "Artistic" :url "http://tymoon.eu")
  19.   (dbinstance :initform NIL :initarg dbinstance :accessor dbinstance))
  20.  
  21. (implement 'database (get-module 'mongodb))
  22.  
  23. (defmethod db-connect ((db mongodb) dbname &key (host (config-tree :database :host))
  24.                                              (port (config-tree :database :port))
  25.                                              (user (config-tree :database :user))
  26.                                              (pass (config-tree :database :pass)))
  27.   "Connects to the mongodb."
  28.   (if (not host) (setf host *mongo-default-host*))
  29.   (if (not port) (setf port *mongo-default-port*))
  30.   (log:info "Connecting to mongoDB on ~a:~a" host port)
  31.   (let ((mongo (mongo :db dbname :port port :host host)))
  32.     (setf (dbinstance db) mongo)
  33.     (when (and user pass)
  34.       (log:info "Authenticating with ~a/~a" user pass)
  35.       (db.auth user pass))
  36.     (log:info "Switching database to ~a" dbname)
  37.     (db.use dbname)))
  38.  
  39. (defmethod db-disconnect ((db mongodb) &key)
  40.   "Disconnects from mongodb."
  41.   (mongo-close :default)
  42.   (setf (dbinstance db) NIL))
  43.  
  44. (defmethod db-connected-p ((db mongodb) &key)
  45.   "Returns T if a connection exists, NIL otherwise."
  46.   (if (dbinstance db) T NIL))
  47.  
  48. (defmethod db-collections ((db mongodb) &key)
  49.   "Returns a list of all collection names available in the database."
  50.   (db.collections))
  51.  
  52. (defmethod db-create ((db mongodb) (collection string) &key indices)
  53.   "Creates a new collection on the database. Optionally a list of indexed fields can be supplied."
  54.   (db.create-collection collection)
  55.   (loop for index in indices
  56.      do (destructuring-bind (keys &key drop-duplicates unique) index
  57.           (db.ensure-index collection keys :drop-duplicates drop-duplicates :unique unique))))
  58.  
  59. (defmethod db-select ((db mongodb) (collection string) query &key (skip 0) (limit 0) sort)
  60.   "Select data from the collection. Using the iterate function is generally faster."
  61.   (db-iterate db collection query #'document->alist :skip skip :limit limit :sort sort))
  62.  
  63. (defmethod db-iterate ((db mongodb) (collection string) query function &key (skip 0) (limit 0) sort)
  64.   "Iterate over a set of data. The collected return values are returned."
  65.   (if sort (setf query (kv (kv "query" query) (kv "orderby" (alist->document sort)))))
  66.   (let ((result (db.find collection query :limit limit :skip skip)))
  67.     (multiple-value-bind (iterator collection docs) (cl-mongo::db.iterator result)
  68.       (loop
  69.          for next = '(NIL (0 1)) then (db.next collection iter)
  70.          for iter = iterator then (nth-value 0 (cl-mongo::db.iterator next))
  71.          for idocs = docs then (append idocs (second next))
  72.          until (zerop (length (second next)))
  73.          finally (setf docs idocs))
  74.       (loop for doc in docs collect (funcall function doc)))))
  75.  
  76. (defmethod db-insert ((db mongodb) (collection string) data &key)
  77.   "Insert data into the collection using the rows/fields provided in data."
  78.   (db.insert collection (alist->document data)))
  79.  
  80. (defmethod db-remove ((db mongodb) (collection string) query &key (skip 0) (limit 0))
  81.   "Remove data from the collection that matches the query. Note that if skip or limit are supplied, the delete operation will be pretty slow due to having to use a select and a remove for each match."
  82.   (if (= 0 skip limit)
  83.       (db.delete collection query)
  84.       (cl-mongo:rm collection (iter (db.find collection query :limit limit :skip skip)))))
  85.  
  86. ;@todo for some reason it won't accept the data document????
  87. (defmethod db-update ((db mongodb) (collection string) query data &key (skip 0) (limit 0) insert-inexistent)
  88.   "Update all rows that match the query with the new data. Note that if skip or limit are supplied, the update operation will be pretty slow due to having to use a select and an update for each match."
  89.   (if (= 0 skip limit)
  90.       (db.update collection query (alist->document data) :multi T :upsert insert-inexistent)
  91.       (let ((docs (docs (db.find collection query :limit limit :skip skip))))
  92.         (if (= 0 (length docs))
  93.             (db.insert collection data)
  94.             (loop for doc in docs do (db.update collection doc data))))))
  95.  
  96. (defmethod db-apropos ((db mongodb) (collection string) &key)
  97.   "Always returns NIL as any field or type is allowed in MongoDB."
  98.   NIL)
  99.  
  100. (defun document->alist (document)
  101.   "Turns a document into an alist."
  102.   (%document->alist document))
  103.  
  104. (defgeneric %document->alist (value))
  105. (defmethod %document->alist (value) value)
  106. (defmethod %document->alist ((value cl-mongo:document))
  107.   (loop with alist = ()
  108.      with map = (cl-mongo::elements value)
  109.      for key being the hash-keys of map
  110.      for val being the hash-values of map
  111.      do (setf alist (acons key (%document->alist val) alist))
  112.      finally (return alist)))
  113.  
  114. (defun alist->document (alist)
  115.   "Turns an alist into a document."
  116.   (loop with doc = (make-document)
  117.      for (key . val) in alist
  118.      do (add-element key (%alist->document val) doc)
  119.      finally (return doc)))
  120.  
  121. (defgeneric %alist->document (value))
  122. (defmethod %alist->document (value) value)
  123. (defmethod %alist->document ((value cons))
  124.   (if (listp (cdr value))
  125.       (loop for val in value collect (%alist->document val))
  126.       (add-element (car value) (%alist->document (cdr value)) (make-document))))
  127.  
  128. (defmacro query (&rest funcs)
  129.   "Construct a query parameter. See the spec for more information on how to use it."
  130.   (case (length funcs)
  131.     (0 'all)
  132.     (1 (%query-part (car (first funcs)) (cdr (first funcs))))
  133.     (otherwise (kv "$and" (loop for func in funcs collect (%query-part (car func) (cdr func)))))))
  134.  
  135. (defgeneric %query-part (func args))
  136.  
  137. (defmethod %query-part ((func symbol) args)
  138.   (error "Function ~a unknown." func))
  139.  
  140. (defmethod %query-part ((func (eql 'or)) args)
  141.   (kv "$or" (loop for func in args collect (%query-part (car func) (cdr func)))))
  142.  
  143. (defmethod %query-part ((func (eql 'and)) args)
  144.   (kv "$and" (loop for func in args collect (%query-part (car func) (cdr func)))))
  145.  
  146. (defmethod %query-part ((func (eql 'not)) args)
  147.   ($not (%query-part (car args) (cdr args))))
  148.  
  149. (defmethod %query-part ((func (eql '=)) args)
  150.   (kv (first args) (second args)))
  151.  
  152. (defmethod %query-part ((func (eql '!=)) args)
  153.   ($!= (first args) (second args)))
  154.  
  155. (defmethod %query-part ((func (eql '>)) args)
  156.   ($> (first args) (second args)))
  157.  
  158. (defmethod %query-part ((func (eql '<)) args)
  159.   ($< (first args) (second args)))
  160.  
  161. (defmethod %query-part ((func (eql '>=)) args)
  162.   ($>= (first args) (second args)))
  163.  
  164. (defmethod %query-part ((func (eql '<=)) args)
  165.   ($<= (first args) (second args)))
  166.  
  167. (defmethod %query-part ((func (eql 'in)) args)
  168.   ($in (first args) (second args)))
  169.  
  170. (defmethod %query-part ((func (eql '!in)) args)
  171.   ($!in (first args) (second args)))
  172.  
  173. (defmethod %query-part ((func (eql 'matches)) args)
  174.   ($/ (first args) (second args)))
  175.  
  176. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  177.  
  178. ;;;;;;;;;;;;;;;;;;;;;;;;;;DATA-MODEL.LISP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  179.  
  180. #|
  181.   This file is a part of TyNETv5/Radiance
  182.   (c) 2013 TymoonNET/NexT http://tymoon.eu (shinmera@tymoon.eu)
  183.   Author: Nicolas Hafner <shinmera@tymoon.eu>
  184. |#
  185.  
  186. (in-package :radiance-mod-mongo)
  187.  
  188. (defclass mongo-data-model (data-model)
  189.   ((document :initarg :document :initform (make-document :oid T) :accessor document)
  190.    (collection :initarg :collection :initform (error "Collection required") :accessor collection))
  191.   (:documentation "Datamodel for mongodb."))
  192.  
  193. (defmethod print-object ((model mongo-data-model) out)
  194.   (print-unreadable-object (model out :type T)
  195.     (if (collection model)
  196.         (format out "~a" (collection model))
  197.         (format out "STUB"))))
  198.  
  199. (implement 'data-model (make-instance 'mongo-data-model :collection NIL :document NIL))
  200.  
  201. (defmethod model-field ((model mongo-data-model) (field string) &key)
  202.   "Get the value of a field in the document."
  203.   (gethash field (cl-mongo::elements (document model))))
  204.  
  205. (defun model-field-set (model field value)
  206.   "Set the value of a field in the document."
  207.   (setf (gethash field (cl-mongo::elements (document model))) value))
  208.  
  209. (defmethod model-get ((model mongo-data-model) (collection string) query &key (skip 0) (limit 0) sort)
  210.   "Get a model for each document in the query result."
  211.   (db-iterate
  212.    (implementation 'database) collection query
  213.    (lambda (doc) (make-instance 'mongo-data-model :collection collection :document doc))
  214.    :sort sort :limit limit :skip skip))
  215.  
  216. (defmethod model-get-one ((model mongo-data-model) (collection string) query &key (skip 0) sort)
  217.   "Get a model of the first result in the query."
  218.   (if sort (setf query (kv (kv "query" query) (kv "orderby" (alist->document sort)))))
  219.   (make-instance
  220.    'mongo-data-model :collection collection
  221.    :document (first (docs (iter (db.find collection query :skip skip))))))
  222.  
  223. (defmethod model-hull ((model mongo-data-model) (collection string) &key)
  224.   "Create an empty model."
  225.   (make-instance 'mongo-data-model :collection collection))
  226.  
  227. (defmethod model-save ((model mongo-data-model) &key)
  228.   "Save an existing model."
  229.   (assert (not (eq (doc-id (document model)) T)) () "Model has not been inserted before.")
  230.   (db.save (collection model) (document model)))
  231.  
  232. (defmethod model-delete ((model mongo-data-model) &key)
  233.   "Delete an existing model."
  234.   (assert (not (eq (doc-id (document model)) T)) () "Model has not been inserted before.")
  235.   (db.delete (collection model) (kv "_id" (doc-id (document model)))))
  236.  
  237. (defmethod model-insert ((model mongo-data-model) &key (clone T))
  238.   "Insert the given model as a new entry and return it. If clone is T, a new copy of the document is created and the original is left untouched."
  239.   (let ((model (make-instance 'mongo-data-model :collection (collection model)
  240.                               :document (if clone (clone-document (document model)) (document model)))))
  241.     (db.insert (collection model) (document model))
  242.     model))
  243.  
  244. (defmacro with-fields ((&rest fields) model &rest body)
  245.   "Lets you access fields directly by name. This is the same stuff as with-accessors or with-slots."
  246.   (let ((vargens (gensym "MODEL")))
  247.     `(let ((,vargens ,model))
  248.        (symbol-macrolet ,(loop for field in fields collect `(,field (model-field ,vargens ,(string-downcase (symbol-name field)))))
  249.          ,@body))))
  250.  
  251. (defsetf model-field model-field-set)
  252.  
  253. (defgeneric clone-document (var))
  254.  
  255. (defmethod clone-document (var) var)
  256.  
  257. (defmethod clone-document ((var cons))
  258.   (if (listp (cdr var))
  259.       (loop for item in var collect (clone-document item))
  260.       (cons (car var) (clone-document (cdr var)))))
  261.  
  262. (defmethod clone-document ((document document))
  263.   (let ((doc (make-document)))
  264.     (setf (cl-mongo::elements doc)
  265.           (clone-document (cl-mongo::elements document)))
  266.     doc))
  267.  
  268. (defmethod clone-document ((table hash-table))
  269.   (loop with trg = (make-hash-table :test 'equal)
  270.        for key being the hash-keys of table
  271.        for val being the hash-values of table
  272.        do (setf (gethash key trg) (clone-document val))
  273.        finally (return trg)))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement