Base De Datos SQLite En Android Con Múltiples Tablas

Anteriormente había creado un tutorial sobre el uso de bases de datos SQLite en Android, donde cubría los aspectos básicos. Sin embargo no expliqué como podrías tratar con múltiples tablas si este caso se te presentara.

Por eso esta vez abordaremos un problema sobre pedidos, productos y clientes el cual contiene varias tablas a relacionar dentro de la base de datos.

Si sigues leyendo, verás cómo:

  • Implementar el CRUD (Create, Read, Update y Delete) de cada entidad propuesta en la base de datos.
  • Usar llaves foráneas y compuestas.
  • Realizar JOINs entre tablas
  • Usar clases de utilidad para SQLite como lo son SQLiteBuilder y DatabaseUtils.

Descargar Proyecto Android Studio De Pedidos

Para desbloquear el link de descarga para el código final del tutorial sigue estas instrucciones:

[sociallocker id=»7121″]Descargar Gratis[/sociallocker]

Base De Datos SQLite De Pedidos

Como te venía diciendo, usaremos uno de los modelos de datos más populares en el mundo del desarrollo de software y las clases universitarias. Se trata de una base de datos que soporta la interacción entre un cliente que realiza pedidos de diferentes productos que posee un establecimiento comercial.

Para llevar a cabo la construcción de la aplicación debes seguir estos pasos:

  1. Diseña modelo de datos para definir la estructura de las tablas y sus relaciones
  2. Crear de nuevo proyecto en Android Studio
    1. Crear clase contrato
    2. Crear manejador SQLiteOpenHelper
    3. Crear clases modelo para las entidades
    4. Generar operaciones de bases de datos necesarias para cada entidad
    5. Probar el funcionamiento

Diseño De Base De Datos Para Pedidos

El diseño de la base de datos no incluye el análisis de requerimientos, ni identificación de temporalidades o especificaciones de triggers, ni levantamiento de restricciones.

Partirás directamente desde el siguiente diagrama entidad-relación:

Nota: Si tienes problemas diseñando bases de datos, te recomiendo mi ebook con un metodología de diseño y plantillas para simplificar el trabajo.

Diagrama ER de pedidos en Android

Este modelo es parecido al que expliqué en el artículo sobre sistemas de facturación, donde de la relación muchos a muchos entre pedidos y productos, se genera una nueva tabla que representa las líneas (detalles, ítems, etc.) del pedido.

El siguiente mini diccionario de datos muestra una explicación de cada tabla y columna:

Entidad Descripción Columnas
CabeceraPedido Representa los datos generales del pedido
  • id: Identificador del pedido
  • id_cliente: Llave foránea de la relación con el cliente
  • fecha: Momento en que se confirmó el pedido
  • id_forma_pago: Llave foránea de la relación con la forma de pago
DetallePedido Es cada uno de los renglones dentro del pedido
  • id_cabecera_pedido: Llave foránea (también es llave parcial) de la relación con CabeceraPedido.
  • secuencia: Llave parcial que indica la posición del detalle en el pedido.
  • id_producto: Llave foránea de la relación con Producto.
  • cantidad: Cantidad de unidades que se compraron de un Producto.
  • precio: Precio del producto en el momento en que se compró.
Producto Artículo que se vende en el establecimiento comercial
  • id: Identificador único del producto.
  • nombre: Nombre descriptivo del producto.
  • precio: Precio actual del producto.
  • existencias: Cantidad de productos que hay en las reservas (stock).
Cliente Persona que acude a comprar productos
  • id: Identificador único del cliente.
  • nombres: Nombre compuesto del cliente.
  • apellidos: Los apellidos del cliente.
  • telefono: Número de teléfono para contactar al cliente.
FormaDePago Cada una de las elecciones financieras que tiene el cliente para pagar un pedido
  •  id: Identificador único de cliente.
  • nombre: Descripción del tipo de pago.

1. Crear Nuevo Proyecto En Android Studio

Si ya entendiste el modelo de datos anterior, entonces es hora de codificar nuestra app Android de ejemplo.

1. Abre Android Studio y crea un nuevo proyecto llamado «Pedidos». Puedes usar tu paquete personalizado, pero si deseas que cada código que ponga en este tutorial funcione correctamente, entonces usa "com.herprogramacion.pedidos".

2. Añade una actividad en blanco llamada ActividadListaPedidos.java y confirma la creación del proyecto.

3. Crea tres nuevos paquetes haciendo click derecho el paquete java com.herprogramacion.pedidos, luego selecciona New y clickea package. Los nombres de los paquetes son «modelo», «sqlite» y «ui».

Android Studio: Crear nuevo paquete Java

Con ello tendrás la siguiente estructura:

Android Studio: Estructura de paquetes Java

2. Crear Clase Contrato

