Книга: Practical Common Lisp
Defining a Schema
Defining a Schema
Thus, to make an instance of table
, you need to build a list of column
objects. You could build the list by hand, using LIST
and MAKE-INSTANCE
. But you'll soon notice that you're frequently making a lot column objects with the same comparator and equality-predicate combinations. This is because the combination of a comparator and equality predicate essentially defines a column type. It'd be nice if there was a way to give those types names that would allow you to say simply that a given column is a string column, rather than having to specify STRING<
as its comparator and STRING=
as its equality predicate. One way is to define a generic function, make-column
, like this:
(defgeneric make-column (name type &optional default-value))
Now you can implement methods on this generic function that specialize on type
with EQL
specializers and return column
objects with the slots filled in with appropriate values. Here's the generic function and methods that define column types for the type names string
and number
:
(defmethod make-column (name (type (eql 'string)) &optional default-value)
(make-instance
'column
:name name
:comparator #'string<
:equality-predicate #'string=
:default-value default-value
:value-normalizer #'not-nullable))
(defmethod make-column (name (type (eql 'number)) &optional default-value)
(make-instance
'column
:name name
:comparator #'<
:equality-predicate #'=
:default-value default-value))
The following function, not-nullable
, used as the value-normalizer
for string
columns, simply returns the value it's given unless the value is NIL
, in which case it signals an error:
(defun not-nullable (value column)
(or value (error "Column ~a can't be null" (name column))))
This is important because STRING<
and STRING=
will signal an error if called on NIL
; it's better to catch bad values before they go into the table rather than when you try to use them.[293]
Another column type you'll need for the MP3 database is an interned-string
whose values are interned as discussed previously. Since you need a hash table in which to intern values, you should define a subclass of column
, interned-values-column
, that adds a slot whose value is the hash table you use to intern.
To implement the actual interning, you'll also need to provide an :initform
for value-normalizer
of a function that interns the value in the column's interned-values
hash table. And because one of the main reasons to intern values is to allow you to use EQL
as the equality predicate, you should also add an :initform
for the equality-predicate
of #'eql
.
(defclass interned-values-column (column)
((interned-values
:reader interned-values
:initform (make-hash-table :test #'equal))
(equality-predicate :initform #'eql)
(value-normalizer :initform #'intern-for-column)))
(defun intern-for-column (value column)
(let ((hash (interned-values column)))
(or (gethash (not-nullable value column) hash)
(setf (gethash value hash) value))))
You can then define a make-column
method specialized on the name interned-string
that returns an instance of interned-values-column
.
(defmethod make-column (name (type (eql 'interned-string)) &optional default-value)
(make-instance
'interned-values-column
:name name
:comparator #'string<
:default-value default-value))
With these methods defined on make-column
, you can now define a function, make-schema
, that builds a list of column
objects from a list of column specifications consisting of a column name, a column type name, and, optionally, a default value.
(defun make-schema (spec)
(mapcar #'(lambda (column-spec) (apply #'make-column column-spec)) spec))
For instance, you can define the schema for the table you'll use to store data extracted from MP3s like this:
(defparameter *mp3-schema*
(make-schema
'((:file string)
(:genre interned-string "Unknown")
(:artist interned-string "Unknown")
(:album interned-string "Unknown")
(:song string)
(:track number 0)
(:year number 0)
(:id3-size number))))
To make an actual table for holding information about MP3s, you pass *mp3-schema*
as the :schema
initarg to MAKE-INSTANCE
.
(defparameter *mp3s* (make-instance 'table :schema *mp3-schema*))
- 27. Practical: An MP3 Database
- Inserting Values
- Defining mail transport protocols
- 1.1.4 Defining the Embedded System
- 4.3 Defining an RTOS
- 5.2 Defining a Task
- 6.2 Defining Semaphores
- 7.2 Defining Message Queues
- Defining a Class
- Defining an Interface
- 8. Macros: Defining Your Own
- Defining New Functions