Procesar tabla de ejemplares

A veces encontramos errores de diseño en la fase de codificación. Este es uno de esos casos, al intentar eliminar cualquier fila de la tabla de ejemplares obtenemos un error, o más bien, una excepción:

sqlite> DELETE FROM ejemplar;
Error: foreign key mismatch
sqlite>

Esto se debe a un error de diseño de las tablas de la base de datos. Concretamente, en la tabla de préstamos usamos un campo clavelibro que es una clave foránea de la tabla ejemplar:

 ... clavelibro INTEGER REFERENCES ejemplar(clavelibro) ON DELETE ...

Sería más lógico usar como clave foránea una columna de la tabla ejemplar que no fuese a su vez una clave foránea de la tabla libro. Esto nos obliga a crear una nueva columna en la tabla ejemplar, claveejemplar que será además su clave primaria, y que usaremos para referenciar el ejemplar desde la tabla de prestamo.

Al mismo tiempo, crearemos una restricción de unicidad para la combinación clavelibro/numeroorden, para que no puedan existir dos ejemplares iguales.

Las tablas ejemplar y prestamo quedan así:

CREATE TABLE ejemplar(claveejemplar INTEGER PRIMARY KEY,
    clavelibro INTEGER NOT NULL
    REFERENCES libro(clavelibro)
    ON DELETE CASCADE ON UPDATE CASCADE,
    numeroorden INTEGER NOT NULL,
    edicion INTEGER,
    ubicacion TEXT,
    UNIQUE(numeroorden,clavelibro));
CREATE TABLE prestamo(clavesocio INTEGER REFERENCES socio(clavesocio)
    ON DELETE SET NULL ON UPDATE CASCADE,
    claveejemplar INTEGER REFERENCES ejemplar(claveejemplar)
    ON DELETE SET NULL ON UPDATE CASCADE,
    fecha_prestamo DATE NOT NULL,
    fecha_devolucion DATE DEFAULT NULL,
    notas TEXT);

En cuanto a las modificaciones, hacemos como en los casos anteriores. Primero añadimos los identificadores de menú necesarios en "menus.h":

#define NUEVOEJEMPLAR       22
#define EDITAREJEMPLAR      23
#define BORRAREJEMPLAR      24
#define CONSULTAEJEMPLAR    25

También añadimos las nuevas entradas en la tabla de menús, en menus.cpp:

    {9, "-", "---MENU EJEMPLARES---",0,TITULO},
    {9, "1", "Nuevo", 0, NUEVOEJEMPLAR},
    {9, "2", "Editar", 0, EDITAREJEMPLAR},
    {9, "3", "Borrar", 0, BORRAREJEMPLAR},
    {9, "4", "Consultar", 0, CONSULTAEJEMPLAR},
    {9, "0", "Salir <", 3, ABRIRMENU}

En main.cpp añadimos el include del nuevo fichero de cabecera (ejemplar.h), y los tratamientos de las nuevas opciones de menú dentro del switch:

#include "ejemplar.h"
...
            case NUEVOEJEMPLAR:
               NuevoEjemplar(db);
               break;
            case EDITAREJEMPLAR:
               EditarEjemplar(db);
               break;
            case BORRAREJEMPLAR:
               BorrarEjemplar(db);
               break;
            case CONSULTAEJEMPLAR:
               BuscarEjemplar(db);
               break;

Añadimos dos nuevos ficheros al proyecto: ejemplar.h, con las declaraciones de prototipos, y ejemplar.cpp con la implementación de las funciones:

/*
 * Aplicación de ejemplo de uso de SQLite en C++
 * EjemploSQLite
 * Salvador Pozo, Con Clase (www.conclase.net)
 * Abril de 2012
 * Fichero: ejemplar.h
 * fichero de cabecera para manupular datos de ejemplares de libros
 */

#ifndef __EJEMPLAR_H__
#define __EJEMPLAR_H__

#include <sqlite/sqlite3.h>

void NuevoEjemplar(sqlite3 *);
int ListaEjemplares(sqlite3 *);
void EditarEjemplar(sqlite3 *);
void BorrarEjemplar(sqlite3 *);
void BuscarEjemplar(sqlite3 *);
#endif
/*
 * Aplicación de ejemplo de uso de SQLite en C++
 * EjemploSQLite
 * Salvador Pozo, Con Clase (www.conclase.net)
 * Abril de 2012
 * Fichero: ejemplar.cpp
 * fichero de implementación para manipular datos de ejemplares de libros
 */

#include <iostream>
#include <iomanip>
#include "ejemplar.h"
#include "libro.h"

using namespace std;