Dentro del paquete sqlite crea una nueva clase llamada ContratoPedidos.java. El propósito de esta es guardar todos los metadatos y componentes auxiliares para definir la estructura general de la base de datos.

Un enfoque práctico para definir las columnas de cada tabla es crear una interfaz por cada una y añadir el nombre estándar. Luego implementas la interfaz en una clase auxiliar que te ayude a la administración de cada tabla.

ContratoPedidos.java

import java.util.UUID;

/**
 * Clase que establece los nombres a usar en la base de datos
 */
public class ContratoPedidos {

    interface ColumnasCabeceraPedido {
        String ID = "id";
        String FECHA = "fecha";
        String ID_CLIENTE = "id_cliente";
        String ID_FORMA_PAGO = "id_forma_pago";
    }

    interface ColumnasDetallePedido {
        String ID = "id";
        String SECUENCIA = "secuencia";
        String ID_PRODUCTO = "id_producto";
        String CANTIDAD = "cantidad";
        String PRECIO = "precio";
    }

    interface ColumnasProducto {
        String ID = "id";
        String NOMBRE = "nombre";
        String PRECIO = "precio";
        String EXISTENCIAS = "existencias";
    }

    interface ColumnasCliente {
        String ID = "id";
        String NOMBRES = "nombres";
        String APELLIDOS = "apellidos";
        String TELEFONO = "telefono";
    }

    interface ColumnasFormaPago {
        String ID = "id";
        String NOMBRE = "nombre";
    }

    public static class CabecerasPedido implements ColumnasCabeceraPedido {
        public static String generarIdCabeceraPedido() {
            return "CP-" + UUID.randomUUID().toString();
        }
    }

    public static class DetallesPedido implements ColumnasDetallePedido {
        // Métodos auxiliares
    }

    public static class Productos implements ColumnasProducto{
        public static String generarIdProducto() {
            return "PRO-" + UUID.randomUUID().toString();
        }
    }

    public static class Clientes implements ColumnasCliente{
        public static String generarIdCliente() {
            return "CLI-" + UUID.randomUUID().toString();
        }
    }

    public static class FormasPago implements ColumnasFormaPago{
        public static String generarIdFormaPago() {
            return "FP-" + UUID.randomUUID().toString();
        }
    }


    private ContratoPedidos() {
    }

}

La mayoría de clases auxiliares tienen un método que genera un identificador basado en la clase UUID, el cuál te servirá al momento de realizar inserciones.

3. Crear Manejador SQLiteOpenHelper

Recuerda que acceder al archivo SQLite que representa tu base de datos requiere de una clase que extienda de SQLiteOpenHelper. En ella debes sobrescribir los controladores onCreate() y onUpgrade() para crear y actualizar tu base de datos.

Al crear este manejador ten en cuenta definir los siguientes ingredientes:

  • El nombre de la base de datos y su versión actual.
  • El nombre de las tablas que crearás.
  • Las referencias de llaves foráneas. Este aspecto solo funciona desde Android Froyo 2.2 con la versión de Sqlite 3.6.19.
  • La sobrescritura del método onOpen() para agregar el soporte de llaves foráneas y las clausulas ON DELETE.
  • Los scripts SQL que usarás para implementar el modelo de datos.
  • Los cambios precisos en las tablas por si deseas actualizar la base de datos.

1. Teniendo en cuenta lo anterior, crea una nueva clase dentro de sqlite llamada BaseDatosPedidos.java y extiéndela de SQLiteOpenHelper.

2. Usa constantes e interfaces para generalizar los elementos descritos arriba con el fin de mejorar la reusabilidad y orden del código.

BaseDatosPedidos.java

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
import android.provider.BaseColumns;

import com.herprogramacion.pedidos.sqlite.ContratoPedidos.CabecerasPedido;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.DetallesPedido;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.Productos;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.Clientes;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.FormasPago;

/**
 * Clase que administra la conexión de la base de datos y su estructuración
 */
public class BaseDatosPedidos extends SQLiteOpenHelper {

    private static final String NOMBRE_BASE_DATOS = "pedidos.db";

    private static final int VERSION_ACTUAL = 1;

    private final Context contexto;

    interface Tablas {
        String CABECERA_PEDIDO = "cabecera_pedido";
        String DETALLE_PEDIDO = "detalle_pedido";
        String PRODUCTO = "producto";
        String CLIENTE = "cliente";
        String FORMA_PAGO = "forma_pago";
    }

    interface Referencias {

        String ID_CABECERA_PEDIDO = String.format("REFERENCES %s(%s) ON DELETE CASCADE",
                Tablas.CABECERA_PEDIDO, CabecerasPedido.ID_CABECERA_PEDIDO);

        String ID_PRODUCTO = String.format("REFERENCES %s(%s)",
                Tablas.PRODUCTO, Productos.ID_PRODUCTO);

