|
This is a pre-print extract from the forthcoming O'Reilly book Lisp Outside the Box. Contents are subject to change as the book's production progresses. Feedback is most welcome, either in private by or in public by responding to the blog entry which announced this chapter. Table of Contents Persistence is the first Lisp feature we’ve encountered which is not part of Common Lisp. Its goal is to make storage unobtrusive for the programmer by—in some sense—remembering “forever” all instances of certain CLOS classes. A typical side-effect of persistence is data sharing between different Lisp images. When people list features beyond the ANSI standard which they’d like to see standardized and more widely implemented, persistence is often high on their list. There’s a choice of persistence libraries and each has its pros and cons. We’re going to look at one in some detail. I’ve picked AllegroCache because it’s a good example of a proprietary library which ships with a Lisp implementation, in this case Allegro Common Lisp. We can expect it to integrate well with the underlying Lisp system but must realise in advance that any code we write using it won’t port to other Lisps. TipWe can also expect it to integrate well with other Allegro add-ons. For example, Allegro Prolog can reason directly over AllegroCache data. There’s a database at the back of AllegroCache and many of the operations you can perform with persistent objects have equivalents in the relational database world. That leads us quite rightly to asking why we shouldn’t use a standard SQL database instead. Persistence has the following advantages over SQL:
In short, integrating persistence with the underlying objects makes you, the programmer, more productive by allowing you to get on with thinking about your application instead of the demands of communicating with the database upon which it depends. Here’s a simple example in which I’ll create a database, serve it on my network, and consider using it as an authoring tool. We’ll cover the following operations in detail later.
;; Load AllegroCache and use its exports in the current package.
(require :acache "acache-2.1.11.fasl")
(use-package :db.ac)
;; Define a persistent class.
(defclass sentence ()
((text :initarg :text :accessor sentence-text)
(next :initform nil :accessor sentence-next))
(:metaclass persistent-class))
;; Create a database and connect to it.
(start-server "~/play/acache" 8707 :if-does-not-exist :create)
(open-network-database "localhost" 8707)
;; Start writing; create some persistent objects...
(let ((first-sentence
(make-instance 'sentence
:text (format nil "Persistence is the first Lisp~@
feature we've encountered which~@
is not part of Common Lisp.")))
(second-sentence
(make-instance 'sentence
:text "Its goal is to make storage unobtrusive.")))
(setf (sentence-next first-sentence) second-sentence))
;; ... and save them to the database.
(commit)
And here’s one way in which someone elsewhere (my editor, perhaps?) might check on my progress:
;; Load AllegroCache as above and connect to my server.
(require :acache "acache-2.1.11.fasl")
(use-package :db.ac)
(open-network-database "gannet.ravenbrook.com" 8707)
;; View all sentences, in some undetermined order.
(let ((sentences nil))
(doclass (sentence 'sentence)
(push (sentence-text sentence) sentences))
sentences)
=>
("Persistence is the first Lisp
feature we've encountered which
is not part of Common Lisp."
"Its goal is to make storage unobtrusive.")
;; Hmm, OK so far but not quite done.
TipThere isn’t space here to document every aspect of AllegroCache in full. You can download the AllegroCache reference manual along with Flash presentations, a tutorial, administrator’s guide, and more by visiting:
This chapter and the next will show you how to create and update persistent objects with AllegroCache, work with its transaction model, build complex queries, and administer the underlying database. You’ll need to be comfortable with the Common Lisp Object System (Chapter 7, The Object System) to get the most from them; the material here will be useful for following examples in Chapters 15 and 16 but is not necessary for understanding the rest of the book. AllegroCache brings us into contact with the libraries that deal with memory management and with multi-threading. Like AllegroCache they’re non-standard but unlike it they’re ubiquitous. They’ll be the subjects of the Chapters 15 and 16. While writing this I worked with version 2.1.11 of AllegroCache running on version 8.1 of Allegro CL’s free Express Edition on Windows XP. You can expect identical behavior with different editions of Allegro CL and with different OS platforms. Some features of AllegroCache itself may change incompatibly in future versions and the library’s documentation makes it very clear which these are; we’ll steer away from these here. AllegroCache ships with the Lisp it runs on, Allegro CL. In the first instance, once you’ve installed ACL you don’t have to download or install anything else in order to use the AllegroCache library. AllegroCache and Allegro CL have different release cycles. So it’s possible that the AllegroCache which you installed along with Allegro CL isn’t the most recent version. That shouldn’t be a problem for the purposes of this chapter but if you do want to get your hands on the latest and greatest then you’ll need to follow these steps.
Whether or not you’ve done this download, the next step is to load AllegroCache into your Lisp session, thus: (require :acache)
Don’t fret if the
CL-USER(1):
The output you’ll see depends on precise version and
platform and so might not be exactly the same as this. What matters
is that it doesn’t say All AllegroCache utilities are exported from the
CL-USER(2):
In a real application it’s often a matter of taste whether you “use” packages or explicitly qualify the symbols you need. AllegroCache stores your data in a (proprietary) database and our next task is to connect to that. There are two modes of use:
To connect to a standalone database, call CL-USER(3): Note (lines 5 and 7) that the second call to The database is represented by a Lisp object which is returned
from the call to TipNote this pattern: we have a function which sets the object it’s about to return into a global variable; utility functions accept a keyword argument which defaults to the current value of that variable. It’s a useful approach and one you’ll meet elsewhere. As an example, to close the default database connection:
CL-USER(9):
—note how the database’s printed representation has now changed—and to close any other connection either (close-database :db *my-database*) or (let ((*allegrocache* *my-database*)) (close-database)) As another example, here’s a utility we might write for use on days when writer’s block gets the better of us. In SQL we'd use the “delete” command...
(defun flush-all-sentences (&key (connection *allegrocache*))
(doclass (sentence 'sentence :db connection)
(delete-instance sentence))
(commit :db connection))
Later on we’ll find the following macro handy. It takes a list of variables, a form which should return a database connection, and a body. Each of the variables will be bound to its own connection and then body will be run. The connections are guaranteed to be closed on exit. TipWith a standalone database only one connection is possible at a
time and in this case you should only supply one
(defmacro with-connections (connection-vars opening-form &body body)
`(let ,(loop for connection in connection-vars collect
`(,connection ,opening-form))
(unwind-protect
(progn ,@body)
(dolist (connection (list ,@connection-vars))
(close-database :db connection)))))
ExerciseUse ExerciseWrite a related macro (called ExerciseDo you need to be running ACL to work on the last two exercises or will any Common Lisp implementation do? Why / why not? Here’s what you have to do to make your data persistent:
connect to the database, define a persistent class, make instances
of that class and populate their slots, and
AllegroCache can only store certain types of Lisp object in the database, so you’re limited (but not much) as to what values you can set into a persistent object’s slots:
Making changes to a persistent object’s slots corresponds
to SQL’s ;; Modifying a persistent object's slot (setf (sentence-next no-hash-tables) similar-behavior-with-maps) If you change the values themselves then the new values will be
queued automatically for writing to the database and that’s
all there is to it. But if a slot contains a value which can be
modified (a cons, list, vector, string or non-persistent object)
then it doesn’t matter what changes you like to the internals
of that value: AllegroCache won’t know that the database
needs updating. You’ll have to handhold it by calling
TipHandy debugging aid: in the following example see how the object’s printed representation changes (a) when AllegroCache believes that it needs writing—equivalently the object appears to have been modified since it was last written—and (b) to reflect the transaction number at which the object was last written: if this number hasn’t gone up then the database hasn’t seen the change.
;; Any new persistent object is automatically queued for writing.
(setf goal
(make-instance 'sentence
:text "It's goal is to make storage unobtrusive?"))
=>
#<SENTENCE oid: 1013, ver 5, trans: NIL, modified @ #x212b3d72>
(progn (commit) goal)
=>
#<SENTENCE oid: 1013, ver 5, trans: 13, not modified @ #x212b3d72>
;; Whoa! Apostrophe catastrophe! We replace the text slot with a different
;; string and our change will be queued automatically.
(setf (sentence-text goal) "Its goal is to make storage unobtrusive?")
(progn (commit) goal)
=>
#<SENTENCE oid: 1013, ver 5, trans: 14, not modified @ #x212b3d72>
;; Fix punctuation. This time we modify the text slot (so it's still the
;; same Lisp object) and the change is not queued: the commit does nothing.
(let* ((text (sentence-text goal))
(length (length text)))
(setf (schar text (1- length)) #\.))
(progn (commit) goal)
=>
#<SENTENCE oid: 1013, ver 5, trans: 14, not modified @ #x212b3d72>
;; Manual request for updating. This time (commit) will write our latest
;; change.
(mark-instance-modified goal)
=>
#<SENTENCE oid: 1013, ver 5, trans: 14, modified @ #x212b3d72>
(progn (commit) goal)
=>
#<SENTENCE oid: 1013, ver 5, trans: 16, not modified @ #x212b3d72>
We’ll deal with this subject properly in the next chapter.
For now here’s one way of getting our data back. It’s
the macro (doclass (var class &key db) &body body) The macro executes its body with
(let ((sentences nil))
(doclass (sentence 'sentence)
(push sentence sentences))
sentences)
=>
(#<SENTENCE oid: 19011, ver 6, trans: 96, not modified @ #x21256b5a>
#<SENTENCE oid: 19010, ver 6, trans: 96, not modified @ #x21256b42>)
The related macro AllegroCache has a transaction model which relational database
users should recognize. As already noted, none of your changes will
be written to the database until you call
(let* ((original-text "AllegroCache has a transaction model.")
(sentence (make-instance 'sentence :text original-text)))
(commit)
(setf (sentence-text sentence) "Database users should recognize it.")
(rollback)
(sentence-text sentence))
=>
"AllegroCache has a transaction model."
(let* ((original-text "If you read or write their slots...")
(sentence (make-instance 'sentence :text original-text)))
(rollback)
(setf (sentence-text sentence) "... you'll get an error."))
=>
Error: attempt to access a deleted object: #<SENTENCE oid: 1015, ver 5,
trans: NIL, deleted @ #x21463cda>
TipThe predicate TipIf the database is networked, use Setting up AllegroCache to run as a server over your network is
straightforward enough. The function to do it is (start-server "~/play/acache" 8707 :if-does-not-exist :create) This function returns an object representing the server. To
close the server, call Once the server is running, any number of clients can connect to
it by calling (open-network-database "gannet.ravenbrook.com" 8707) => #<AllegroCache db "port 2345 to gannet.ravenbrook.com:8707" @ #x214d9e6a> CautionIf you’re running the Express Edition of ACL, don’t forget that there’s a limit of three open connections. If you’re experimenting and opening lots of connections, you should close them as you go. To close a connection, as with standalone connections, call
CautionCalling An AllegroCache transaction is atomic. That is, either it succeeds and all its changes are written to the database, or it fails and none of the changes are written. A typical reason for a write failing is that some other client has modified one of your objects behind your back. We’ll take a proper look at this in the next section. An AllegroCache connection is isolated from other connections. That is, you don’t see other clients’ changes until you’re ready to do so and they don’t see yours until you want them to.
The following example employs the
(with-connections (connection-1 connection-2)
(open-network-database "localhost" 8707)
;; Use the utility we defined earlier to flush all sentences out of the
;; database (and commit the change).
(flush-all-sentences :connection connection-1)
;; Make sure the other connection is in the same state.
(rollback :db connection-2)
;; First connection inserts a sentence and commits the change.
(let ((*allegrocache* connection-1))
(make-instance 'sentence
:text "An AllegroCache transaction is atomic."))
(commit :db connection-1)
;; Second connection inserts and commits its own sentence.
(let ((*allegrocache* connection-2))
(make-instance 'sentence
:text "An AllegroCache connection is isolated."))
(commit :db connection-2)
;; Second connection now sees both sentences (because commit updates your
;; cache).
(let ((sentences nil))
(doclass (sentence 'sentence :db connection-2)
(push (sentence-text sentence) sentences))
sentences))
=>
("An AllegroCache connection is isolated."
"An AllegroCache transaction is atomic.")
Two users can’t modify the same object at the same time.
More precisely: if you
(with-connections (connection-1 connection-2)
(open-network-database "localhost" 8707)
(flush-all-sentences :connection connection-1)
;; Start with both connections seeing one (and the same) sentence:
(let* ((*allegrocache* connection-1)
(sentence-1 (make-instance 'sentence
:text "If you commit a change...")))
(commit :db connection-1)
(rollback :db connection-2)
;; The first connection now updates the sentence and commits
;; its change.
(setf (sentence-text sentence-1) "no other client can do likewise...")
(commit :db connection-1)
;; The second connection has a own private copy of the sentence.
;; This copy does not reflect the update made by connection-1. (Why?)
(doclass (sentence 'sentence :db connection-2)
;; The following change hasn't been committed and is therefore
;; local to connection-2.
(setf (sentence-text sentence)
"until they've picked up your change."))
;; This attempt to commit must fail.
;; The unwind-protect in with-connections ensures that the
;; connections are closed cleanly.
(commit :db connection-2)))
=>
Error: Cannot commit because object #<SENTENCE oid: 4010, ver 5,
trans: 27, modified> was updated in another transaction
ExerciseModify this example so that it succeeds. ExerciseTrace |