void NuevoEjemplar(sqlite3 *db) {
    int clavelibro;
    int numeroorden;
    int edicion;
    char ubicacion[32];
    char categoria[2]; // "A".."F"
    sqlite3_stmt *ppStmt;
    int rc;
    char consulta[1024];
    bool existe, ignorar=false;
    char titulo[64];
    char numero[15];
    char resp[2];

    for(int i= 0; i < 24; i++) cout << endl;
    cout << "A continuacion se pedira el libro, numero de orden, edicion, ubicacion y categoría del ejemplar." << endl;

    cin.ignore();
    cout << "Libro: ";
    clavelibro = ListaLibros(db);
    cin.ignore();
    cout << "Numero de orden: ";
    cin.getline(numero,15);
    numeroorden = atoi(numero);
    cout << "Edicion: ";
    cin.getline(numero,15);
    edicion = atoi(numero);
    cout << "Ubicacion: ";
    cin.getline(ubicacion,32);
    cout << "Categoria: ";
    cin.getline(categoria,2);

    // Verificar si el nombre existe ya:
    sprintf(consulta, "SELECT titulo FROM ejemplar NATURAL JOIN libro "
            "WHERE clavelibro=%d AND numeroorden=%d;", clavelibro, numeroorden);
    rc = sqlite3_prepare_v2(db, consulta, -1, &ppStmt, NULL);
    existe=false;
    if( rc!=SQLITE_OK ){
        cout << "Error: " << sqlite3_errmsg(db) << endl;
    } else {
        if(SQLITE_ROW == sqlite3_step(ppStmt)) {
            existe = true;
            strncpy(titulo, (const char*)sqlite3_column_text(ppStmt, 0), 64);
            titulo[63]=0;
        }
        sqlite3_finalize(ppStmt);
    }

    if(!existe) {
        sprintf(consulta, "INSERT INTO ejemplar(clavelibro,numeroorden,edicion,ubicacion,categoria) VALUES(%d,%d,%d,'%s','%c');",
                clavelibro, numeroorden, edicion, ubicacion, categoria[0]);
    } else {
        cout << "Ya existe un ejemplar del libro [" << titulo << "] con el numero de orden [" << numeroorden;
        cout << "]\n(s)obrescribir, insert(a)r o (i)gnorar: " << endl;
        cin >> resp;
        switch(resp[0]) {
            case 's':
                sprintf(consulta, "UPDATE ejemplar SET edicion=%d,ubicacion='%s',categoria='%c' WHERE clavelibro=%d AND numeroorden=%d;",
                        edicion, ubicacion, categoria[0], clavelibro, numeroorden);
                break;
            case 'a':
                sprintf(consulta, "INSERT INTO ejemplar(clavelibro,numeroorden,edicion,ubicacion,categoria) VALUES(%d,%d,%d,'%s','%c');",
                        clavelibro, numeroorden, edicion, ubicacion,categoria[0]);
                break;
            case 'i':
            default:
                ignorar=true;
        }
    }
    if(!ignorar) {
        if(SQLITE_OK != sqlite3_exec(db, consulta, 0, 0, 0)) {
            cout << "Error: " << sqlite3_errmsg(db) << endl;
        }
        else cout << "Ejemplar insertado" << endl;
    }
    cin.ignore();
}

int ListaEjemplares(sqlite3 *db) {
    sqlite3_stmt *ppStmt;
    int rc;
    char consulta[1024];
    int desplazamiento=0;
    char resp[10];
    bool salir=false;
    bool ultima;
    int fila=0;
    int i;

    // Mostrar una lista, teniendo en cuenta que puede haber más de las que caben en una pantalla.
    do {
        cout << "Elegir ejemplar" << endl << endl;
        sprintf(consulta, "SELECT claveejemplar,titulo,numeroorden FROM ejemplar NATURAL JOIN libro "
                "ORDER BY titulo,numeroorden LIMIT 20 OFFSET %d;", desplazamiento);
        rc = sqlite3_prepare_v2(db, consulta, -1, &ppStmt, NULL);
        if( rc!=SQLITE_OK ){
            cout << "Error: " << sqlite3_errmsg(db) << endl;
        } else {
            i = 0;
            while(SQLITE_ROW == sqlite3_step(ppStmt)) {
                cout << sqlite3_column_int(ppStmt, 0) << ") " <<
                    sqlite3_column_text(ppStmt, 1) << " [" <<
                    sqlite3_column_int(ppStmt, 2) << "]" << endl;
                i++;
            }
            sqlite3_finalize(ppStmt);
        }
        ultima = (i < 20);
        while(i < 20) { cout << endl; i++; }
        cout << "\n" << "(n) editar, (s)ig pagina, (a)nt pagina, (x)salir" << endl;
        cin >> resp;
        switch(resp[0]) {
            case 's':
                if(!ultima) desplazamiento+=20;
                break;
            case 'a':
                if(desplazamiento > 0) desplazamiento-=20;
                break;
            case 'x':
                salir=true;
                break;
            default:
                if(isdigit(resp[0])) {
                    fila = atoi(resp);
                    salir=true;
                }
                break;
        }
    } while(!salir);

    return fila;
}