        String ID_CLIENTE = String.format("REFERENCES %s(%s)",
                Tablas.CLIENTE, Clientes.ID_CLIENTE);

        String ID_FORMA_PAGO = String.format("REFERENCES %s(%s)",
                Tablas.FORMA_PAGO, FormasPago.ID_FORMA_PAGO);
    }

    public BaseDatosPedidos(Context contexto) {
        super(contexto, NOMBRE_BASE_DATOS, null, VERSION_ACTUAL);
        this.contexto = contexto;
    }

    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);
        if (!db.isReadOnly()) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                db.setForeignKeyConstraintsEnabled(true);
            } else {
                db.execSQL("PRAGMA foreign_keys=ON");
            }
        }
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT," +
                        "%s TEXT UNIQUE NOT NULL,%s DATETIME NOT NULL,%s TEXT NOT NULL %s," +
                        "%s TEXT NOT NULL %s)",
                Tablas.CABECERA_PEDIDO, BaseColumns._ID,
                CabecerasPedido.ID_CABECERA_PEDIDO, CabecerasPedido.FECHA,
                CabecerasPedido.ID_CLIENTE, Referencias.ID_CLIENTE,
                CabecerasPedido.ID_FORMA_PAGO, Referencias.ID_FORMA_PAGO));

        db.execSQL(String.format("CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT," +
                        "%s TEXT NOT NULL %s,%s INTEGER NOT NULL CHECK (%s>0),%s INTEGER NOT NULL %s," +
                        "%s INTEGER NOT NULL,%s REAL NOT NULL,UNIQUE (%s,%s) )",
                Tablas.DETALLE_PEDIDO, BaseColumns._ID,
                DetallesPedido.ID_CABECERA_PEDIDO, Referencias.ID_CABECERA_PEDIDO,
                DetallesPedido.SECUENCIA, DetallesPedido.SECUENCIA,
                DetallesPedido.ID_PRODUCTO, Referencias.ID_PRODUCTO,
                DetallesPedido.CANTIDAD, DetallesPedido.PRECIO,
                DetallesPedido.ID_CABECERA_PEDIDO, DetallesPedido.SECUENCIA));

        db.execSQL(String.format("CREATE TABLE %s ( %s INTEGER PRIMARY KEY AUTOINCREMENT," +
                        "%s TEXT NOT NULL UNIQUE,%s TEXT NOT NULL,%s REAL NOT NULL," +
                        "%s INTEGER NOT NULL CHECK(%s>=0) )",
                Tablas.PRODUCTO, BaseColumns._ID,
                Productos.ID_PRODUCTO, Productos.NOMBRE, Productos.PRECIO,
                Productos.EXISTENCIAS, Productos.EXISTENCIAS));

        db.execSQL(String.format("CREATE TABLE %s ( %s INTEGER PRIMARY KEY AUTOINCREMENT," +
                        "%s TEXT NOT NULL UNIQUE,%s TEXT NOT NULL,%s TEXT NOT NULL,%s )",
                Tablas.CLIENTE, BaseColumns._ID,
                Clientes.ID_CLIENTE, Clientes.NOMBRES, Clientes.APELLIDOS, Clientes.TELEFONO));

        db.execSQL(String.format("CREATE TABLE %s ( %s INTEGER PRIMARY KEY AUTOINCREMENT," +
                        "%s TEXT NOT NULL UNIQUE,%s TEXT NOT NULL )",
                Tablas.FORMA_PAGO, BaseColumns._ID,
                FormasPago.ID_FORMA_PAGO, FormasPago.NOMBRE));

    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        db.execSQL("DROP TABLE IF EXISTS " + Tablas.CABECERA_PEDIDO);
        db.execSQL("DROP TABLE IF EXISTS " + Tablas.DETALLE_PEDIDO);
        db.execSQL("DROP TABLE IF EXISTS " + Tablas.PRODUCTO);
        db.execSQL("DROP TABLE IF EXISTS " + Tablas.CLIENTE);
        db.execSQL("DROP TABLE IF EXISTS " + Tablas.FORMA_PAGO);

        onCreate(db);
    }


}

Me parece importante destacar el uso de las llaves foráneas a través de la sentencia REFERENCES. La sintaxis funciona como índice al final del comando CREATE:

FOREIGN KEY(columna) REFERENCES otra_tabla(columna_otra_tabla)

o en línea:

columna REFERENCES otra_tabla(columna_otra_tabla)

También puedes agregar la cláusula ON DELETE CASCADE para propagar la eliminación en cascada de los detalles que estén relacionados con una cabecera que se eliminó.

Ahora para el caso de 'detalle_pedido' se usa una llave primaria compuesta. Debido a que '_id' es la clave principal, debes usar la cláusula UNIQUE para satisfacer el rol de índice único. Por lo que puedes especificar ambas columnas así:

UNIQUE (id_cabecera_pedido, secuencia)

