En este tema, aprenderemos cómo interactuar con bases de datos desde Haskell. Cubriremos los conceptos básicos de la conexión a una base de datos, la ejecución de consultas y la manipulación de resultados. Utilizaremos la biblioteca persistent para facilitar estas tareas.

Contenido

Introducción a Persistent

persistent es una biblioteca de Haskell que proporciona una forma sencilla y segura de interactuar con bases de datos. Soporta varios sistemas de bases de datos, incluyendo SQLite, PostgreSQL y MySQL.

Ventajas de usar Persistent

  • Seguridad de tipos: Las consultas y operaciones están fuertemente tipadas.
  • Abstracción: Proporciona una capa de abstracción sobre SQL.
  • Facilidad de uso: Simplifica la definición de esquemas y la ejecución de consultas.

Configuración del Entorno

Para comenzar, necesitamos instalar las bibliotecas necesarias. Asegúrate de tener cabal o stack instalado.

Instalación con Cabal

cabal update
cabal install persistent persistent-sqlite persistent-template

Instalación con Stack

Añade las siguientes dependencias a tu archivo stack.yaml:

extra-deps:
- persistent-2.13.0.3
- persistent-sqlite-2.13.0.3
- persistent-template-2.8.2.3

Y luego ejecuta:

stack build

Definición de Esquemas

En persistent, definimos esquemas utilizando una sintaxis especial en un archivo de plantilla. Aquí hay un ejemplo de cómo definir un esquema para una tabla de usuarios:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}

import Database.Persist.TH

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
    name String
    age Int
    deriving Show
|]

Explicación del Código

  • GADTs, GeneralizedNewtypeDeriving, etc.: Extensiones de Haskell necesarias para persistent.
  • share: Función que genera el código necesario para interactuar con la base de datos.
  • mkPersist, mkMigrate: Funciones que crean las instancias necesarias para el esquema.
  • persistLowerCase: Quasi-quote que define el esquema.

Operaciones Básicas de CRUD

Conexión a la Base de Datos

Primero, necesitamos establecer una conexión a la base de datos. Aquí hay un ejemplo de cómo conectarse a una base de datos SQLite:

import Database.Persist.Sqlite
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runStdoutLoggingT)

main :: IO ()
main = runStdoutLoggingT $ withSqliteConn "test.db" $ \backend -> liftIO $ do
    runSqlConn (runMigration migrateAll) backend

Crear (Create)

Para insertar un nuevo usuario en la base de datos:

import Database.Persist

main :: IO ()
main = runStdoutLoggingT $ withSqliteConn "test.db" $ \backend -> liftIO $ do
    runSqlConn (runMigration migrateAll) backend
    runSqlConn (insert $ User "Alice" 30) backend

Leer (Read)

Para leer usuarios de la base de datos:

main :: IO ()
main = runStdoutLoggingT $ withSqliteConn "test.db" $ \backend -> liftIO $ do
    runSqlConn (runMigration migrateAll) backend
    users <- runSqlConn (selectList [] []) backend
    liftIO $ print (users :: [Entity User])

Actualizar (Update)

Para actualizar un usuario existente:

main :: IO ()
main = runStdoutLoggingT $ withSqliteConn "test.db" $ \backend -> liftIO $ do
    runSqlConn (runMigration migrateAll) backend
    userId <- runSqlConn (insert $ User "Alice" 30) backend
    runSqlConn (update userId [UserAge =. 31]) backend

Eliminar (Delete)

Para eliminar un usuario:

main :: IO ()
main = runStdoutLoggingT $ withSqliteConn "test.db" $ \backend -> liftIO $ do
    runSqlConn (runMigration migrateAll) backend
    userId <- runSqlConn (insert $ User "Alice" 30) backend
    runSqlConn (delete userId) backend

Ejemplo Práctico

Vamos a crear un pequeño programa que permita agregar, listar y eliminar usuarios desde la línea de comandos.

Código Completo

{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}

import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runStdoutLoggingT)
import System.Environment (getArgs)

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
    name String
    age Int
    deriving Show
|]

main :: IO ()
main = do
    args <- getArgs
    case args of
        ("add":name:age:_) -> runDb $ insert_ (User name (read age))
        ("list":_)         -> runDb $ selectList [] [] >>= liftIO . print
        ("delete":name:_)  -> runDb $ deleteWhere [UserName ==. name]
        _                  -> putStrLn "Usage: add <name> <age> | list | delete <name>"

runDb :: SqlPersistT (LoggingT IO) a -> IO a
runDb query = runStdoutLoggingT $ withSqliteConn "test.db" $ \backend -> runSqlConn query backend

Explicación del Código

  • main: Lee los argumentos de la línea de comandos y ejecuta la operación correspondiente.
  • runDb: Función auxiliar para ejecutar consultas en la base de datos.

Ejercicios Prácticos

  1. Ejercicio 1: Modifica el programa para que también permita actualizar la edad de un usuario.
  2. Ejercicio 2: Añade una nueva tabla Post con los campos title y content, y crea relaciones entre User y Post.

Soluciones

Solución Ejercicio 1

main :: IO ()
main = do
    args <- getArgs
    case args of
        ("add":name:age:_)    -> runDb $ insert_ (User name (read age))
        ("list":_)            -> runDb $ selectList [] [] >>= liftIO . print
        ("delete":name:_)     -> runDb $ deleteWhere [UserName ==. name]
        ("update":name:age:_) -> runDb $ updateWhere [UserName ==. name] [UserAge =. read age]
        _                     -> putStrLn "Usage: add <name> <age> | list | delete <name> | update <name> <age>"

Solución Ejercicio 2

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
    name String
    age Int
    deriving Show

Post
    title String
    content String
    userId UserId
    deriving Show
|]

Conclusión

En esta sección, hemos aprendido cómo interactuar con bases de datos en Haskell utilizando la biblioteca persistent. Hemos cubierto la configuración del entorno, la definición de esquemas y las operaciones básicas de CRUD. Además, hemos implementado un ejemplo práctico y proporcionado ejercicios para reforzar los conceptos aprendidos.

En el próximo tema, exploraremos cómo realizar pruebas y depuración en Haskell para asegurar la calidad y el correcto funcionamiento de nuestras aplicaciones.

© Copyright 2024. Todos los derechos reservados