quinta-feira, 19 de junho de 2008

Acessando base de dados SQL com Scheme

Em alguns projetos em que estou trabalhando seguidamente tenho que acessar tabelas de bases de dados. Costumo usar o Postgres através do egg postgresql do sistema Chicken.

Para evitar de esquecer de fechar as conexões com o banco, normalmente uso um procedimento que recebe uma query como argumento. Este procedimento abre a conexão com o banco, executa a query e fecha a conexão automaticamente (o desempenho que se dane :-)). As credenciais do banco mantenho em um parâmetro (definido com make-parameter). O procedimento é algo como:

(define db-credentials (make-parameter '()))

(define (db-query query)
(let* ((db (pg:connect (db-credentials)))
(output (pg:query-tuples query db)))
(pg:close db)
output))

Mesmo com o uso do procedimento db-query, o acesso a colunas do banco não é das tarefas mais simples. Abaixo está um exemplo em que quero acessar as colunas username e email de uma tabela e associar o valor delas à variáveis em Scheme:

(db-credentials '((host . "localhost")
(user . "usuario")
(password . "****")
(dbname . "nome-da-base")))

(let* ((results
(let ((results
(db-query
"select username,email from users where user_id=1")))
(if (null? results)
#f
(car results))))
(username (and results (vector-ref results 0)))
(email (and results (vector-ref results 1))))
(print username)
(print email))

Como pode ser visto no exemplo, associar valores de colunas da base de dados a variáveis em Scheme é uma certa novela. Para facilitar esta tarefa, fiz o esquema mostrado abaixo:

(use postgresql)

(define db-map:credentials (make-parameter '()))

(define db-map:create-object
(let ()
(define (db-query query)
(let* ((db (pg:connect (db-map:credentials)))
(output (pg:query-tuples query db)))
(pg:close db)
output))
(lambda (query fields)
(let* ((query-results (let ((results (db-query query)))
(if (null? results)
#f
(car results)))))
(lambda (field)
(and query-results
(let ((pos (list-index (cut eq? <> field)
fields)))
(vector-ref query-results pos))))))))

O procedimento db-map:create-object recebe uma query SQL e uma lista de símbolos a serem associados com os valores das colunas obtidos como resultado da execução da query. db-map:create-object retorna um procedimento que recebe como argumento um símbolo representando uma coluna da base de dados e que retorna o valor associado ao símbolo.

Assim, para acessar o valor das colunas username e email, faço o seguinte:

(let ((obj (db-map:create-object
"select username,email from users where user_id=1"
'(username email))))
(print (obj 'username))
(print (obj 'email)))

A ordem dos símbolos da lista passada como segundo argumento deve ser a mesma dos valores das colunas resultantes da query SQL.

quinta-feira, 12 de junho de 2008

Persistência de dados (e código!) em Scheme

Hoje eu e o Vilson estávamos conversando sobre persistência de dados (e código!) em Lisp. Fiz um exemplo simples e estou colocando abaixo para não perder a viagem. :-)

O exemplo implementa um objeto mem (criado com o procedimento make-mem) e procedimentos para manipulação desse tipo de objeto: mem-get (para leitura de dados) e mem-set! (para escrita em memória e em disco).

A leitura de dados é sempre feita da memória (exceto na criação do objeto, que pode aproveitar dados do arquivo passado como argumento). As escritas são feitas em memória e em disco.

Os dados são armazenados em uma hash-table e indexados por símbolos.

Abaixo está a implementação simplificada (em Chicken Scheme), que usa o egg s11n:

(use s11n)

(define (make-mem file)
(cons file (if (file-exists? file)
(with-input-from-file
file
(cut deserialize))
(make-hash-table))))

(define (mem-get mem key #!optional default)
(hash-table-ref/default (cdr mem) key default))

(define (mem-set! mem key val)
(let ((file (car mem))
(data (cdr mem)))
(hash-table-set! data key val)
(with-output-to-file file (cut serialize data))))

A seguir está um exemplo que armazena uma lista e um procedimento (código!):

(let ((mem (make-mem "teste.data")))
(print (mem-get mem 'a))
(mem-set! mem 'a '(1 2 3))
(print (mem-get mem 'a))
(mem-set! mem 'soma (lambda (a b) (+ a b)))
(print ((mem-get mem 'soma) 2 2)))

O resultado da execução do código do exemplo é (caso em que teste.data inicialmente não existe):


#f
(1 2 3)
4