Para hacer efectiva las referencias de integridad de las llaves foráneas en SQLite es necesario usar el método setForeignKeyConstraintsEnabled() pasando el valor true dentro de onOpen(). Este método va desde Android Jelly Bean en adelante.

db.setForeignKeyConstraintsEnabled(true);

Para el soporte, puedes habilitar manualmente a través de la sentencia PRAGMA foreign_keys=ON.

db.execSQL("PRAGMA foreign_kets=ON");

4. Crear Modelos Para La Base De Datos

Crear clases modelo permite representar los registros de la base de datos como entidades dentro de la app Android que pueden ayudarte a procesar eventos desde la vista.

1. El primer modelo será el de la cabecera del pedido. Así que crea una nueva clase llamada DetallePedido.java dentro del paquete modelo y agrega los atributos de la siguiente forma.

public class CabeceraPedido {

    public String idCabeceraPedido;

    public String fecha;

    public String idCliente;

    public String idFormaPago;

    public CabeceraPedido(String idCabeceraPedido, String fecha, 
                          String idCliente, String idFormaPago) {
        this.idCabeceraPedido = idCabeceraPedido;
        this.fecha = fecha;
        this.idCliente = idCliente;
        this.idFormaPago = idFormaPago;
    }
}

2. De la misma forma crea una clase con el nombre de DetallePedido.java y escribe los atributos:

DetallePedido.java

public class DetallePedido {

    public String idCabeceraPedido;

    public int secuencia;

    public String idProducto;

    public int cantidad;

    public float precio;

    public DetallePedido(String idCabeceraPedido, int secuencia, 
                         String idProducto, int cantidad, float precio) {
        this.idCabeceraPedido = idCabeceraPedido;
        this.secuencia = secuencia;
        this.idProducto = idProducto;
        this.cantidad = cantidad;
        this.precio = precio;
    }
}

3. Crea una clase llamada Producto.java y añade los atributos visto en el modelo de datos:

Producto.java

public class Producto {

    public String idProducto;

    public String nombre;

    public float precio;

    public int existencias;

    public Producto(String idProducto, String nombre, float precio, int existencias) {
        this.idProducto = idProducto;
        this.nombre = nombre;
        this.precio = precio;
        this.existencias = existencias;
    }
}

4. Ahora es el turno para los clientes. Añade una clase nueva con el nombre de Cliente.java e inserta los respectivos atributos.

Cliente.java

public class Cliente {

    public String idCliente;

    public String nombres;

    public String apellidos;

    public String telefono;

    public Cliente(String idCliente, String nombres, String apellidos, String telefono) {
        this.idCliente = idCliente;
        this.nombres = nombres;
        this.apellidos = apellidos;
        this.telefono = telefono;
    }
}

5. Finalmente termina con la clase FormaPago.java. Recuerda que si necesitas depurar tus elementos puedes sobrescribir el método toString().

FormaPago.java

public class FormaPago {

    public String idFormaPago;

    public String nombre;

    public FormaPago(String idFormaPago, String nombre) {
        this.idFormaPago = idFormaPago;
        this.nombre = nombre;
    }
}

5. CRUD Para La Base De Datos

Ahora es el turno de generar operaciones de lectura, inserción, modificación y eliminación por cada uno de las tablas que tenemos.

Todas las operaciones serán ejecutadas desde una clase auxiliar a la cuál denominarás OperacionesBaseDatos.java. Esta se definirá bajo un patrón singleton que permita propagar el uso de los métodos a través de una sola instancia a lo largo de la app.

5.1 Patrón Singleton Para SQLite

Crea una nueva clase llamada OperacionesBaseDatos.java e implementa un patrón singleton. Esto significa, poner su constructor principal como privado, definir un miembro estático de la clase y generar un método estático que permita la obtención del único miembro.

OperacionesBaseDatos.java

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.herprogramacion.pedidos.modelo.CabeceraPedido;
import com.herprogramacion.pedidos.modelo.Cliente;
import com.herprogramacion.pedidos.modelo.DetallePedido;
import com.herprogramacion.pedidos.modelo.FormaPago;
import com.herprogramacion.pedidos.modelo.Producto;
import com.herprogramacion.pedidos.sqlite.BaseDatosPedidos.Tablas;

/**
 * Clase auxiliar que implementa a {@link BaseDatosPedidos} para llevar a cabo el CRUD
 * sobre las entidades existentes.
 */
public final class OperacionesBaseDatos {

    private static BaseDatosPedidos baseDatos;

    private static OperacionesBaseDatos instancia = new OperacionesBaseDatos();

    private OperacionesBaseDatos() {
    }

    public static OperacionesBaseDatos obtenerInstancia(Context contexto) {
        if (baseDatos == null) {
            baseDatos = new BaseDatosPedidos(contexto);
        }
        return instancia;
    }
}

