Biblioteca HDBC

De Wikihaskell
Saltar a: navegación, buscar
Bases de datos
Manipulación de bases de datos relacionales
Lenguaje Haskell
Biblioteca HDBC
Autores Emma Blanco
Francisco Javier Vázquez
Sonia Peinado

HDBC (Haskell Database Connectivity) proporciona una capa de abstracción entre programas escritos en Haskell y el SQL usado en las bases de datos relacionales. Esto permite escribir código Haskell manipulando la BD una vez, y poder usarlo con distintos SGBD, como pueden ser:

  • MySQL
  • PostgreSQL
  • Oracle
  • SQLite
  • BD compatibles con ODBC


Contenido

Estructura de la biblioteca

Estructura de la biblioteca HDBC

En la actualidad existen multitud de bases de datos, entre las más conocidas están: Oracle, MySQL, SQLite... Todas ellas tienen en común que hacen uso de un lenguaje estandarizado llamado SQL. El uso de este estándar nos permite definir una capa abstracta, en la cual podemos interactuar con los diversos motores de bases de datos de forma transparente, de forma que si queremos dejar de usar Oracle en una de nuestras aplicaciones, esta interfaz nos lo permitirá sin realizar prácticamente ningún cambio en nuestro programa.


Como podemos observar, en el nivel más bajo están los diferentes motores de las bases de datos, estos utilizan como lenguaje de interactuación SQL, no obstante hay características que son propias de cada motor de base de datos, como puede ser el proceso de conexión. Es por ello, que se hace necesario definir una capa sobre los motores de las bases de datos, que nos permita abstraernos de estos detalles, a dicha capa le llamaremos HDBC Drivers, la cual nos permitirá un acceso transparente a las diferentes bases de datos. Dado que existen varios HDBC Drivers, se hace necesario definir una última capa, desde la que realmente accederemos a los distintos modelos de bases de datos.


Drivers disponibles


Instalación

Se deberá tener instalado el compilador GHC

Desde fichero tar.gz

Descargar los ficheros .tar.gz (o .zip) de HDBC y el driver necesario para el SGBD a usar:

Para cada uno de los paquetes descargados, realizar el siguiente proceso (sustituir fichero por el nombre del paquete concreto):

  1. Desempaquetar el fichero tar.gz
$ tar -xvzf fichero.tar.gz
  1. Acceder al directorio desempaquetado
$ cd fichero
  1. Ejecutar los comandos:
$ runghc Setup.lhs configure
$ runghc Setup.lhs build
$ runghc Setup.lhs install

Desde repositorio

Descargar HDBC y el driver necesario para el SGBD a usar:

  • libghc6-hdbc-dev
  • libghc6-hdbc-sqlite3-dev
  • libghc6-hdbc-postgresql-dev
  • libghc6-hdbc-odbc-dev

Para la instalación, sustituir paquete por los paquetes necesarios de la lista anterior.

  1. Actualizar la lista de paquetes
$ sudo apt-get update
  1. Descargar los paquetes necesarios:
$ sudo apt-get install paquete


Módulos

HDBC

HDBC-sqlite3

HDBC-postgresql

HDBC-odbc


Uso de la Biblioteca

Conexión a la BD

Cada base de datos tiene su propio método único de conexión (connectSqlite3, connectPostgreSQL, connectODBC, ...). La función de conexión de base de datos devolverá un manejador de la base de datos con el que podremos interactuar, dicho manejador lo hemos nombrado conn. El tipo exacto de este manejador puede variar, pero siempre será una instancia de la clase IConnection. Todas las funciones que utilizará para operar en las bases de datos funcionan con cualquier tipo instancia de IConnection. Cuando terminemos de trabajar con la base de datos, llamaremos a la función disconnect para desconectarnos.

ghci> conn <- connectSqlite3 "nombreBD"
ghci> disconnect conn

Transacciones

Una transacción es diseñada para asegurar que todos los componentes de una modificación se ven afectados o no. Las transacciones ayudan a prevenir que otros procesos accedan a la misma base de datos y vean datos que están siendo modificados en ese mismo instante. Por eso, muchas bases de datos requieren que se confirmen explícitamente los cambios antes de que se realicen en el disco. HDBC no soporta el modo de confirmación automática. Para confirmar los cambios en HDBC puede hacerse de dos formas:

  • Con la función commit, para escribir los datos en disco.
  • Con la función withTransaction, que confirma los datos cuando la función termina con éxito.
ghci> commit conn

En ocasiones podemos tener problemas con la BD, encontrar un error en los datos... y queremos deshacer los últimos cambios que se han producido en la BD, esto podemos conseguirlo con la función rollback. Esta función deshará los cambios hasta el último commit o withTransaction.

Los tipos de tablas por defecto de MySQL no soportan las transacciones. Por ello, el controlador de HDBC ODBC tiene instrucciones para la configuración de MySQL para indicar a HDBC que no soporta transacciones. De forma alternativa, se pueden usar las tablas de tipo InnoDB que sí soportan transacciones.

