I have been tinkering with cl-elephant and I have a question:
How do I create unique key with elephant objects?
(defpclass book ()
((isbn :initarg :isbn
:accessor isbn
:index t)
(title :initarg :title
:accessor title
:index t)
(author :initarg :author
:accessor author
:index t)))
I know that there is oid (object id) for every object but what if I
want isbn to be unique? Do I have to check on my objects before
creating a new one?
Thanks for your help.
Um, yes, I think you should do that check yourself. What to do in case of
non-unique keys is application-dependent, and so you need to implement a
reaction to it yourself anyway.
Hi Alex. Thanks for your reply.
So I have to create a function (for example isbn-exists?) to check
whether a certain ISBN is already in the database, cl-elephant could
not do this automatically. I just find it strange to have to do this
manually.
Throwing CFFI out of my asdf-registry in favor of UFFI, I succeeded in
making Elephant work, and very soon I also missed that feature
(especially since I am a used to CLSQL where one can enforce this with
":db-kind :key" in the slot definition).
As a remedy, I inserted such a test for uniqueness in the
initialize-instance method as follows:
(defmethod initialize-instance :before
((ml ele-mailing-list) &key name &allow-other-keys)
...
(assert (null (find-ele-ml name)) () "This list already exists"))
Having a ":db-kind :key" slot option would make such kind of tests
superfluous and indicate intent as early as possible, and this I would
like. OTOH, since CLOS itself does not have this feature, I do not
consider it a necessesity for Elephant.
Nicolas
Hi Nicolas,
Thanks for giving me a hint about using initialize-instance :before to
check whether a key is in the database. That solves my question.
I am a newbie, and I learn by looking at simple example codes and I
just think cl-elephant example codes out there are a rare.
Cheers.
Well, Elephant is a CLOS persistence layer, not an object-relational
mapping. You're supposed to write application in CLOS-style, Lisp-style
instead of mimicking concepts or relational databases.
It is not like automatic constraint checks really buy you anything. Let's
suppose you have to show an error message when book with given ISBN already
exists.
Here's how a function which adds book into a database might look with manual
checks:
(defun add-book (isbn title author)
(if (get-instances-by-value 'book 'isbn isbn)
(show-error "Book with this ISBN already exists.")
(make-instance 'book :isbn isbn :title title :author author)))
And how it could look like if make-instance would throw an error on unique
constraing violation:
(defun add-book (isbn title author)
(handler-case
(make-instance 'book :isbn isbn :title title :author author)
(unique-constraint-violation ()
(show-error "Book with this ISBN already exists."))))
It didn't get shorter with automatic checking, and first version looks more
straightforward IMHO.
Automatic checking might prevent database inconsistency in case your code
has a bug -- you forgot to check it.
But as Common Lisp is dynamically-typed and doesn't usually check stuff
automatically, it would be kind of strange that you want automatic check for
this specific piece and not all over your code.
But if you think you actually need constraints, it's not that hard to
implement them.
E.g. something like (beware -- proof-of-concept quality code):
(defun effective->direct-slot-def (class esd)
(find (slot-definition-name esd) (class-direct-slots class)
:key #'slot-definition-name))
(defmethod (setf slot-value-using-class) :before
(new-value (class persistent-metaclass) (instance persistent-object)
(slot-def indexed-slot-definition))
(when (eq (indexed-p (effective->direct-slot-def class slot-def)) :unique)
(let ((sc (get-con instance)))
(ensure-transaction (:store-controller sc)
(let* ((idx (or (get-slot-def-index slot-def sc)
(ensure-slot-def-index slot-def sc)))
(other-oid (get-value new-value idx)))
(when (and other-oid (/= other-oid (oid instance)))
(error "unique constraint violation, new value:~a for slot ~a is already
present for instance ~a" new-value (slot-definition-name slot-def)
(get-value new-value idx))))))))
And now if you define it like this:
(defpclass book ()
((isbn :initarg :isbn
:accessor isbn
:index :unique)
(title :initarg :title
:accessor title
:index t)
(author :initarg :author
:accessor author
:index t)))
It will do check on that index.
ELE> (make-instance 'book :isbn "3" :author "a" :title "t")
#<BOOK oid:201>
ELE> (make-instance 'book :isbn "3" :author "a" :title "t")
unique constraint violation, new value:3 for slot ISBN is already present
for instance 201
[Condition of type SIMPLE-ERROR]
Restarts:
0: [ABORT] Return to SLIME's top level.
ELE> (get-instances-by-value 'book 'isbn "3")
(#<BOOK oid:201>)
So, it did not add second instance because of unique constrain violation.
But it only works so because signaling error have aborted transaction.
Well, this code has absolutely nothing to do with Elephant, it is a plain
CLOS code. That illustrates a point -- it is a good idea to learn CLOS if
you're using Elephant.
To clarify, this code was for illustrative purposes only, it works only in
simple situations when there is no class inheritance.
If you think that Elephant should implement unique constraints on indices,
please write about it into Elephant's mailing list.
It is actually pretty easy to add this feature, but I don't know whether
other Elephant developers agree on this.
Alex, thanks for helping me out. I still have a lot to learn. Your
remark about cl-elephant being a persistent CLOS layer, not ORM, I am
seeing the light now.
I think I am sticking with manual check, the latter one is more
complex, and as you said, it is not desirable to implement unique
constraints on indices, but actually it is what I originally asked
for :).
Thanks.