5.2 CRUD cabeceras de pedido

1. El siguiente método obtendrá todas las cabeceras de pedidos que existan en la base de datos, haciendo un INNER JOIN con 'cliente' y 'forma_pago'. El valor de retorno será Cursor.

public Cursor obtenerCabecerasPedidos() {
        SQLiteDatabase db = baseDatos.getReadableDatabase();

        SQLiteQueryBuilder builder = new SQLiteQueryBuilder();

        builder.setTables(CABECERA_PEDIDO_JOIN_CLIENTE_Y_FORMA_PAGO);

        return builder.query(db, proyCabeceraPedido, null, null, null, null, null);
    }

  private static final String CABECERA_PEDIDO_JOIN_CLIENTE_Y_FORMA_PAGO = "cabecera_pedido " +
            "INNER JOIN cliente " +
            "ON cabecera_pedido.id_cliente = cliente.id " +
            "INNER JOIN forma_pago " +
            "ON cabecera_pedido.id_forma_pago = forma_pago.id";


    private final String[] proyCabeceraPedido = new String[]{
            Tablas.CABECERA_PEDIDO + "." + CabecerasPedido.ID,
            CabecerasPedido.FECHA,
            Clientes.NOMBRES,
            Clientes.APELLIDOS,
            FormasPago.NOMBRE};

Usa clase SQLiteQueryBuilder para fabricar consultas complejas que impliquen joins o uniones. El método setTablets() te permite asociar las tablas necesarias a consultar.

También se liga una proyección de columnas para determinar los datos necesarios a consultar. Recuerda que cómo varias tablas tienen el campo 'id', es necesario anteponer un alias o el mismo nombre de la tabla para no caer en ambigüedades. Por eso la primera columna de la proyección es 'cabecera_pedido.id'.

2. Obtener cabecera de pedido por identificador — Este método es similar al anterior, solo que esta vez se liga una sentencia WHERE para seleccionar solo la cabecera de pedido que tenga el id que viene por parámetro.

public Cursor obtenerCabeceraPorId(String id) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    String selection = String.format("%s=?", CabecerasPedido.ID);
    String[] selectionArgs = {id};

    SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
    builder.setTables(CABECERA_PEDIDO_JOIN_CLIENTE_Y_FORMA_PAGO);

    String[] proyeccion = {
            Tablas.CABECERA_PEDIDO + "." + CabecerasPedido.ID,
            CabecerasPedido.FECHA,
            Clientes.NOMBRES,
            Clientes.APELLIDOS,
            FormasPago.NOMBRE};

    return builder.query(db, proyeccion, selection, selectionArgs, null, null, null);
}

3. Insertar cabecera de pedido — Genera un identificador para el pedido, crea una estructura ContentValues para albergar los datos y luego aplica la inserción con insertOrThrow().

public String insertarCabeceraPedido(CabeceraPedido pedido) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    // Generar Pk
    String idCabeceraPedido = CabecerasPedido.generarIdCabeceraPedido();

    ContentValues valores = new ContentValues();
    valores.put(CabecerasPedido.ID_CABECERA_PEDIDO, idCabeceraPedido);
    valores.put(CabecerasPedido.FECHA, pedido.fecha);
    valores.put(CabecerasPedido.ID_CLIENTE, pedido.idCliente);
    valores.put(CabecerasPedido.ID_FORMA_PAGO, pedido.idFormaPago);

    // Insertar cabecera
    db.insertOrThrow(Tablas.CABECERA_PEDIDO, null, valores);

    return idCabeceraPedido;
}

3. Actualizar cabecera de pedido — El siguiente método actualiza los datos de una cabecera cuyo identificador sea igual al entrante. El proceso es similar a la inserción, solo que esta vez debes generar la cláusula de la condición más un parámetro.

public boolean actualizarCabeceraPedido(CabeceraPedido pedidoNuevo) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    ContentValues valores = new ContentValues();
    valores.put(CabecerasPedido.FECHA, pedidoNuevo.fecha);
    valores.put(CabecerasPedido.ID_CLIENTE, pedidoNuevo.idCliente);
    valores.put(CabecerasPedido.ID_FORMA_PAGO, pedidoNuevo.idFormaPago);

    String whereClause = String.format("%s=?", CabecerasPedido.ID_CABECERA_PEDIDO);
    String[] whereArgs = {pedidoNuevo.idCabeceraPedido};

    int resultado = db.update(Tablas.CABECERA_PEDIDO, valores, whereClause, whereArgs);

    return resultado > 0;
}

4. Eliminar cabecera de pedido — Usa el método delete() para remover una cabecera de pedido, donde la condición se especifica por el identificador que entra como parámetro al método.

