Книга: Practical Common Lisp

Getting at the Results

Getting at the Results

Since select returns another table, you need to think a bit about how you want to get at the individual row and column values in a table. If you're sure you'll never want to change the way you represent the data in a table, you can just make the structure of a table part of the API—that table has a slot rows that's a vector of plists—and use all the normal Common Lisp functions for manipulating vectors and plists to get at the values in the table. But that representation is really an internal detail that you might want to change. Also, you don't necessarily want other code manipulating the data structures directly—for instance, you don't want anyone to use SETF to put an unnormalized column value into a row. So it might be a good idea to define a few abstractions that provide the operations you want to support. Then if you decide to change the internal representation later, you'll need to change only the implementation of these functions and macros. And while Common Lisp doesn't enable you to absolutely prevent folks from getting at "internal" data, by providing an official API you at least make it clear where the boundary is.

Probably the most common thing you'll need to do with the results of a query is to iterate over the individual rows and extract specific column values. So you need to provide a way to do both those things without touching the rows vector directly or using GETF to get at the column values within a row.

For now these operations are trivial to implement; they're merely wrappers around the code you'd write if you didn't have these abstractions. You can provide two ways to iterate over the rows of a table: a macro do-rows, which provides a basic looping construct, and a function map-rows, which builds a list containing the results of applying a function to each row in the table.[296]

(defmacro do-rows ((row table) &body body)
`(loop for ,row across (rows ,table) do ,@body))
(defun map-rows (fn table)
(loop for row across (rows table) collect (funcall fn row)))

To get at individual column values within a row, you should provide a function, column-value, that takes a row and a column name and returns the appropriate value. Again, it's a trivial wrapper around the code you'd write otherwise. But if you change the internal representation of a table later, users of column-value needn't be any the wiser.

(defun column-value (row column-name)
(getf row column-name))

While column-value is a sufficient abstraction for getting at column values, you'll often want to get at the values of multiple columns at once. So you can provide a bit of syntactic sugar, a macro, with-column-values, that binds a set of variables to the values extracted from a row using the corresponding keyword names. Thus, instead of writing this:

(do-rows (row table)
(let ((song (column-value row :song))
(artist (column-value row :artist))
(album (column-value row :album)))
(format t "~a by ~a from ~a~%" song artist album)))

you can simply write the following:

(do-rows (row table)
(with-column-values (song artist album) row
(format t "~a by ~a from ~a~%" song artist album)))

Again, the actual implementation isn't complicated if you use the once-only macro from Chapter 8.

(defmacro with-column-values ((&rest vars) row &body body)
(once-only (row)
`(let ,(column-bindings vars row) ,@body)))
(defun column-bindings (vars row)
(loop for v in vars collect `(,v (column-value ,row ,(as-keyword v)))))
(defun as-keyword (symbol)
(intern (symbol-name symbol) :keyword))

Finally, you should provide abstractions for getting at the number of rows in a table and for accessing a specific row by numeric index.

(defun table-size (table)
(length (rows table)))
(defun nth-row (n table)
(aref (rows table) n))

Оглавление книги

Генерация: 1.958. Запросов К БД/Cache: 3 / 0
Вверх Вниз