pixel ([info]pinterface) wrote,
@ 2009-05-07 02:46:00
Previous Entry  Add to memories!  Tell a Friend  Next Entry
Entry tags:cl-perec, lisp, programming, tutorial

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) - (Post a new comment)

Suggestions, clarifications, comments by a cl-perec author
[info]melevy
2009-06-11 08:39 pm UTC (link)
Let me give you some hints, suggestions. Please don't take this an offence, I'm trying to be productive here.

You don't need to create a new database class, cl-rdbms supports mixing in other classes into its own database classes.

(setf *database*
(make-instance 'cl-rdbms:postgresql-postmodern :transaction-mixin 'cl-perec:transaction-mixin :connection-specification ...))

This way you don't have to find a name for each database subclass that has cl-perec transaction-mixin as its superclass. Otherwise you would end up with names like cl-perec-postgresql-postmodern, etc.

BTW, I think the name you gave your database class is quite confusing in that it mixes up the concepts of database and connection which are different things in cl-rdbms terms. (and they are also different from transaction)

If you set the database instance into cl-rdbms:*database* then you don't need to provide it to with-transaction* and you can use the simpler version called with-transaction. Having SLIME and fuzzy completion we simply type wtr TAB TAB and get with-transaction immediately.

Inspecting persistent instances is possible outside of a transaction. There is SLIME/swank support for this under perec/integration/swank.lisp. Unfortunately it is loaded only for our own SLIME branch, because there were some problems with SLIME regarding this at the time it was written. I don't know if that has been fixed meanwhile.

The way you eliminated the prefixes is correct. We have the prefix to be able to have table names that are keywords in SQL while we don't want to add quotes all around the generated SQL statements but you can easily change that.

Maybe you didn't notice it, but you can also execute simple SQL statements using cl-rdbms with-transaction and execute, so you don't have to fight with another SQL tool. Just use the REPL... There is also start-sql-recording which will print all executed SQL statements along with the bindings.

Some other hints: there are many test cases which can help you to see how cl-perec's API can be used. We know that there are practically no documentation but we have other priorities right now.

Some other nice things you may want to check:
- multiple inheritance
- association specialization
- multi dimensional slot values (dimensions such as time is simple to use), see cl-dwim authenticated-session if you also would like to have an example how it can be used for a full blown history
- full control over slot storage location for each effective slot of each class, the two extremes are: all class instances stored in a single table, all classes have their own table, and many other ways in between...

Have fun and don't hesitate to ask on cl-perec-devel.

levy

--
There's no perfectoin

(Reply to this) (Thread)

Re: Suggestions, clarifications, comments by a cl-perec author
[info]pinterface
2009-07-22 03:09 am UTC (link)
Let me give you some hints, suggestions. Please don't take this an offence, I'm trying to be productive here.

Criticism is always welcome! Sometimes the only way to know you're going the wrong way is for somebody to smack you upside the head and say so.

You don't need to create a new database class, cl-rdbms supports mixing in other classes into its own database classes.

Except I still need to make the database class to futz with SQL name generation, don't I? (Well, assuming I don't want to override it for everything inside the lisp image, I mean.)

If you set the database instance into cl-rdbms:*database* then you don't need to provide it to with-transaction*

I'm certain I considered that and rejected it, but I can't think of any reason for having done so and have thusly updated things accordingly. Thanks for calling me on that.

Inspecting persistent instances is possible outside of a transaction. … Unfortunately it is loaded only for our own SLIME branch

Man, slime's enough of a moving target without worrying about forks!

Maybe you didn't notice it, but you can also execute simple SQL statements … There is also start-sql-recording which will print all executed SQL statements

Indeed, that fact failed to register. I'll have to play with that. And how did I miss #'start-sql-recording? It has been usefully aiding me quite well since I started using it yesterday.

Some other hints: there are many test cases which can help you to see how cl-perec's API can be used.

Indeed. At some point I need to sit down and work through those.

So far I'm really enjoying cl-perec's nearly obscene flexibility, if only because it lets me disagree with the defaults where I think the defaults are wrong. On the downside, it means I've got a lot of ground to cover and figuring out what's going on can be pretty tricky. So many levels of indirection!

(Reply to this) (Parent)

Elephant
[info]dlw.myopenid.com
2009-09-25 01:09 pm UTC (link)
I'd be interested in hearing more about what problem you had with Elephant. I don't understand what you mean about "not playing well with non-Elephant applications." Thank you! -- Dan Weinreb

(Reply to this) (Thread)

Re: Elephant
[info]pinterface
2009-09-29 10:51 am UTC (link)

In short: I wanted an ORM, and Elephant isn't.

Cross-language operation is important to me, so if I can't give a copy of the DB schema to, say, a Perl programmer and have it be fairly obvious how to generate a DBIx::Class object, I'm not comfortable with the system. Not so much because I expect to be having multiple languages accessing the data (it's possible, but by no means certain) as for the confidence of knowing if all I have is the database, I can still do something useful with it. (Perhaps I should have said "non-lisp applications" instead?) Elephant's postmodern backend generates a DB schema which does not resemble the data at all.

Wanting the database to look pretty much identical to how I'd do things without all the fancy autogeneration is, unfortunately, a cause of a number of my cl-perec woes as well. But it at least starts close enough I can see how to get (mostly) there. :)

Once I'm ready to take the plunge away from the comfortably familiar relational database, Elephant is at the top of my list.

(Reply to this) (Parent)


(4 comments) - (Post a new comment)

Create an Account
Forgot your login or password?
Login w/ OpenID
English • Español • Deutsch • Русский…