public boolean eliminarCabeceraPedido(String idCabeceraPedido) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    String whereClause = CabecerasPedido.ID_CABECERA_PEDIDO + "=?";
    String[] whereArgs = {idCabeceraPedido};

    int resultado = db.delete(Tablas.CABECERA_PEDIDO, whereClause, whereArgs);

    return resultado > 0;
}

No olvides que al eliminar una cabecera, todos sus detalles desaparecerán, debido al activador ON DELETE que pusiste en la creación de la tabla.

5.3 CRUD para detalles de pedido

1. Obtener detalles de pedido — El siguiente método obtendrá todos los detalles de un pedido determinado por el parámetro idDetallePedido.

public Cursor obtenerDetallesPorIdPedido(String idCabeceraPedido) {
    SQLiteDatabase db = baseDatos.getReadableDatabase();

    String sql = String.format("SELECT * FROM %s WHERE %s=?",
            Tablas.DETALLE_PEDIDO, CabecerasPedido.ID_CABECERA_PEDIDO);

    String[] selectionArgs = {idCabeceraPedido};

    return db.rawQuery(sql, selectionArgs);

}

2. Insertar detalle de pedido — Este método inserta un detalle de pedido a través de una instancia DetallePedido. El retorno es la llave compuesta del nuevo detalle de forma 'idCabeceraPedido#secuencia'.

public String insertarDetallePedido(DetallePedido detalle) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    ContentValues valores = new ContentValues();
    valores.put(DetallesPedido.ID_CABECERA_PEDIDO, detalle.idCabeceraPedido);
    valores.put(DetallesPedido.SECUENCIA, detalle.secuencia);
    valores.put(DetallesPedido.ID_PRODUCTO, detalle.idProducto);
    valores.put(DetallesPedido.CANTIDAD, detalle.cantidad);
    valores.put(DetallesPedido.PRECIO, detalle.precio);
    
    db.insertOrThrow(Tablas.DETALLE_PEDIDO, null, valores);
    
    return String.format("%s#%d", detalle.idCabeceraPedido, detalle.secuencia);

}

3. Actualizar un detalle de pedido — Se actualiza el detalle a través de nuevos valores que vienen en una instancia DetallePedido. Esta solo se lleva a cabo si el registro tiene el mismo id de cabecera y número de secuencia.

public boolean actualizarDetallePedido(DetallePedido detalle) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    ContentValues valores = new ContentValues();
    valores.put(DetallesPedido.SECUENCIA, detalle.secuencia);
    valores.put(DetallesPedido.CANTIDAD, detalle.cantidad);
    valores.put(DetallesPedido.PRECIO, detalle.precio);

    String selection = String.format("%s=? AND %s=?",
            DetallesPedido.ID_CABECERA_PEDIDO, DetallesPedido.SECUENCIA);
    final String[] whereArgs = {detalle.idCabeceraPedido, String.valueOf(detalle.secuencia)};

    int resultado = db.update(Tablas.DETALLE_PEDIDO, valores, selection, whereArgs);

    return resultado > 0;
}

4. Eliminar un detalle de pedido — Se elimina un registro de la tabla detalle_pedido cuyo id_cabecera_pedido y secuencia concuerden con los parámetros.

public boolean eliminarDetallePedido(String idCabeceraPedido, int secuencia) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    String selection = String.format("%s=? AND %s=?",
            DetallesPedido.ID_CABECERA_PEDIDO, DetallesPedido.SECUENCIA);
    String[] whereArgs = {idCabeceraPedido, String.valueOf(secuencia)};

    int resultado = db.delete(Tablas.DETALLE_PEDIDO, selection, whereArgs);

    return resultado > 0;
}

5.4 CRUD de las tablas restantes

Debido a que los demás recursos siguen el mismo patrón para las operaciones, obviaré las explicaciones separadas. Intenta crearlas por ti mismo como práctica para interiorizar los métodos relacionados con bases de datos.

A continuación te dejo la implementación de los recursos faltantes:

// [OPERACIONES_PRODUCTO]
public Cursor obtenerProductos() {
    SQLiteDatabase db = baseDatos.getReadableDatabase();

    String sql = String.format("SELECT * FROM %s", Tablas.PRODUCTO);

    return db.rawQuery(sql, null);
}

public String insertarProducto(Producto producto) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    ContentValues valores = new ContentValues();
    // Generar Pk
    String idProducto = Productos.generarIdProducto();
    valores.put(Productos.ID, idProducto);
    valores.put(Productos.NOMBRE, producto.nombre);
    valores.put(Productos.PRECIO, producto.precio);
    valores.put(Productos.EXISTENCIAS, producto.existencias);

    db.insertOrThrow(Tablas.PRODUCTO, null, valores);

    return idProducto;

}