void EditarEjemplar(sqlite3 *db) {
    sqlite3_stmt *ppStmt;
    int rc;
    char consulta[1024];
    int numeroorden;
    int edicion;
    char ubicacion[32];
    char categoria[2]; // "A".."F"
    char numero1[16];
    char numero2[16];
    int i;
    int fila;
    bool salir=true;

    fila = ListaEjemplares(db);

    // Editar:
    for(i = 0; i < 22; i++) cout << endl;
    sprintf(consulta, "SELECT titulo,numeroorden,edicion,ubicacion,categoria FROM ejemplar NATURAL LEFT JOIN libro WHERE claveejemplar='%d';", fila);
    rc = sqlite3_prepare_v2(db, consulta, -1, &ppStmt, NULL);
    if( rc!=SQLITE_OK ){
        cout << "Error: " << sqlite3_errmsg(db) << endl;
    } else {
        i = 0;
        if(SQLITE_ROW == sqlite3_step(ppStmt)) {
            cout << "Titulo:          " << sqlite3_column_text(ppStmt, 0) << endl;
            cout << "Numero de orden: " << sqlite3_column_int(ppStmt, 1) << endl;
            cout << "Edicion:         " << sqlite3_column_int(ppStmt, 2) << endl;
            cout << "Ubicacion:       " << sqlite3_column_text(ppStmt, 3) << endl;
            cout << "Categoria:       " << sqlite3_column_text(ppStmt, 3) << endl;
            cout << "Dejar en blanco los campos que no se quieren modifiar" << endl;
            salir=false;
        }
        sqlite3_finalize(ppStmt);
    }
    if(!salir){
        cin.ignore();
        cout << "Numero de orden: ";
        cin.getline(numero1,15);
        numeroorden = atoi(numero1);
        cout << "Edicion: ";
        cin.getline(numero2,15);
        edicion = atoi(numero2);
        cout << "Ubicacion: ";
        cin.getline(ubicacion,32);
        cout << "Categoria: ";
        cin.getline(categoria,2);
        if(strlen(numero1)>0) {
            sprintf(consulta, "UPDATE ejemplar SET numeroorden=%d WHERE claveejemplar=%d;", numeroorden, fila);
            if(SQLITE_OK != sqlite3_exec(db, consulta, 0, 0, 0)) {
                cout << "Error: " << sqlite3_errmsg(db) << endl;
            }
        }
        if(strlen(numero2)>0) {
            sprintf(consulta, "UPDATE ejemplar SET edicion=%d WHERE claveejemplar=%d;", edicion, fila);
            if(SQLITE_OK != sqlite3_exec(db, consulta, 0, 0, 0)) {
                cout << "Error: " << sqlite3_errmsg(db) << endl;
            }
        }
        if(strlen(ubicacion)>0) {
            sprintf(consulta, "UPDATE ejemplar SET ubicacion='%s' WHERE claveejemplar=%d;", ubicacion, fila);
            if(SQLITE_OK != sqlite3_exec(db, consulta, 0, 0, 0)) {
                cout << "Error: " << sqlite3_errmsg(db) << endl;
            }
        }
        if(strlen(categoria)>0) {
            sprintf(consulta, "UPDATE ejemplar SET categoria='%c' WHERE clavelibro=%d;", categoria[0], fila);
            if(SQLITE_OK != sqlite3_exec(db, consulta, 0, 0, 0)) {
                cout << "Error: " << sqlite3_errmsg(db) << endl;
            }
        }
        cout << "Ejemplar modificado" << endl;
        cin.ignore();
    }
}