Consultas sencillas

Algunas de las consultas más simples de SQL no devuelven ningún valor. Estas consultas sirven para crear tablas, insertar o eliminar datos, etc. La función más básica para enviar consultas a una base de datos es run. Esta función recibe un objeto de tipo IConnection, una cadena que representa la consulta y una lista de parámetros.

ghci> run conn "consultaSQL" []
NumFilasAfectadas

Tras conectarnos a la BD, con run podemos hacer una consulta a la BD y tras ejecutar la sentencia, obtendremos el número de filas que se han visto afectadas. Si la consulta está destinada a crear una tabla, el número de filas afectadas será cero.

Para las consultas destinadas a mostrar información de la BD se utiliza la función quickQuery' en lugar de run, de modo que cuando se ejecuta la sentencia en vez de mostrar el número de filas afectadas, se muestra una lista con los resultados.

ghci> quickQuery' conn "consultaSQL" []
[listaResultados]

Observar que estas modificaciones son aparentes, hasta que no confirmemos los cambios no se producirán realmente en el disco.

SQLValues

HDBC pretende ser una interfaz genérica para interactuar con el mayor número de SGBD posible, es por ello que dicha biblioteca deberá permitirnos abstraernos de los detalles que difieran entre los distintos SGBD. Uno de los principales focos de este problema, son los diferentes esquemas de tipos usados por cada SGBD, ya que cada uno de estos puede definir sus tipos de forma diferente, es más, podría darse el caso de que los SGBD para los cuales queremos dar soporte, ni si quiera tuvieran los mismos tipos.

Para salvar este problema HDBC define un tipo llamado SqlValue:

data SqlValue = SqlString String
              | SqlByteString B.ByteString
              | SqlWord32 Word32
              | SqlWord64 Word64
              | SqlInt32 Int32
              | SqlInt64 Int64
              | SqlInteger Integer
              | SqlChar Char
              | SqlBool Bool
              | SqlDouble Double
              | SqlRational Rational
              | SqlEpochTime Integer                  
              | SqlTimeDiff Integer                                 
              | SqlNull                                         
     deriving (Show)

A través de él, la biblioteca establece los tipos que podremos usar para interactuar con los distintos SGBD a través de HDBC. No obstante, para entender la utilidad de este nuevo tipo, deberemos conocer la definición de la clase SqlType:

class (Show a) => SqlType a where
    toSql :: a -> SqlValue
    fromSql :: SqlValue -> a

Esta clase declara dos funciones toSql y fromSql, el objetivo de éstas, es convertir entre los tipos de Haskell y los definidos por HDBC. Para ello se define una instancia de dicha clase para cada uno de los tipos de Haskell:

instance SqlType String
instance SqlType B.ByteString
instance SqlType Int
instance SqlType Int32
instance SqlType Int64
instance SqlType Word32
instance SqlType Word64
instance SqlType Integer
instance SqlType Bool
instance SqlType Char
instance SqlType Double 
instance SqlType Rational 
instance SqlType ClockTime
instance SqlType TimeDiff
instance SqlType CalendarTime

Dichas instancias definen los métodos toSql y fromSql para cada uno de sus tipos asociados. Para llegar a entender completamente como funciona este sistema de tipos, es necesario conocer como se definen estas instancias, para ello mostraremos la instanciación de SqlType para Bool:

instance SqlType Bool where
    toSql = SqlBool
    fromSql (SqlString x) =
        case map toUpper x of
                           "TRUE" -> True
                           "T" -> True
                           "FALSE" -> False
                           "F" -> False
                           "0" -> False
                           "1" -> True
                           _ -> error $ "fromSql: cannot convert SqlString "
                                        ++ show x ++ " to Bool"
    fromSql (SqlByteString x) = (fromSql . SqlString . byteString2String) x
    fromSql (SqlInt32 x) = numToBool x
    fromSql (SqlInt64 x) = numToBool x
    fromSql (SqlWord32 x) = numToBool x
    fromSql (SqlWord64 x) = numToBool x
    fromSql (SqlInteger x) = numToBool x
    fromSql (SqlChar x) = numToBool (ord x)
    fromSql (SqlBool x) = x
    fromSql (SqlDouble x) = numToBool x
    fromSql (SqlRational x) = numToBool x
    fromSql (SqlEpochTime x) = numToBool x
    fromSql (SqlTimeDiff x) = numToBool x
    fromSql (SqlNull) = error "fromSql: cannot convert SqlNull to Bool"
 

Como podemos ver en la instanciación, un Bool puede ser convertido en un SqlBool usando para ello la función toSql. Por otro lado, es posible convertir cualquiera de los tipos definidos por HDBC en un tipo Bool, de hecho podemos ver en la definición como es posible convertir un SqlString en un Bool. A través de estas conversiones, HDBC podría llegar a dar soporte a un SGBD que solo disponga del tipo "String", encargándose las citadas funciones de conversión de permitirnos trabajar estos valores según nuestras necesidades.