public boolean actualizarProducto(Producto producto) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    ContentValues valores = new ContentValues();
    valores.put(Productos.NOMBRE, producto.nombre);
    valores.put(Productos.PRECIO, producto.precio);
    valores.put(Productos.EXISTENCIAS, producto.existencias);

    String whereClause = String.format("%s=?", Productos.ID);
    String[] whereArgs = {producto.idProducto};

    int resultado = db.update(Tablas.PRODUCTO, valores, whereClause, whereArgs);

    return resultado > 0;
}

public boolean eliminarProducto(String idProducto) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    String whereClause = String.format("%s=?", Productos.ID);
    String[] whereArgs = {idProducto};

    int resultado = db.delete(Tablas.PRODUCTO, whereClause, whereArgs);

    return resultado > 0;
}
// [/OPERACIONES_PRODUCTO]

// [OPERACIONES_CLIENTE]
public Cursor obtenerClientes() {
    SQLiteDatabase db = baseDatos.getReadableDatabase();

    String sql = String.format("SELECT * FROM %s", Tablas.CLIENTE);

    return db.rawQuery(sql, null);
}

public String insertarCliente(Cliente cliente) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    // Generar Pk
    String idCliente = Clientes.generarIdCliente();

    ContentValues valores = new ContentValues();
    valores.put(Clientes.ID, idCliente);
    valores.put(Clientes.NOMBRES, cliente.nombres);
    valores.put(Clientes.APELLIDOS, cliente.apellidos);
    valores.put(Clientes.TELEFONO, cliente.telefono);

    return db.insertOrThrow(Tablas.CLIENTE, null, valores) > 0 ? idCliente : null;
}

public boolean actualizarCliente(Cliente cliente) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    ContentValues valores = new ContentValues();
    valores.put(Clientes.NOMBRES, cliente.nombres);
    valores.put(Clientes.APELLIDOS, cliente.apellidos);
    valores.put(Clientes.TELEFONO, cliente.telefono);

    String whereClause = String.format("%s=?", Clientes.ID);
    final String[] whereArgs = {cliente.idCliente};

    int resultado = db.update(Tablas.CLIENTE, valores, whereClause, whereArgs);

    return resultado > 0;
}

public boolean eliminarCliente(String idCliente) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    String whereClause = String.format("%s=?", Clientes.ID);
    final String[] whereArgs = {idCliente};

    int resultado = db.delete(Tablas.CLIENTE, whereClause, whereArgs);

    return resultado > 0;
}
// [/OPERACIONES_CLIENTE]

// [OPERACIONES_FORMA_PAGO]
public Cursor obtenerFormasPago() {
    SQLiteDatabase db = baseDatos.getReadableDatabase();

    String sql = String.format("SELECT * FROM %s", Tablas.FORMA_PAGO);

    return db.rawQuery(sql, null);
}

public String insertarFormaPago(FormaPago formaPago) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    // Generar Pk
    String idFormaPago = FormasPago.generarIdFormaPago();

    ContentValues valores = new ContentValues();
    valores.put(FormasPago.ID, idFormaPago);
    valores.put(FormasPago.NOMBRE, formaPago.nombre);

    return db.insertOrThrow(Tablas.FORMA_PAGO, null, valores) > 0 ? idFormaPago : null;
}

public boolean actualizarFormaPago(FormaPago formaPago) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    ContentValues valores = new ContentValues();
    valores.put(FormasPago.NOMBRE, formaPago.nombre);

    String whereClause = String.format("%s=?", FormasPago.ID);
    String[] whereArgs = {formaPago.idFormaPago};

    int resultado = db.update(Tablas.FORMA_PAGO, valores, whereClause, whereArgs);

    return resultado > 0;
}

public boolean eliminarFormaPago(String idFormaPago) {
    SQLiteDatabase db = baseDatos.getWritableDatabase();

    String whereClause = String.format("%s=?", FormasPago.ID);
    String[] whereArgs = {idFormaPago};

    int resultado = db.delete(Tablas.FORMA_PAGO, whereClause, whereArgs);

    return resultado > 0;
}

public SQLiteDatabase getDb() {
    return baseDatos.getWritableDatabase();
}


// [/OPERACIONES_FORMA_PAGO]

Probar Base De Datos SQLite

Abre ActividadListaPedidos.java y crea una tarea asíncrona interna llamada TareaPruebaDatos. El objetivo es ejecutar las operaciones de base de datos dentro de esta para no entorpecer el hilo de UI.

ActividadListaPedidos.java

import android.database.DatabaseUtils;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;

import com.herprogramacion.pedidos.R;
import com.herprogramacion.pedidos.modelo.CabeceraPedido;
import com.herprogramacion.pedidos.modelo.Cliente;
import com.herprogramacion.pedidos.modelo.DetallePedido;
import com.herprogramacion.pedidos.modelo.FormaPago;
import com.herprogramacion.pedidos.modelo.Producto;
import com.herprogramacion.pedidos.sqlite.OperacionesBaseDatos;

import java.util.Calendar;

public class ActividadListaPedidos extends AppCompatActivity {

