Wednesday, July 22nd, 2009

Peering Down the Rabbit Hole with cl-perec

Don't forget to check out the other articles in this series:

  1. Getting Started with cl-perec
  2. Persisting Simple Types with cl-perec
  3. Sensible Serializing with cl-perec
  4. Peering Down the Rabbit Hole with cl-perec (you are here)

As anyone following this guide to cl-perec has undoubtedly noticed, while I've spent considerable time exploring how to put data in the database, I've not spent any on getting all that precious data back out. Today, I shall remedy that.

Let us suppose your PHB wanders by and asks you what the title is of the report located in the file "/share/tps-reports/Testing". Not to worry, you can select a single instance using cl-perec:select-instance.¹

select-instance has a fairly simple calling syntax.

(select-instance (/var-name/ /class-of-instance/)
  (where [condition-form]))

For example, we can select our tps report by file name with:

(cl-perec:with-transaction
  (cl-perec:select-instance (report tps-report)
    (cl-perec:where
      (equal (report-path-of report)
             #p"/share/tps-reports/Testing"))))

Note that accessor methods on the instance variable are used to pick the SQL column, and an instance of the object to which we want to compare that column is used for our value. This is because the value-to-be-compared goes through all the lisp→sql conversion machinery.²

Of particular interest is the where clause, which is proper CL³. The upshot to this is that it's very easy to select based upon an arbitrary predicate. The downside is that, if cl-perec can't figure out how to translate your where clause, it will filter the parts it doesn't understand in Lisp. Failure to remember that may result in accidentally hosing your performance. #'cl-perec:start-sql-recording is very useful here for checking if things are being converted to SQL in the way you expect.

Simple, standard functions, such as 'equal, get translated to the obvious SQL, as do boolean expressions involving AND, NOT, and OR. Notably excepted, however, string= and string-equal are not translated into SQL.

As you might expect, user-defined equality predicates are not converted to SQL. As usual, however, cl-perec offers unexported functionality to extend itself.

Say, for instance, you want to be able to query report-path using wild pathnames, to fetch all the tps-reports in a certain directory, like so:

(cl-perec:with-transaction
  (cl-perec:select-instances (r tps-report)
    (cl-perec:where (pathname-match-p
                      (report-path-of r)
                      #p"/share/tps-reports/*"))))

A legitimate use case if I ever saw one! To make this work in SQL, we need to convert it to use the LIKE operator, or possibly a regular expression. Rummaging around in query/mapping.lisp, we find the definer for like which points us in a direction. Awesome!

A little trial and error gets us to a very rough start.

(cl-def:def (cl-perec::query-function :lisp-args t) pathname-match-p (path wild)
  "Convert a comparison to a :wild pathname into SQL.  Does not support all possible :wild pathnames."
  (declare (cl-perec::persistent-type (cl-perec::forall (a) (function (a a) boolean))))
  (let* ((wild (cl-perec::value-of wild))
         (root (make-pathname :name nil :type nil :defaults wild))
         (name (if (wild-pathname-p wild :name)
                   "%"
                   (pathname-name wild)))
         (type (if (wild-pathname-p wild :type)
                   "%"
                   (pathname-type wild))))
    (cl-perec::sql-like :string (cl-perec::syntax-to-sql path)
                        :pattern (format nil "~a~a~@[.~a~]" root name type)
                        :case-sensitive-p t)))

path and wild are passed in as cl-perec 'syntax nodes'. syntax-to-sql converts a syntax node to SQL, and value-of, assuming a literal value was used, gets that literal value. Then it's just a matter of converting :wilds into SQL LIKE's wildcard, the percent sign (%).

Regrettably, this is pretty fragile. It only works if the wild pathname is passed in as a literal value; neither let-bound variables, special variables, nor #'make-pathname will work. (Not to mention the flaws in the matching itself!) Let's see what we can do about that.

(cl-def:def (cl-perec::query-function :lisp-args t) pathname-match-p (path wild)
  "Convert a comparison to a :wild pathname into SQL.  Does not support all possible :wild pathnames."
  (declare (cl-perec::persistent-type (cl-perec::forall (a) (function (a a) boolean))))
  (cl-perec::sql-like
    :string (cl-perec::syntax-to-sql path)
    :pattern (cl-perec::syntax-to-sql
              (cl-perec::make-special-form
               :operator 'let*
               :operands `(((wild ,wild)
                            (root (make-pathname :name nil :type nil :defaults wild))
                            (name (if (wild-pathname-p wild :name)
                                      "%"
                                      (pathname-name wild)))
                            (type (if (wild-pathname-p wild :type)
                                      "%"
                                      (pathname-type wild))))
                           (format nil "~a~a~@[.~a~]" root name type))))
    :case-sensitive-p t))

This is a bit messy—because the arguments we're given have already been converted into an abstract syntax tree, what would otherwise be fairly straightforward code must be converted into an AST as well—but it works. make-special-form creates an object that eventually gets evaluated, and it somehow all magically works out. The in-sql matching still isn't perfect, but it's pretty useable. Give it a try!

(cl-perec:with-transaction
  (cl-perec:select-instances (r tps-report)
    (cl-perec:where
      (pathname-match-p (report-path-of r)
                        #p"/share/tps-reports/*"))))
(cl-perec:with-transaction
  (cl-perec:select-instances (r tps-report)
    (cl-perec:where
      (pathname-match-p (report-path-of r)
                        (make-pathname :directory '(:absolute "share" "tps-reports") :name :wild)))))
(let ((w (make-pathname :directory '(:absolute "share" "tps-reports") :name :wild)))
  (cl-perec:with-transaction
    (cl-perec:select-instances (r tps-report)
      (cl-perec:where
        (pathname-match-p (report-path-of r)
                          w)))))
(defvar *wild* (make-pathname :directory '(:absolute "share" "tps-reports") :name :wild))
(cl-perec:select-instances (r tps-report)
  (cl-perec:where
    (pathname-match-p (report-path-of r)
                      *wild*)))

All should produce SQL that looks something like this:

 BEGIN
 $1 = /share/tps-reports/% as TEXT
 SELECT r._oid, r.report_title, r.report_text, r.report_path FROM tps_report_ap r WHERE ((r.report_path LIKE $1::TEXT))
 COMMIT

Pathname matching that handles directories with :wild and :wild-inferiors properly is left as an exercise to the reader, but I'll leave you with a few hints: look into the re-like query-function, which provides you with access to the database server's regular expression engine; and ignore the fact you can't portably rely on the format of namestrings.

Footnotes

  1. You can also get multiple objects using select-instances which, other than the pluralization, has identical syntax to select-instance. You'll see that in action a little later.
  2. You could write your lisp→sql conversion function to pass through strings and avoid the temporary object, but presumably you're using an object instead of a string because it buys you something.
  3. There is, however, a subtle caveat: the parts of an expression cl-perec can convert to SQL are (quite reasonably) not verified in Lisp land. This means comparison functions can return different results depending on whether they were applied via SQL or in Lisp.
    (with-transaction
      (values
        (select-instance (r tps-report)
          (where (eql (report-title-of r) "Testing P")))
        (select-instance (r tps-report)
          (where (funcall #'eql (report-title-of r) "Testing P")))))
    
    #<TPS-REPORT …>, NIL
  4. I assume there's an easier way and I just haven't figured it out yet. This is scary far into the depths of cl-perec and all the meta hurts my little brain.
(Leave a comment)

Tuesday, July 21st, 2009

Sensible Serializing with cl-perec

Don't forget to check out the other articles in this series:

  1. Getting Started with cl-perec
  2. Persisting Simple Types with cl-perec
  3. Sensible Serializing with cl-perec (you are here)
  4. Peering Down the Rabbit Hole with cl-perec

If a class slot contains an object, cl-perec will do its best to persist that object into the database using cl-serializer. Unfortunately, that persistence leaves something to be desired: namely, interoperability with non-lisp systems. I mean, binary strings and type codes? Ew!¹

Fortunately, because we know our data fairly well, we're going to serialize without the hassle of type codes. First, let's revisit our TPS-REPORT class, and add a pathname² at which that report can be found.

WARNING! Don't execute this yet, or you'll have to restart your lisp image.³

(cl-perec:defpclass* tps-report ()
  ((report-title :type boring-string)
   (report-text  :type even-string)
   (report-path  :type pathname)))

Calling back upon cl-perec::defmapping, which we discovered earlier, we map the pathname type to an sql character type.

(cl-perec::defmapping pathname
  (cl-rdbms::sql-character-large-object-type)
  'pathname-sql-reader
  'pathname-sql-writer)

pathname-sql-reader and -writer don't exist, so we'll have to make them. They aren't terribly complicated functions: all they do is turn a pathname into a string and vice versa. Actually, that sounds kinda like some functions that already exist in Common Lisp, making our job pretty easy.

The reader function is supposed to take a sequence and an index, then get its value from that sequence starting at index. The writer function takes a value to convert, and a sequence plus index where it is supposed to store the value.

(defun pathname-sql-reader (rdbms-values index)
  (pathname (elt rdbms-values index)))
(defun pathname-sql-writer (slot-value rdbms-values index)
  (setf (elt rdbms-values index)
        (namestring slot-value)))

Exciting, no? But there's one more thing we need to do: ensure cl-perec will actually use our mapping.

(pushnew 'pathname cl-perec::*mapped-type-precedence-list*)

Note this is a different list from what we used when we were defining types. I don't know why.

Our mapping defined, it's safe to redefine the tps-report class as above. Redefining the class earlier would have caused the pathname type mapping to be set, after which changing the mapping has no effect.³

That done, we can point our tps-report records to their corresponding file.

(cl-perec:with-transaction
  (make-instance 'tps-report
                 :report-title "Testing P"
                 :report-text "This is a test"
                 :report-path #p"/share/tps-reports/Testing"))
test_db=> select * from tps_report;
_oidreport_titlereport_textreport_path
4752604Testing PThis is a test/share/tps-reports/Testing

And, best of all, non-lisp code can still make sense of report_path!

cl-perec offers an incredible amount of flexibility in how your data is mapped between your lisp image and the database, upon which we have barely scratched the surface. Just remember that with great power comes great responsibility, and probably a few bugs. Also remember that order matters, so if something isn't working you think should be, execution order may be to blame.

Footnotes

  1. As I've mentioned before, the entire point of using an ORM rather than an object database is so other systems in other languages can play along.
  2. Strangely, pathnames are not supported by cl-serializer, so we'd have to jump through these hoops anyway. But it applies just as well to other objects.
  3. cl-perec seems to cache computed values a little too aggressively and I haven't figured out how to force it to recompute things. Unfortunately, this means I end up restarting my lisp pretty frequently during development and that's a pretty painful way to mold a codebase.
  4. I'm told you can use the index to do funky things like splitting a value across multiple columns. I haven't found myself with the desire for that and so leave that particular exercise to the reader (for now, at least).
  5. Specifically, the pathname type would be mapped to the t class, causing pathnames to run through the cl-serializer machinery and eventually error out during a database write because cl-serializer doesn't support pathname serialization.

Continue to Peering Down the Rabbit Hole with cl-perec

(Leave a comment)

Thursday, May 21st, 2009

Persisting Simple Types with cl-perec

Don't forget to check out the other articles in this series:

  1. Getting Started with cl-perec
  2. Persisting Simple Types with cl-perec (you are here)
  3. Sensible Serializing with cl-perec
  4. Peering Down the Rabbit Hole with cl-perec

You're busy defining your data classes, when without thinking you do something like this:

(deftype scale-1-10 () `(integer 1 10))

(defpclass* pointy-haired-boss (generic-guy)
  ((hair-color :black :type (member :black :blue :green))
   (intelligence 5 :type scale-1-10)
   (moxy 5 :type scale-1-10)
   (grumpiness 5 :type scale-1-10)))

Unfortunately, while :type (integer 1 10) would have worked fine, your named type does not, instead resulting in some unhelpful output:

WARNING: Could not process type SCALE-1-10 specified for slot INTELLIGENCE, falling back to type T. The error was: Unknown type specifier SCALE-1-10
WARNING: Could not process type SCALE-1-10 specified for slot MOXY, falling back to type T. The error was: Unknown type specifier SCALE-1-10
WARNING: Could not process type SCALE-1-10 specified for slot GRUMPINESS, falling back to type T. The error was: Unknown type specifier SCALE-1-10

Not even real conditions, just stuff liable to get lost in miles of scrolling compilation notes.¹ Seemingly, this is easily solved by changing 'deftype' to 'cl-perec:defptype'.

(cl-perec:defptype scale-1-10 () `(integer 1 10))

Sadly, this comes with one rather annoying and unmentioned caveat: your package must :use cl-perec, or at least import the right symbols. You see, defptype uses defclass-star, which when generating accessor methods from slot-names does not use the slot-name's home package, no-no. It uses the current *package*, potentially resulting in undefined methods where there shouldn't be.

The defptype expansion includes this:

(defclass-star:defclass* scale-1-10-type
    (cl-perec:persistent-type)
  ((cl-perec::name 'scale-1-10)
   (cl-perec::args 'nil)
   (cl-perec::body '('(integer 1 10)))
   (cl-perec::substituter …)
   (cl-perec::parser …))
  (:export-accessor-names-p t))

which expands into:

(defclass scale-1-10-type
    (cl-perec:persistent-type)
  ((cl-perec::name … :accessor name-of …)
   (cl-perec::args … :accessor args-of …)
   (cl-perec::body … :accessor body-of …)
   (cl-perec::substituter … :accessor substituter-of …)
   (cl-perec::parser      … :accessor parser-of …)))

Notice how name-of, args-of, body-of, substituter-of, and parser-of are not prefixed by the cl-perec package. That's because they aren't in the cl-perec package, even though they should be, which doesn't really do us much good.

There are a few ways to work around this:²

  1. Import the necessary symbols from cl-perec into our own package.
  2. Create a secondary package which imports all the necessary symbols and exports just the stuff we want in our main package.
  3. Put an (in-package #:cl-perec) form just before our defptype and refer to our symbols package-qualified.
Basically, so long as defptype is macroexpanded in a package that includes symbols from cl-perec, it will generate code that actually does what it's supposed to. Our options are narrowed considerably, however, when we discover upon macroexpansion an assortment of symbols 'helpfully' exported, all related to the internal machinations of defptype. Because we don't want those symbols infesting our primary package, the path of least resistance—and least code—is option number 3: a well-placed in-package.

Not to be deterred from causing trouble, defptype will 'helpfully' try to export your type-name and fail because the symbol it's trying to export isn't available in the cl-perec package, so you'll also need to add an (import '(type-name)) after the in-package.³

To shield ourselves from as many problems as possible, let's package up a combination of options 2 and 3. First, a defstar-shield package.

(defpackage #:defstar-shield
  (:documentation "Beware proton torpedoes.")
  (:use #:cl #:cl-perec)
  (:shadowing-import-from #:cl-perec #:set #:time)
  (:shadow #:defptype)
  (:export #:defptype))

Then a wrapper around cl-perec:defptype.

(in-package #:defstar-shield)
(defmacro defptype (name args &body body)
  "Wrapper around cl-perec:defptype."
  (let ((package (package-name *package*)))
    `(eval-when (:compile-toplevel :load-toplevel :execute)
       (in-package #:defstar-shield)
       (import '(,name)) ; lame
       (cl-perec:defptype ,name ,args ,@body)
       (in-package ,package))))

This is a bit messy, but fairly simple: it arranges *package* so cl-perec:defptype gets macroexpanded within the defstar-shield package, making sure not to leak that package change into your surrounding code.

That finally out of the way, you continue on.

(defun string-has-even-length-p (str)
  (evenp (length str)))

(cl-perec:defpclass* tps-report ()
  ((report-title
     :type (and string
                (satisfies string-has-even-length-p)))))

And once again, you are met with frustrating failure. cl-perec doesn't really support (and) type specifiers, and so when you try to create a tps-report you end up with errors thorough investigation reveals to be related to the string type. You see, cl-perec's type mapping is a bit awkward. For instance, the string mapping machinery gets called for an '(and string …) type, and then promptly mistakes (and …) for (string n), and tries to determine the string's length, which goes over about as well as you might expect.

At this point, we've got a couple of options: we can dig into the string mapping and fix that, or we can give our type a name and trick cl-perec into thinking it's a primitive type. I've tried to go both ways, and the latter is less work (and more in line with using a library), so that's what I'll show you.

First, we give our type a name.

(defstar-shield:defptype even-string ()
  `(and string (satisfies string-has-even-length-p)))

Then we push it onto the list of canonical types.

(pushnew 'even-string cl-perec::*canonical-types*)

This ensures cl-perec won't convert 'even-string into '(and string …), which was causing our woes. At this point, cl-perec will treat even-strings as though they were regular strings—quite possibly by accident, but it works.

(cl-perec:defpclass* tps-report ()
  ((report-title :type even-string)
   (report-text  :type string)))

And now we can generate tps-reports, with the strange and arbitrary requirement that their titles have an even number of characters.

Later, our boss comes by and says each TPS report should be barcoded for easy automatic identification.

(defun calculate-checkdigit (barcode)
  "UPC-A barcode checkdigit."
  (- (nth-value
      1
      (ceiling
       (loop :for digit :across barcode
             :for pos :from 1
             :for num = (digit-char-p digit)
             :sum (if (oddp pos)
                      (* 3 num)
                      num))
       10))))

(defun valid-barcode-p (barcode)
  "Returns true if the given string is a valid barcode."
  (and (= 12 (length barcode))
       (every #'digit-char-p barcode)
       (= (calculate-checkdigit (subseq barcode 0 11))
          (digit-char-p (char barcode (1- (length barcode)))))))

(defstar-shield:defptype barcode ()
  `(and (string 12) (satisfies valid-barcode-p)))
(pushnew 'barcode cl-perec::*canonical-types*)

(cl-perec:defpclass* tps-report ()
  ((barcode      :type barcode)
   (report-title :type even-string)
   (report-text  :type string)))

You get all that coded up only to discover your barcode type isn't creating a fixed-length field in the database like it should. All our fancy workarounds are coming back to haunt us!

We could backtrack, figure out how to fix the combination of (and string) types in cl-perec, and go from there. Or, we could trudge along delving further into the scary guts of cl-perec.

Delving farther in than a library user should probably go, we discover cl-perec::defmapping, which looks promising. Copying things from the default string defmapping and modifying slightly, we come up with something that works.

(cl-perec::defmapping barcode
  (cl-rdbms::sql-character-type :size 12)
  'cl-perec::identity-reader
  'cl-perec::identity-writer)

It's pretty scary that everything we use is unexported, but what we're doing is pretty simple: this says the lisp-type 'barcode equates to an SQL string 12 characters long. identity-reader and identity-writer are the functions that convert a string from SQL into a lisp object—and vice versa—in this case, by doing nothing.

Regrettably, this all feels pretty hacky. After all, we've just gone to a lot of trouble for no other reason than that cl-perec doesn't properly support (and string satisfies) types. But we've got an application to write, so it'll have to do for now.

Footnotes

  1. a comment inside cl-perec notes due to the MOP we must not fail when this is called, otherwise the entire (sblc) [sic] image breaks. So much for using the debugger!
  2. I don't include fixing defstar-class in this list. For all I know, it does this stuff intentionally.
  3. This means your type name cannot conflict with any symbol in your defstar-shield package. If this might be a problem, be wary of which packages are used by your defstar-shield package, possibly limiting yourself to importing only necessary symbols.
  4. It also breaks the return value, but you didn't want that anyway.
  5. Common Lisp lacks any standardized facility to perform type inspection, so we can't really blame cl-perec for the defptype requirement.
  6. Common Lisp type specifiers are turing-complete, which makes full automatic conversion to database types impossible. (and string) may not seem difficult, but it isn't far to a fairly hairy type specifier.

Continue to Sensible Serializing with cl-perec

(2 comments | Leave a comment)

Thursday, May 7th, 2009

Getting Started with cl-perec

Don't forget to check out the other articles in this series:

  1. Getting Started with cl-perec (you are here)
  2. Persisting Simple Types with cl-perec
  3. Sensible Serializing with cl-perec
  4. Peering Down the Rabbit Hole with cl-perec

So, you want to write a database-backed Lisp thang. You've certainly got plenty of options: elephant, postmodern, and cl-perec, to name a few.

Elephant you rule out because it doesn't play very well with non-elephant applications (last I checked, anyway). Postmodern you rule out because, while pretty close to what you want, cl-perec's associations are dreamy and your data involves a lot of relationships.

First, you'll need to get postgresql and cl-perec. For postgres, use the closest available package management system if at all possible. For cl-perec, because its list of dependencies is long and troublesome, I recommend acquisition using clbuild¹. That's how I did it, and it's worked out pretty well so far.

Second, we need to create a database, and a user who can access that database. See postgres' Manual for details.² It's long and complicated, so I'll see you next week.

Back already? Excellent. Now we need to tell cl-perec about our database. That starts with creating a class for the database connection. I don't know why, it just does.

(defclass database-connection
    (cl-perec:database-mixin
     cl-rdbms:postgresql-postmodern)
  ())

The required cl-perec:database-mixin provides some of the necessary machinations for our database-connection class to be recognized properly by cl-perec. cl-rdbms:postgresql-postmodern represents the underlying data store—you might also use cl-rdbms:oracle or cl-rdbms:sqlite, depending on which database you want.

Then we can create a variable to hold an instance of our new class. Or, in this case, we'll use the variable thoughtfully provided by cl-perec.

(defparameter
  cl-perec:*database*
  (make-instance 'database-connection
    :connection-specification
    '(:database  "test_db"
      :user-name "test_user"
      :password  "test_password")))

Exciting, no? Finally, we can venture into the land of ORM and create our tables.

(cl-perec:defpclass* generic-guy ()
  ((name      "Ted" :type string)
   (tie-color :blue :type (member :blue :green))))

Over in pgsql, we do a \dt and see … nothing. That's because cl-perec doesn't create the tables until you try to create an object using those tables. Since a database with no data is pretty useless, this isn't really a big deal. Still, if you really want to force those tables into existence even without data, cl-perec::ensure-exported is what you're looking for.³

cl-perec demands everything happen within a transaction, which means we're going to have to wrap everything we type at the REPL in a with-transaction so we can see its results over in pgsql right away. So, if you aren't already using it, look into slime-fuzzy-completion: it dramatically reduces the amount of typing necessary to produce a long symbol.

That out of the way, it's time to create our first generic-guy.

(cl-perec:with-transaction
  (make-instance 'generic-guy :tie-color :green)) =>
#<GENERIC-GUY :persistent t>

(inspect *) => ERROR

Whoops! You can't access the slots of a persistent object outside of a transaction. We're just playing around though, so let's opt out of that requirement.

(setf (cl-perec:persistent-p *) nil)

Now we can (inspect **) without worrying about transactions—just don't expect to see any changes reflected in the database!

Back in pgsql, we notice that all the table and column names created for us by cl-perec are prefixed by incredibly ugly underscores.

test_db=> \dt+
List of relations
SchemaNameTypeOwnerDescription
public_generic_guytabletest_user
test_db=> select * from _generic_guy;
_oid_name_tie_color
996143Ted1

This isn't really what we want because, well, why would we want it? We're using an underlying SQL database so other applications in other programming languages can also get at the data, and forcing an underscore prefix on them isn't really very nice. Fortunately, our heretofore seemingly pointless database-connection class can help us make changes without affecting any other cl-perec-using code.

;; I'm not a fan of having an underscore prefix
;; everything in postgres.
(defmethod cl-rdbms::calculate-rdbms-name
  ((db database-connection) thing name)
  "Cuts off the end of names that are too long and appends the
   hash of the original name.

   WARNING: This name mapping is not injective, different lisp
   names /may/ be mapped to the same rdbms name."
  (cl-rdbms::calculate-rdbms-name-with-utf-8-length-limit
    name
    cl-rdbms.postgresql::+maximum-rdbms-name-length+
    :prefix ""))

The usage of cl-rdbms internals is unfortunate, but I didn't see an easier way to eliminate the underscore prefix. (If you know of one, feel free to let me know.)

Unfortunately, now you're going to run into a bit of trouble: cl-perec (quite reasonably) stores the results of calculate-rdbms-name, which means old tables are going to continue using the underscore-prefixed names, while any new tables will use the prefix-less variants. Fortunately, we're just playing around so nuking the database and restarting the lisp image is a-okay, if suboptimal. (Redefining your class might work, too.)

Retracing our prior steps after we've redefined our name mapping, we can see the results in pgsql.

test_db=> \dt+
List of relations
SchemaNameTypeOwnerDescription
publicgeneric_guytabletest_user
test_db=> select * from generic_guy;
_oidnametie_color
1127215Ted1

Much prettier!

Anyway, now that we have a generic_guy table, and Ted, the green-tied generic guy, it's time to start thinking about relationships. But before we can worry about relationships, we need somebody or something to which we can relate. We could model Ted's love life, but instead we'll model his employment. First, we'll need a heartless corporation.

(cl-perec:defpclass* corporation ()
  ((name         :type string)
   (evilness 5   :type (integer 0 10))))

(cl-perec:with-transaction
  (make-instance 'corporation
    :name "Initech"
    :evilness 8)) =>
#<CORPORATION :persistent t>

Then we can begin defining employee-employer relationships and make Ted work for Initech.

(cl-perec:defassociation*
  ((:class corporation
    :slot employees
    ;; NOT cl:set!
    :type (cl-perec:set generic-guy))
   (:class generic-guy
    :slot employer
    :type corporation)))

;; assuming *ted* and *initech* contain our
;; previously-created objects
(cl-perec:with-transaction
  ;; revive-instance imports an instance into the
  ;; current transaction
  (cl-perec:revive-instances *ted* *initech*)
  (setf (employer-of *ted*) *initech*))

Congratulations Ted, you've been hired! Note how cl-perec handles the other direction of the relationship for us.

(cl-perec:with-transaction
  (cl-perec:revive-instances *initech*)
  (employees-of *initech*)) =>
(#<GENERIC-GUY :persistent t>) ; '(Ted)

Also notice how cl-perec altered the tables to handle our new relationship all by itself.

test_db=> select * from generic_guy;
_oidnametie_coloremployer_oid
1127215Ted11232276
test_db=> select * from corporation;
_oidnameevilness
1232276Initech8

As you can see, in spite of some rather spartan documentation, cl-perec isn't too bad, so get out there and start defining your data. I'll be back with more when I get to that point in my application—but don't hold your breath 'cause I'm pretty slow. :)

Footnotes

  1. clbuild is essentially a technical solution to the social problem of Lispers being terrible at proper releases. Like most open-source projects geared towards programmers, it assumes a POSIX system—if you're on Windows, you've got two options: install a posix system (in a VM is probably fine; on an old, unused box works well too), or shell out for AllegroCache (which has the advantage of actual documentation, though it's closer in spirit to Elephant).
  2. I ended up with something like
    sudo -u postgres createdb …
    sudo -u postgres createuser …
    But your environment may differ. When I figure out how to do it a second time, I'll hopefully remember to update this with specifics.
  3. e.g.,
    (cl-perec:with-transaction
      (cl-perec::ensure-exported
        (find-class 'generic-guy)))
  4. Around this time I managed to bork SBCL's class definer and had to restart lisp if I wanted to define or alter any new classes, so I never bothered to look into what needed to change to start using the new, prettier names. You probably shouldn't change the name mapping in a production system anyway.
  5. While you could achieve similar results by amending the generic-guy class to add an
    (employer nil :type (or corporation null))
    slot, you wouldn't get the otherwise-free employees-of method. Regardless, you'll need to know defassociation for M-N relationships.
  6. Some of the coding conventions are a bit annoying (e.g., the #t/#f readmacros in place of t and nil), but that's a rant for another day.
  7. cl-perec:defpclass* is a thin wrapper around defclass-star which sets the metaclass to cl-perec:persistent-class. defclass-star tries to provide sensible accessor name defaults (slot-name-of), among other things.
  8. If you have multiple databases, or perhaps just multiple database connections, have a look at with-transaction*:
    (cl-perec:with-transaction* (:database *test-db*)
      &body))
    Of course, you could also just bind cl-perec:*database* yourself.

Continue to Persisting Simple Types with cl-perec

(4 comments | Leave a comment)