Parámetros de consulta

HDBC, como la mayoría de las bases de datos, soportan parámetros que pueden ser reemplazados en las consultas. Hay tres ventajas principales al usar parámetros reemplazables:

  • Previenen los ataques de inyección SQL u otros problemas cuando la entrada contiene comillas.
  • Mejoran el rendimiento cuando se ejecutan sentencias similares repetidamente.
  • Permiten una inserción fácil y portable.

Por ejemplo, si se quisieran añadir mil filas a una determinada tabla, se podrían realizar mil inserciones con la cláusula INSERT INTO ... VALUES ..., pero forzaría a que el servidor de la base de datos analizase cada sentencia SQL una por una. En cambio, podríamos reemplazar el contenido del VALUES por una variable, de modo que el servidor sólo analizase una única sentencia SQL y la ejecutase varias veces con diferentes datos.

ghci> run conn "INSERT INTO nombreBD VALUES (?, ?)" [toSql valor1, toSql valor2]
NumFilasAfectadas

En este caso, las interrogaciones representan a las variables. Ahí irán la lista de de SqlValue que se indican tras la consulta usamos toSql para convertir cada ítem en un tipo SqlValue. HDBC automáticamente realiza la conversión en la representación adecuada para su uso en la base de datos.

Sentencias predefinidas

HDBC define una función prepare que prepara una sentencia SQL que no recibe ningún parámetro. Una vez que se tiene la sentencia lista, podemos llamarla todas las veces que queramos por medio de la función execute.

ghci> inserta <- prepare conn "INSERT INTO nombreBD VALUES (?, ?)"
ghci> execute inserta [toSql valor1, toSql valor1]
NumFilasAfectadas

En este ejemplo, creamos una sentencia predefinida inserta, de modo que podemos ejecutarla con distintos valores tantas veces como queramos.

HDBC también ofrece una función executeMany que admite una lista de filas de datos para llamar a la sentencia predefinida, como si hubiéramos utilizado la función execute n veces.

ghci> executeMany inserta [[toSql valor1, toSql valor2], [toSql valor3, toSql valor4]]

Funciones varias

En HDBC existen muchas funciones que nos permitirán obtener información sobre diferentes elementos.

La siguiente función nos permitirá conocer el driver asociado a la conexión que recibe como parámetro

hhdbcDriverName :: IConnection conn => conn -> String

Esta función nos permitirá conocer la versión de la biblioteca cliente que usa HDBC en la conexión asociada

hdbcClientVer :: IConnection conn => conn -> String

Esta función nos permitirá conocer la versión del SGBD asociado a la conexión

dbServerVer :: IConnection conn => conn -> String

Esta otra función nos informará de la posibilidad de realizar transacciones en el SGDB asociado a la conexión

dbTransactionSupport :: IConnection conn => conn -> Bool

Con esta función podremos obtener un listado de las tablas que hay visibles a través de la conexión asociada

getTables :: IConnection conn => conn -> IO [String]


Manejo de excepciones

Las excepciones en HDBC son del tipo SqlError. Un SqlError nos proporcionará información sobre:

  • seState :: String (estado de la BD)
  • seNativeError :: Int (código de error numérico de la BD)
  • seErrorMsg :: String (mensaje de error)

HDBC nos proporciona diferentes funciones con las que podremos realizar el manejo de excepciones:

La función catchSql es similar a la proporcionada por el Prelude de Haskell, con la única diferencia de que solamente captura los SqlError. Esta función ejecutará la acción que se le pasa como primer parámetro, y en el caso que se produzca un SqlError, se llamará al manejador que se le pasa como segundo parámetro, devolviendo el valor que devuelva éste. En caso de que no se produzca ningún SqlError, se procederá normalmente.

catchSql :: IO a -> (SqlError -> IO a) -> IO a

La función handleSql tiene el mismo cometido que catchSql, simplemente que tiene invertidos sus dos parámetros.

handleSql :: (SqlError -> IO a) -> IO a -> IO a

La función handleSqlError captura SqlErrors y los relanza como un IOError. Esta función nos puede ser útil cuando no nos importa capturar la excepción pero queremos que se nos muestre un mensaje indicándonos el error, ya que ghc no es capaz de mostrar un SqlError, simplemente nos mostraría: *** Exception: (unknown). Una buena práctica puede ser empezar nuestros programas que hacen uso de HDBC con: main = handleSqlError $ do, lo cual nos asegurará que todo SqlError que no sea capturado será mostrado de manera inteligible.

handleSqlError :: IO a -> IO a

Ejemplo

Para mostrar el funcionamiento de esta biblioteca, se está construyendo un ejemplo, en el cual construiremos y explotaremos una pequeña base de datos usando HDBC y Sqlite3:

Ejemplo Biblioteca HDBC

Enlaces externos

Herramientas personales