    OperacionesBaseDatos datos;

    public class TareaPruebaDatos extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... params) {

            // [INSERCIONES]
            String fechaActual = Calendar.getInstance().getTime().toString();

            try {

                datos.getDb().beginTransaction();

                // Inserción Clientes
                String cliente1 = datos.insertarCliente(new Cliente(null, "Veronica", "Del Topo", "4552000"));
                String cliente2 = datos.insertarCliente(new Cliente(null, "Carlos", "Villagran", "4440000"));

                // Inserción Formas de pago
                String formaPago1 = datos.insertarFormaPago(new FormaPago(null, "Efectivo"));
                String formaPago2 = datos.insertarFormaPago(new FormaPago(null, "Crédito"));

                // Inserción Productos
                String producto1 = datos.insertarProducto(new Producto(null, "Manzana unidad", 2, 100));
                String producto2 = datos.insertarProducto(new Producto(null, "Pera unidad", 3, 230));
                String producto3 = datos.insertarProducto(new Producto(null, "Guayaba unidad", 5, 55));
                String producto4 = datos.insertarProducto(new Producto(null, "Maní unidad", 3.6f, 60));

                // Inserción Pedidos
                String pedido1 = datos.insertarCabeceraPedido(
                        new CabeceraPedido(null, fechaActual, cliente1, formaPago1));
                String pedido2 = datos.insertarCabeceraPedido(
                        new CabeceraPedido(null, fechaActual,cliente2, formaPago2));

                // Inserción Detalles
                datos.insertarDetallePedido(new DetallePedido(pedido1, 1, producto1, 5, 2));
                datos.insertarDetallePedido(new DetallePedido(pedido1, 2, producto2, 10, 3));
                datos.insertarDetallePedido(new DetallePedido(pedido2, 1, producto3, 30, 5));
                datos.insertarDetallePedido(new DetallePedido(pedido2, 2, producto4, 20, 3.6f));

                // Eliminación Pedido
                datos.eliminarCabeceraPedido(pedido1);

                // Actualización Cliente
                datos.actualizarCliente(new Cliente(cliente2, "Carlos Alberto", "Villagran", "3333333"));

                datos.getDb().setTransactionSuccessful();
            } finally {
                datos.getDb().endTransaction();
            }

            // [QUERIES]
            Log.d("Clientes","Clientes");
            DatabaseUtils.dumpCursor(datos.obtenerClientes());
            Log.d("Formas de pago", "Formas de pago");
            DatabaseUtils.dumpCursor(datos.obtenerFormasPago());
            Log.d("Productos", "Productos");
            DatabaseUtils.dumpCursor(datos.obtenerProductos());
            Log.d("Cabeceras de pedido", "Cabeceras de pedido");
            DatabaseUtils.dumpCursor(datos.obtenerCabecerasPedidos());
            Log.d("Detalles de pedido", "Detalles de pedido");
            DatabaseUtils.dumpCursor(datos.obtenerDetallesPedido());

            return null;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.actividad_lista_pedidos);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        getApplicationContext().deleteDatabase("pedidos.db");
        datos = OperacionesBaseDatos
                .obtenerInstancia(getApplicationContext());

        new TareaPruebaDatos().execute();
    }

}

Lo primero que debes hacer es obtener la instancia de OperacionesBaseDatos. Luego dentro de doInBackground() incluye todas las operaciones que vayas a probar dentro de una transacción.

La transacción la inicias con el método beginTransaction(). Luego marcas que fue exitosa con el método setTransactionSuccessful(). Si todo salió bien, la transacción se termina con endTrasaction() y se confirmarán los cambios definitivamente, de lo contrario habrá un rollback para que ninguna operación se haga efectiva.

La clase DatabaseUtils es para concatenar sentencias, ligar parámetros a comandos, construir condiciones para la cláusula WHERE, etc. En mi caso usé le método dumbCursor() para mostrar de forma presentable las filas de los cursores que vaya a loguear.

Por ejemplo, las filas de los clientes insertados arriba se ven así:

DatabaseUtils Android: Log de dumbCursor()

Lo importante es que las pruebas que vayas a realizar se encuentren en la tarea asíncrona y luego loguees el estado de las tablas para observar los cambios realizados.

Conclusión

Con este tutorial ya aprendiste algunas ideas para relacionar varias tablas dentro de una base de datos SQLite y cómo resolver situaciones comunes cómo: llaves compuestas, llaves foráneas, joins, eliminaciones en cascada, etc.

Viste que es posible controlar las referencias de integridad a través de la sentencia REFERENCES para que SQLite maneje por ti las restricciones de integridad.

Solo falta una aplicación que haga uso de estas explicaciones, sin embargo primero quiero explicarte cómo implementar esta base de datos en un Content Provider con múltiples tablas y luego crear la aplicación de pedidos formalmente.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!