void BorrarEjemplar(sqlite3 *db) {
    sqlite3_stmt *ppStmt;
    int rc;
    char consulta[1024];
    int fila;
    char resp[2];
    int i;
    bool salir=true;

    fila = ListaEjemplares(db);
    cout << "Borrar: " << fila << endl;

    for(i = 0; i < 22; i++) cout << endl;
    sprintf(consulta, "SELECT titulo,numeroorden,edicion,ubicacion,categoria FROM ejemplar NATURAL LEFT JOIN libro WHERE claveejemplar='%d';", fila);
    rc = sqlite3_prepare_v2(db, consulta, -1, &ppStmt, NULL);
    if( rc!=SQLITE_OK ){
        cout << "Error: " << sqlite3_errmsg(db) << endl;
    } else {
        i = 0;
        if(SQLITE_ROW == sqlite3_step(ppStmt)) {
            cout << "Titulo:          " << sqlite3_column_text(ppStmt, 0) << endl;
            cout << "Numero de orden: " << sqlite3_column_int(ppStmt, 1) << endl;
            cout << "Edicion:         " << sqlite3_column_int(ppStmt, 2) << endl;
            cout << "Ubicacion:       " << sqlite3_column_text(ppStmt, 3) << endl;
            cout << "Categoria:       " << sqlite3_column_text(ppStmt, 4) << endl;
            cout << "Dejar en blanco los campos que no se quieren modifiar" << endl;
            salir=false;
        }
        sqlite3_finalize(ppStmt);
    }
    if(!salir){
        cin.ignore();
        cout << "Borrar este registro? (s/n)" << endl;
        cin >> resp;
        if(resp[0] == 's' || resp[0] == 'S') {
            sprintf(consulta, "DELETE FROM ejemplar WHERE claveejemplar=%d;", fila);
            if(SQLITE_OK != sqlite3_exec(db, consulta, 0, 0, 0)) {
                cout << "Error: " << sqlite3_errmsg(db) << endl;
            }
        }
    }
    cout << "Ejemplar borrado" << endl;
    cin.ignore();
}

void BuscarEjemplar(sqlite3 *db) {
    sqlite3_stmt *ppStmt;
    int rc;
    char consulta[1024];
    char filtro[256];
    char titulo[64];
    char numero1[16];
    char numero2[16];
    int edicion;
    int numeroorden;
    char ubicacion[32];
    char categoria[2];
    int i;

    for(i = 0; i < 22; i++) cout << endl;
    // Búsqueda de editoriales por nombre, direccion o telefono:
    cout << "Introducir cadenas de busqueda, _ para comodin de caracter, % para comodin de cadena" << endl;
    cout << "Dejar en blanco para ignorar el campo en la busqueda" << endl;
    cin.ignore();
    cout << "Titulo: ";
    cin.getline(titulo, 64);
    cout << "Numero de orden: ";
    cin.getline(numero1,15);
    numeroorden = atoi(numero1);
    cout << "Edicion: ";
    cin.getline(numero2,15);
    edicion = atoi(numero2);
    cout << "Ubicacion: ";
    cin.getline(ubicacion,2);
    cout << "Categoria: ";
    cin.getline(categoria,2);

    if(strlen(titulo) == 0) strcpy(titulo, "%");
    if(strlen(ubicacion) == 0) strcpy(ubicacion, "%");
    if(strlen(categoria) == 0) strcpy(categoria, "%");
    sprintf(consulta, "SELECT titulo,numeroorden,edicion,ubicacion,categoria FROM ejemplar NATURAL JOIN libro "
            "WHERE titulo LIKE '%s' AND ubicacion LIKE '%s' AND categoria LIKE '%s'", titulo, ubicacion, categoria);
    if(strlen(numero1) > 0) {
        sprintf(filtro, " AND numeroorden=%d", numeroorden);
        strcat(consulta, filtro);
    }
    if(strlen(numero2) > 0) {
        sprintf(filtro, " AND edicion=%d", edicion);
        strcat(consulta, filtro);
    }
    strcat(consulta, ";");

    rc = sqlite3_prepare_v2(db, consulta, -1, &ppStmt, NULL);
    if( rc!=SQLITE_OK ){
        cout << "Error: " << sqlite3_errmsg(db) << "\n" << consulta << endl;
    } else {
        i = 0;
        while(SQLITE_ROW == sqlite3_step(ppStmt)) {
            cout.setf(ios::left);
            cout.width(64);
            cout << sqlite3_column_text(ppStmt, 0);
            cout.width(13);
            cout << "[" << sqlite3_column_text(ppStmt, 1) << "]" << endl;
            cout << "Ed: " << sqlite3_column_text(ppStmt, 2);
            cout.width(32);
            cout << " Ubic:" << sqlite3_column_text(ppStmt, 3);
            cout.width(2);
            cout << " Cat:" << sqlite3_column_text(ppStmt, 4) << endl;
            i +=2;
            if(i >= 21) {
                cout << "Pulsa return";
                cin.ignore();
                cin.get();
            }
        }
        sqlite3_finalize(ppStmt);
    }
    cin.ignore();
    cin.get();
}