ContentProvider Con Múltiples Tablas En Android

En el artículo de una base de datos SQLite con múltiples tablas viste un ejemplo sobre pedidos, donde se probaban varias operaciones de datos. En este presente tutorial implementarás el mismo problema solo que usando un ContentProvider con múltiples tablas.

Debido a que ya tienes el modelo de datos definido, es posible actualizar la aplicación Pedidos para implementar un content provider sobre la base de datos.

Verás cómo generar las uris de contenido para cada entidad y cómo implementar el CRUD para cada una.

Descargar Proyecto En Android Studio De Pedidos

Si deseas desbloquear el link de descarga para el código final de la aplicación que resultará de este tutorial, entonces sigue estas instrucciones:

 

ContentProvider Para Pedidos

Crear un content provider personalizado requiere de una clase contrato, un auxiliar SQLiteOpenHelper y un componente tipo ContenProvider que implemente los métodos necesarios para operar los recursos de la base de datos.

Básicamente la receta es la siguiente:

  1. Definir uris de contenido
  2. Declarar tipos MIME
  3. Crear subclase de ContentProvider
  4. Calificar Uris Con UriMatcher
  5. Implementar métodos de cabeceras de pedido
  6. Implementar métodos de detalles de pedido
  7. Implementar métodos de productos
  8. Implementar métodos de clientes
  9. Implementar métodos de formas de pago

Te dejo el diagrama entidad-relación sobre pedidos que se vio en el artículo de SQLite con varias tablas. Pero si aún no conoces los detalles, te recomiendo que lo leas.

Diagrama ER de pedidos en Android

1. Definir Uris De Contenido Del Provider

La clase ContratoPedidos es un buen espacio para declarar las uris de contenido de cada recurso. Antes de codificar la solución es necesario que determines las acciones que deseas realizar sobre tus entidades.

La siguiente tabla detalla las operaciones que usaré para este ejemplo:

Uri Parámetros Descripción
/cabeceras_pedidos filtro: Organiza las cabeceras por el filtro establecido.

Valores ejemplo: "cliente", "total", "fecha"

Colección de pedidos
/cabeceras_pedidos/* Pedido por id
/cabeceras_pedidos/*/detalles Todos los detalles de un pedido especificado por su id
/detalles_pedido Colección para detalles de pedido
/detalles_pedido/* Pedido por id
/productos Colección de productos
/productos/* Producto por id
/clientes Colección de clientes
/clientes/* Cliente por id
/formas_pago Colección de formas de pago
/formas_pago/* Formas de pago por id

Con este esquema claro puedes agregar las uris de contenido a las clases auxiliares de los recursos. Para ello crea una uri base dentro de ContratoPedidos.java para conjugar las demás.

public static final String AUTORIDAD_CONTENIDO = "com.herprogramacion.pedidos";

public static final Uri URI_BASE = Uri.parse("content://" + AUTORIDAD_CONTENIDO);

private static final String RUTA_PEDIDOS = "cabeceras_pedidos";
private static final String RUTA_DETALLES_PEDIDO = "detalles_pedido";
private static final String RUTA_PRODUCTOS = "productos";
private static final String RUTA_CLIENTES = "clientes";
private static final String RUTA_FORMAS_PAGO = "formas_pago";

Luego expande las clases auxiliares que tenías creadas desde el código pasado con un campo llamado URI_CONTENIDO. Adiciona también algunos métodos para el manejo de uris:

public static class CabecerasPedido implements ColumnasCabeceraPedido {

    public static final Uri URI_CONTENIDO =
            URI_BASE.buildUpon().appendPath(RUTA_CABECERAS_PEDIDOS).build();

    public static final String PARAMETRO_FILTRO = "filtro";
    public static final String FILTRO_CLIENTE = "cliente";
    public static final String FILTRO_TOTAL = "total";
    public static final String FILTRO_FECHA = "fecha";

    public static String obtenerIdCabeceraPedido(Uri uri) {
        return uri.getPathSegments().get(1);
    }

    public static Uri crearUriCabeceraPedido(String id) {
        return URI_CONTENIDO.buildUpon().appendPath(id).build();
    }

    public static Uri crearUriParaDetalles(String id) {
        return URI_CONTENIDO.buildUpon().appendPath(id).appendPath("detalles").build();
    }

    public static boolean tieneFiltro(Uri uri) {
        return uri != null && uri.getQueryParameter(PARAMETRO_FILTRO) != null;
    }

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

public static class DetallesPedido implements ColumnasDetallePedido {
    public static final Uri URI_CONTENIDO =
            URI_BASE.buildUpon().appendPath(RUTA_DETALLES_PEDIDO).build();

    public static Uri crearUriDetallePedido(String id, String secuencia) {
        // Uri de la forma 'detalles_pedido/:id#:secuencia'
        return URI_CONTENIDO.buildUpon()
                .appendPath(String.format("%s#%s", id, secuencia))
                .build();
    }

    public static String[] obtenerIdDetalle(Uri uri) {
        return uri.getLastPathSegment().split("#");
    }
}

public static class Productos implements ColumnasProducto {
    public static final Uri URI_CONTENIDO =
            URI_BASE.buildUpon().appendPath(RUTA_PRODUCTOS).build();

    public static Uri crearUriProducto(String id) {
        return URI_CONTENIDO.buildUpon().appendPath(id).build();
    }

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

    public static String obtenerIdProducto(Uri uri) {
        return uri.getLastPathSegment();
    }
}

public static class Clientes implements ColumnasCliente {
    public static final Uri URI_CONTENIDO =
            URI_BASE.buildUpon().appendPath(RUTA_CLIENTES).build();

    public static Uri crearUriCliente(String id) {
        return URI_CONTENIDO.buildUpon().appendPath(id).build();
    }

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

    public static String obtenerIdCliente(Uri uri) {
        return uri.getLastPathSegment();
    }
}

public static class FormasPago implements ColumnasFormaPago {
    public static final Uri URI_CONTENIDO =
            URI_BASE.buildUpon().appendPath(RUTA_FORMAS_PAGO).build();

    public static Uri crearUriFormaPago(String id) {
        return URI_CONTENIDO.buildUpon().appendPath(id).build();
    }

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

    public static String obtenerIdFormaPago(Uri uri) {
        return uri.getPathSegments().get(1);
    }
}

Cada clase tiene un método para crear las uris al estilo crearUri*(). Esto te facilita la obtención de las rutas en cualquier parte de la app.

En el caso de las cabeceras de pedido, es posible ligar un parámetro a la uri para filtrar los resultados. Esto se logra a través del método appendQueryParameter().Un ejemplo de uri para filtrar los pedidos por fecha sería el siguiente:

content://com.herprogramacion.pedidos/pedidos?filtro=fecha

2. Declarar Tipos MIME

El content provider requiere la especificación del tipo MIME correspondiente por cada recurso a operar. Usa los tipos estándar de Android para crear tipos base que permitan crear los particulares.

public static final String BASE_CONTENIDOS = "pedidos.";

public static final String TIPO_CONTENIDO = "vnd.android.cursor.dir/vnd."
        + BASE_CONTENIDOS;

public static final String TIPO_CONTENIDO_ITEM = "vnd.android.cursor.item/vnd."
        + BASE_CONTENIDOS;
public static String generarMime(String id) {
    if (id != null) {
        return TIPO_CONTENIDO + id;
    } else {
        return null;
    }
}

public static String generarMimeItem(String id) {
    if (id != null) {
        return TIPO_CONTENIDO_ITEM + id;
    } else {
        return null;
    }
}

El método generarMime() crea un tipo para las colecciones de recursos y generarMimeItem() crea un tipo para ítems individuales.

3. Crear Subclase De ContentProvider

Ve al paquete sqlite y presiona click derecho para desplegar el menú contextual de creaciones. Para crear un content provider selecciona New > Other > Content Provider.

Android Studio: New > Other > Content Provider

Ahora pon el nombre de ProviderPedidos en la ventana auxiliar que se desplegó e indica que la autoridad será com.herprogramacion.pedidos.

Android Studio: Template Content Provider

Cuando presiones Finish, se creará un nuevo content provider y automáticamente se registrará el componente en el AndroidManifest.xml.

Todos los métodos necesarios de la clase ContentProvider serán sobrescritos con el mínimo de funcionamiento para que tú incluyas el código necesario.

Por el momento declara dos campos para el helper BaseDatosPedidos y el ContentResolver. Luego en onCreate() obtén una instancia para cada uno.

ProviderPedidos.java

import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;

public class ProviderPedidos extends ContentProvider {

    private BaseDatosPedidos bd;

    private ContentResolver resolver;

    public ProviderPedidos() {
    }

    @Override
    public boolean onCreate() {
        bd = new BaseDatosPedidos(getContext());
        resolver = getContext().getContentResolver();
        return true;
    }
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // Implement this to handle requests to delete one or more rows.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public String getType(Uri uri) {
        // TODO: Implement this to handle requests for the MIME type of the data
        // at the given URI.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // TODO: Implement this to handle requests to insert a new row.
        throw new UnsupportedOperationException("Not yet implemented");
    }


    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
                throw new UnsupportedOperationException("Uri no soportada");  

    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        // TODO: Implement this to handle requests to update one or more rows.
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

4. Calificar Uris Con La Clase UriMatcher

La clase UriMatcher te permite asignar un identificador entero a cada patrón de uri que tengas en tu esquema. Para ello primero designa un número a cada recurso y sus variaciones. En mi caso las cosas quedaron así:

  • Cabeceras de pedido: 100
  • Detalles de pedido : 200
  • Productos : 300
  • Clientes : 400
  • Formas de pago : 500

Ahora crea los matchs dentro del provider:

public static final UriMatcher uriMatcher;

// Casos
public static final int CABECERAS_PEDIDOS = 100;
public static final int CABECERAS_PEDIDOS_ID = 101;
public static final int CABECERAS_ID_DETALLES = 102;

public static final int DETALLES_PEDIDOS = 200;
public static final int DETALLES_PEDIDOS_ID = 201;

public static final int PRODUCTOS = 300;
public static final int PRODUCTOS_ID = 301;

public static final int CLIENTES = 400;
public static final int CLIENTES_ID = 401;

public static final int FORMAS_PAGO = 500;
public static final int FORMAS_PAGO_ID = 501;

public static final String AUTORIDAD = "com.herprogramacion.pedidos";

static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    uriMatcher.addURI(AUTORIDAD, "cabeceras_pedidos", CABECERAS_PEDIDOS);
    uriMatcher.addURI(AUTORIDAD, "cabeceras_pedidos/*", CABECERAS_PEDIDOS_ID);
    uriMatcher.addURI(AUTORIDAD, "cabeceras_pedidos/*/detalles", CABECERAS_ID_DETALLES);

    uriMatcher.addURI(AUTORIDAD, "detalles_pedidos", DETALLES_PEDIDOS);
    uriMatcher.addURI(AUTORIDAD, "detalles_pedidos/*", DETALLES_PEDIDOS_ID);

    uriMatcher.addURI(AUTORIDAD, "productos", PRODUCTOS);
    uriMatcher.addURI(AUTORIDAD, "productos/*", PRODUCTOS_ID);

    uriMatcher.addURI(AUTORIDAD, "clientes", CLIENTES);
    uriMatcher.addURI(AUTORIDAD, "clientes/*", CLIENTES_ID);

    uriMatcher.addURI(AUTORIDAD, "formas_pago", FORMAS_PAGO);
    uriMatcher.addURI(AUTORIDAD, "formas_pago/*", FORMAS_PAGO_ID);
}

Establecer Tipos En El Método getType()

Para informar al content provider sobre los tipos mime de cada uri, sobrescribe getType() con los métodos que generarMime() y generarMimeItem() de la clase contrato. Debido a que son varios recursos, te vendría bien usar una estructura switch o una clase auxiliar que asocie cada elemento.

@Override
public String getType(Uri uri) {
    switch (uriMatcher.match(uri)) {
        case CABECERAS_PEDIDOS:
            return ContratoPedidos.generarMime("cabeceras_pedidos");
        case CABECERAS_PEDIDOS_ID:
            return ContratoPedidos.generarMimeItem("cabeceras_pedidos");
        case DETALLES_PEDIDOS:
            return ContratoPedidos.generarMime("detalles_pedidos");
        case DETALLES_PEDIDOS_ID:
            return ContratoPedidos.generarMimeItem("detalles_pedidos");
        case PRODUCTOS:
            return ContratoPedidos.generarMime("productos");
        case PRODUCTOS_ID:
            return ContratoPedidos.generarMimeItem("productos");
        case CLIENTES:
            return ContratoPedidos.generarMime("clientes");
        case CLIENTES_ID:
            return ContratoPedidos.generarMimeItem("clientes");
        case FORMAS_PAGO:
            return ContratoPedidos.generarMime("formas_pago");
        case FORMAS_PAGO_ID:
            return ContratoPedidos.generarMimeItem("formas_pago");
        default:
            throw new UnsupportedOperationException("Uri desconocida =>" + uri);
    }
}

Operaciones Para Cabeceras De Pedido

1. Consultar todas las cabeceras de pedido — Dirígete al método query() del content provider y usa el matcher para determinar el código de resultado como se hizo en getType().

Luego establece el caso CABECERAS_PEDIDO en un switch y a través de tu helper consulta la tabla 'cabecera_pedido' de la misma forma que se hizo en la clase OperacionesBaseDatos.

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 + "." + ContratoPedidos.CabecerasPedido.ID,
            ContratoPedidos.CabecerasPedido.FECHA,
            ContratoPedidos.Clientes.NOMBRES,
            ContratoPedidos.Clientes.APELLIDOS,
            ContratoPedidos.FormasPago.NOMBRE};
@Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // Obtener base de datos
        SQLiteDatabase db = helper.getReadableDatabase();
        // Comparar Uri
        int match = uriMatcher.match(uri);

        Cursor c;

        switch (match) {
            case CABECERAS_PEDIDOS:
                SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
                builder.setTables(CABECERA_PEDIDO_JOIN_CLIENTE_Y_FORMA_PAGO);
                return builder.query(db, proyCabeceraPedido,
                        null, null, null, null, null);
                break;
            
            default:
                throw new UnsupportedOperationException("Uri no soportada");
        }

        c.setNotificationUri(resolver, uri);

        return c;

    }

La consulta involucra el join de las tablas cliente y formas de pago, ya que deseo mostrar algunas de sus columnas en la vista directamente. Sin embargo recuerda adaptar la consulta, bien sea para obtener solo las columnas de cabecera_pedido o agregar más tablas al join.

Para filtrar los resultados según el parámetro filtro solo extrae el resultado con getQueryParameter(). Luego crea una sentencia ORDER BY dependiendo del filtro.

En mi caso solo es poner el nombre de la columna según el parámetro.

case CABECERAS_PEDIDOS:
    // Obtener filtro
    String filtro = CabecerasPedido.tieneFiltro(uri)
            ? construirFiltro(uri.getQueryParameter("filtro")) : null;

    // Consultando todas las cabeceras de pedido
    builder.setTables(CABECERA_PEDIDO_JOIN_CLIENTE_Y_FORMA_PAGO);
    c = builder.query(db, proyCabeceraPedido,
            null, null, null, null, filtro);
    break;

//...
private String construirFiltro(String filtro) {
    String sentencia = null;

    switch (filtro) {
        case CabecerasPedido.FILTRO_CLIENTE:
            sentencia = "cliente.nombres";
            break;
        case CabecerasPedido.FILTRO_FECHA:
            sentencia = "cabecera_pedido.fecha";
            break;
    }

    return sentencia;
}

2. Consultar cabecera de pedido por id — Liga a la consulta anterior el identificador del pedido que viene en el segundo segmento de la uri. Para extraerlo usa el método de utilidad obtenerIdCabeceraPedido().

case CABECERAS_PEDIDOS_ID:
    // Consultando una cabecera de pedido
    String idCabeceraPedido = CabecerasPedido.obtenerIdCabeceraPedido(uri);
    builder.setTables(CABECERA_PEDIDO_JOIN_CLIENTE_Y_FORMA_PAGO);
    c = builder.query(db, proyCabeceraPedido,
            CabecerasPedido.ID + "=" + "\'" + idCabeceraPedido + "\'"
                    + (!TextUtils.isEmpty(selection) ?
                    " AND (" + selection + ')' : ""),
            selectionArgs, null, null, null);
    break;

No olvides usar el escape para las comillas \' si deseas tratar una variable como string en SQLite.

3. Obtener detalles del pedido — Consulta todos los detalles que pertenecen a un pedido con cierto id. La ruta del recurso muestra como cabecera_pedido es una tabla padre y detalle_pedido una tabla hija.

Sin embargo, también es posible obtener el mismo resultado refiriéndose a la ruta "/detalles" y especificando el id en los parámetros del Content Resolver.

 private static final String DETALLE_PEDIDO_JOIN_PRODUCTO =
            "detalle_pedido " +
                    "INNER JOIN producto " +
                    "ON detalle_pedido.id_producto = producto.id";
private String[] proyDetalle = {
            Tablas.DETALLE_PEDIDO + ".*",
            Productos.NOMBRE
    };

// ...
            case CABECERAS_ID_DETALLES:
                id = CabecerasPedido.obtenerIdCabeceraPedido(uri);
                builder.setTables(DETALLE_PEDIDO_JOIN_PRODUCTO);
                c = builder.query(db, proyDetalle,
                        DetallesPedido.ID_CABECERA_PEDIDO + "=" + "\'" + id + "\'"
                                + (!TextUtils.isEmpty(selection) ?
                                " AND (" + selection + ')' : ""),
                        selectionArgs, null, null, sortOrder);
                break;

La obtención de los detalles la hice con un join hacia el producto, con el fin de recolectar en los resultados el nombre de cada uno. Así podré proyectarlo en la vista cuando lo requiera.

4. Insertar cabecera de pedido — Ve al método insert() del provider, procesa la uri de entrada y crea el caso de coincidencia para las cabeceras de pedido. Luego usa el método del helper de bases de datos insertOrThrow() sobre la tabla cabecera_pedido.

@Override
public Uri insert(Uri uri, ContentValues values) {

    Log.d(TAG, "Inserción en " + uri + "( " + values.toString() + " )\n");

    SQLiteDatabase bd = helper.getWritableDatabase();

    String id = null;

    switch (uriMatcher.match(uri)) {
        case CABECERAS_PEDIDOS:
            // Generar Pk
            if (null == values.getAsString(CabecerasPedido.ID)) {
                id = CabecerasPedido.generarIdCabeceraPedido();
                values.put(CabecerasPedido.ID, id);
            }

            bd.insertOrThrow(Tablas.CABECERA_PEDIDO, null, values);
            notificarCambio(uri);
            return CabecerasPedido.crearUriCabeceraPedido(id);

        default:
            throw new UnsupportedOperationException("Uri no soportada");
    }


}

private void notificarCambio(Uri uri) {
    resolver.notifyChange(uri, null);
}

Genera el identificador de la cabecera con el método generarIdCabeceraPedido() si su valor es es null. Luego propaga el cambio con el método ContentResolver.notifyChange() y por último retorna la nueva uri de contenido con crearUriCabeceraPedido().

5. Modificar cabecera de pedido — Para actualizar cabeceras del pedido debes ubicarte en el método update(). Esta definición solo modificará un registro dependiendo de su id, por lo que usa el denominador CABECERA_PEDIDO_ID y adjunta el identificador a la operación.

@Override
public int update(Uri uri, ContentValues values, String selection,
                  String[] selectionArgs) {
    SQLiteDatabase db = helper.getWritableDatabase();
    String id;
    int afectados;

    switch (uriMatcher.match(uri)) {
        case CABECERAS_PEDIDOS_ID:
            id = CabecerasPedido.obtenerIdCabeceraPedido(uri);
            afectados = db.update(Tablas.CABECERA_PEDIDO, values,
                    CabecerasPedido.ID + " = ?", new String[]{id});
            notificarCambio(uri);
            break;
        default:
            throw new UnsupportedOperationException("Uri no soportada");
    }

    return afectados;
}

6. Eliminar cabeceras de pedido — Agrega dentro de delete() el switch para  la gestión de coincidencias de uris y luego establece el caso para una cabecera. De la misma forma que la modificación, usa el identificador de la cabecera para establecer el WHERE dentro de SQLiteDatabase.delete().

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
    Log.d(TAG, "delete: " + uri);

    SQLiteDatabase bd = helper.getWritableDatabase();
    String id;
    int afectadas;

    switch (uriMatcher.match(uri)) {
        case CABECERAS_PEDIDOS_ID:
            // Obtener id
            id = CabecerasPedido.obtenerIdCabeceraPedido(uri);
            afectadas = bd.delete(
                    Tablas.CABECERA_PEDIDO,
                    CabecerasPedido.ID + " = ? ",
                    new String[]{id}
            );
            notificarCambio(uri);
            break;
        default:
            throw new UnsupportedOperationException(URI_NO_SOPORTADA);
    }
    return afectadas;
}

Operaciones Para Detalles De Pedidos

1. Obtener todos los detalles de pedidos — Para consultar todos los registros de la tabla detalle_pedido usa el código DETALLES_PEDIDO en el método query(). Usa la instancia de SQLiteQueryBuilder para setear el inner join entre los detalle_pedido y producto, con el fin de obtener el nombre de cada producto en los resultados.

private static final String DETALLE_PEDIDO_JOIN_PRODUCTO =
            "detalle_pedido " +
                    "INNER JOIN producto " +
                    "ON detalle_pedido.id_producto = producto.id";
private String[] proyDetalle = {
            Tablas.DETALLE_PEDIDO + ".*",
            Productos.NOMBRE
    };

// ...
case DETALLES_PEDIDOS:
    builder.setTables(DETALLE_PEDIDO_JOIN_PRODUCTO);
    c = builder.query(db, proyDetalle,
            selection, selectionArgs, null, null, sortOrder);
    break;

2. Insertar detalle de pedido — La inserción de un detalle es mucho más cómodo si usas el código CABECERAS_ID_DETALLES, puesto que sabes cuál es el id de la cabecera a la cual se debe relacionar el detalle.

case CABECERAS_ID_DETALLES:
    // Setear id_cabecera_pedido
    id = CabecerasPedido.obtenerIdCabeceraPedido(uri);

    values.put(DetallesPedido.ID_CABECERA_PEDIDO, id);
    bd.insertOrThrow(Tablas.DETALLE_PEDIDO, null, values);
    notificarCambio(uri);

    String secuencia = values.getAsString(DetallesPedido.SECUENCIA);

    return DetallesPedido.crearUriDetallePedido(id, secuencia);

3. Modificar detalle de pedido —Ve a update() y agrega el caso para DETALLES_PEDIDO_ID. La uri viene con un segmento con la llave compuesta del detalle, por lo que debes usar el método DetallesPedido.obtenerIdDetalle() para extraer la columna id_cabecera_pedido y secuencia. Con estos valores crea la condición de la actualización.

case DETALLES_PEDIDOS_ID:
    String[] claves = DetallesPedido.obtenerIdDetalle(uri);

    String seleccion = String.format("%s=? AND %s=?",
            DetallesPedido.ID_CABECERA_PEDIDO, DetallesPedido.SECUENCIA);

    afectados = db.update(Tablas.DETALLE_PEDIDO, values, seleccion, claves);
    break;

4. Eliminar detalle de pedido — En delete() aumenta los casos con DETALLES_PEDIDO_ID y elimina el detalle basado en su llave compuesta.

case DETALLES_PEDIDOS_ID:
    String[] claves = DetallesPedido.obtenerIdDetalle(uri);

    String seleccion = String.format("%s=? AND %s=?",
            DetallesPedido.ID_CABECERA_PEDIDO, DetallesPedido.SECUENCIA);

    afectados = bd.delete(Tablas.DETALLE_PEDIDO, seleccion, claves);
    break;

Operaciones Para Productos

1. Obtener todos los productos — Consultar los productos es sencillo, solo realiza una consulta general dentro de query() con el caso PRODUCTOS.

case PRODUCTOS:
    c = bd.query(Tablas.PRODUCTO, projection,
            selection, selectionArgs,
            null, null, sortOrder);
    break;

2. Obtener producto por id — De la misma forma que el punto anterior, haz una consulta sobre la tabla producto y agrega como condición la igualdad del id a través de Productos.obtenerIdProducto().

case PRODUCTOS_ID:
    id = Productos.obtenerIdProducto(uri);
    c = bd.query(Tablas.PRODUCTO, projection,
            Productos.ID + "=" + "\'" + id + "\'"
                    + (!TextUtils.isEmpty(selection) ?
                    " AND (" + selection + ')' : ""),
            selectionArgs,
            null, null, sortOrder);
    break;

3. Insertar productos — Adjunta el caso PRODUCTOS en insert() para insertar un nuevo producto.

case PRODUCTOS: 
    bd.insertOrThrow(Tablas.PRODUCTO, null, values);
    notificarCambio(uri);
    return Productos.crearUriProducto(values.getAsString(Tablas.PRODUCTO));

4. Modificar un producto — Ahora ve a update() para añadir el caso PRODUCTOS_ID y actualizar el producto según el id obtenido por Productos.obtenerIdProducto().

case PRODUCTOS_ID:
    id = Productos.obtenerIdProducto(uri);
    afectados = bd.update(Tablas.PRODUCTO, values,
            Productos.ID + "=" + "\"" + id + "\""
                    + (!TextUtils.isEmpty(selection) ?
                    " AND (" + selection + ')' : ""),
            selectionArgs);
    break;

5. Eliminar un producto — Similar a la modificación, ve a delete() y establece la condición con el identificador del producto a eliminar.

case PRODUCTOS_ID:
    id = Productos.obtenerIdProducto(uri);
    afectados = bd.delete(Tablas.PRODUCTO,
            Productos.ID + "=" + "\"" + id + "\""
                    + (!TextUtils.isEmpty(selection) ?
                    " AND (" + selection + ')' : ""),
            selectionArgs);
    break;

Operaciones Para Clientes

1. Obtener todos los clientes — De igual manera que los otros recursos, usa el caso CLIENTES para generar la coincidencia dentro del switch.

case CLIENTES:
    c = bd.query(Tablas.CLIENTE, projection,
            selection, selectionArgs, null, null, sortOrder);
    break;

2. Obtener cliente por su id — Allí mismo en query() establece el caso CLIENTES_ID y condiciona la anterior  consulta con el resultado de Clientes.obtenerIdCliente().

case CLIENTES_ID:
    id = Clientes.obtenerIdCliente(uri);
    c = bd.query(Tablas.CLIENTE, projection,
            Clientes.ID + " = ?",
            new String[]{id}, null, null, null);
    break;

3. Insertar un cliente — Usa el caso CLIENTES dentro de insert() para añadir un nuevo registro en la tabla cliente. Retorna la uri del nuevo cliente con crearUriCliente().

case CLIENTES:
    bd.insertOrThrow(Tablas.CLIENTE, null, values);
    notificarCambio(uri);
    return Clientes.crearUriCliente(values.getAsString(Clientes.ID));

4. Modificar un cliente — Dirígete a update(), específica el caso CLIENTES_ID y actualiza el registro basado en el resultado de Clientes.obtenerIdCliente().

case CLIENTES_ID:
    id = Clientes.obtenerIdCliente(uri);
    afectados = bd.update(Tablas.CLIENTE, values,
            Productos.ID + "=" + "\"" + id + "\""
                    + (!TextUtils.isEmpty(selection) ?
                    " AND (" + selection + ')' : ""),
            selectionArgs);
    break;

5. Eliminar un cliente — Define el caso CLIENTES_ID en delete() y elimina el registro que coincida con el id retornado por Clientes.obtenerIdCliente().

case CLIENTES_ID:
    id = Clientes.obtenerIdCliente(uri);
    afectados = bd.update(Tablas.CLIENTE, values,
            Clientes.ID + "=" + "\"" + id + "\""
                    + (!TextUtils.isEmpty(selection) ?
                    " AND (" + selection + ')' : ""),
            selectionArgs);
    break;

Operaciones Para Formas De Pago

1. Obtener todos las formas de pago — Sigue el mismo patrón de las consultas anteriores, pero esta vez con el caso FORMAS_PAGO para consultar la tabla forma_pago.

case FORMAS_PAGO:
    c = bd.query(Tablas.FORMA_PAGO, projection,
            selection, selectionArgs, null, null, sortOrder);
    break;

2. Obtener formas de pago por su id — Adiciona un where a la consulta anterior para obtener la forma de pago basada en el identificador obtenido en FormasPago.obtenerIdFormaPago().

case FORMAS_PAGO_ID:
    id = FormasPago.obtenerIdFormaPago(uri);
    c = bd.query(Tablas.FORMA_PAGO, projection,
            FormasPago.ID + " = ?",
            new String[]{id}, null, null, null);
    break;

3. Insertar forma de pago — Crea el caso FORMAS_PAGO_ID dentro de insert() y añade la nueva forma de pago en forma_pago. Retorna la nueva uri con crearUriFormaPago().

case FORMAS_PAGO:
    bd.insertOrThrow(Tablas.FORMA_PAGO, null, values);
    notificarCambio(uri);
    return FormasPago.crearUriFormaPago(values.getAsString(FormasPago.ID));

4. Modificar forma de pago — Expande el switch del método update() para insertar el caso FORMAS_PAGO_ID y modificar la forma de pago con el id arrojado por el método obtenerIdFormaPago().

case FORMAS_PAGO_ID:
    id = FormasPago.obtenerIdFormaPago(uri);
    afectados = bd.update(Tablas.FORMA_PAGO, values,
            FormasPago.ID + "=" + "\"" + id + "\""
                    + (!TextUtils.isEmpty(selection) ?
                    " AND (" + selection + ')' : ""),
            selectionArgs);
    break;

5. Eliminar forma de pago — Finalmente en delete() crea el caso de eliminación con el identificador arrojado por obtenerIdFormaPago().

case FORMAS_PAGO_ID:
    id = FormasPago.obtenerIdFormaPago(uri);
    afectados = bd.delete(Tablas.FORMA_PAGO,
            FormasPago.ID + "=" + "\"" + id + "\""
                    + (!TextUtils.isEmpty(selection) ?
                    " AND (" + selection + ')' : ""),
            selectionArgs);
    break;

Probar ContentProvider De Pedidos

Las pruebas las haremos replicando las operaciones que hiciste en el artículo de sqlite con múltiples tablas.

Para crear una transacción donde se alberguen varias operaciones sobre el content provider es necesario usar el método ContentProvider.applyPatch().

Cada operación debe ser preparada en objetos del tipo ContentProviderOperation con el fin de pasar una lista de ellas y así ejecutarlas en una transacción. Esta clase provee 3 métodos estáticos que te permitirán representar cada modificación:

  • newInsert(): Crea un Builder para estructurar una inserción. Recibe como parámetro la uri que será afectada.
  • newUpdate(): Crea un Builder para estructurar una modificación.
  • newDelete(): Crea un Builder para estructurar una eliminación.

Antes de preparar la lista de operaciones, sobrescribe applyBatch() en tu content provider. La idea es crear un ciclo para aplicar cada elemento dentro de una transacción.

@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    final SQLiteDatabase db = helper.getWritableDatabase();
    db.beginTransaction();
    try {
        final int numOperations = operations.size();
        final ContentProviderResult[] results = new ContentProviderResult[numOperations];
        for (int i = 0; i < numOperations; i++) {
            results[i] = operations.get(i).apply(this, results, i);
        }
        db.setTransactionSuccessful();
        return results;
    } finally {
        db.endTransaction();
    }
}

Ahora solo modifica los métodos obsoletos por la nueva modalidad del content provider, luego aplica el batch y al final loguea el resultado de las tablas:

import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.OperationApplicationException;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.RemoteException;
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.sqlite.ContratoPedidos;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.CabecerasPedido;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.Clientes;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.DetallesPedido;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.FormasPago;
import com.herprogramacion.pedidos.sqlite.ContratoPedidos.Productos;

import java.util.ArrayList;
import java.util.Calendar;

public class ActividadListaPedidos extends AppCompatActivity {

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

            ContentResolver r = getContentResolver();

            // Lista de operaciones
            ArrayList<ContentProviderOperation> ops = new ArrayList<>();

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

            // Inserción Clientes
            String cliente1 = Clientes.generarIdCliente();
            String cliente2 = Clientes.generarIdCliente();
            ops.add(ContentProviderOperation.newInsert(Clientes.URI_CONTENIDO)
                    .withValue(Clientes.ID, cliente1)
                    .withValue(Clientes.NOMBRES, "Veronica")
                    .withValue(Clientes.APELLIDOS, "Del Topo")
                    .withValue(Clientes.TELEFONO, "4552000")
                    .build());
            ops.add(ContentProviderOperation.newInsert(Clientes.URI_CONTENIDO)
                    .withValue(Clientes.ID, cliente2)
                    .withValue(Clientes.NOMBRES, "Carlos")
                    .withValue(Clientes.APELLIDOS, "Villagran")
                    .withValue(Clientes.TELEFONO, "4440000")
                    .build());


            // Inserción Formas de pago
            String formaPago1 = FormasPago.generarIdFormaPago();
            String formaPago2 = FormasPago.generarIdFormaPago();
            ops.add(ContentProviderOperation.newInsert(FormasPago.URI_CONTENIDO)
                    .withValue(FormasPago.ID, formaPago1)
                    .withValue(FormasPago.NOMBRE, "Efectivo")
                    .build());
            ops.add(ContentProviderOperation.newInsert(FormasPago.URI_CONTENIDO)
                    .withValue(FormasPago.ID, formaPago2)
                    .withValue(FormasPago.NOMBRE, "Crédito")
                    .build());

            // Inserción Productos
            String producto1 = Productos.generarIdProducto();
            String producto2 = Productos.generarIdProducto();
            String producto3 = Productos.generarIdProducto();
            String producto4 = Productos.generarIdProducto();
            ops.add(ContentProviderOperation.newInsert(Productos.URI_CONTENIDO)
                    .withValue(Productos.ID, producto1)
                    .withValue(Productos.NOMBRE, "Manzana unidad")
                    .withValue(Productos.PRECIO, 2)
                    .withValue(Productos.EXISTENCIAS, 100)
                    .build());
            ops.add(ContentProviderOperation.newInsert(Productos.URI_CONTENIDO)
                    .withValue(Productos.ID, producto2)
                    .withValue(Productos.NOMBRE, "Pera unidad")
                    .withValue(Productos.PRECIO, 3)
                    .withValue(Productos.EXISTENCIAS, 230)
                    .build());
            ops.add(ContentProviderOperation.newInsert(Productos.URI_CONTENIDO)
                    .withValue(Productos.ID, producto3)
                    .withValue(Productos.NOMBRE, "Guayaba unidad")
                    .withValue(Productos.PRECIO, 5)
                    .withValue(Productos.EXISTENCIAS, 55)
                    .build());
            ops.add(ContentProviderOperation.newInsert(Productos.URI_CONTENIDO)
                    .withValue(Productos.ID, producto4)
                    .withValue(Productos.NOMBRE, "Maní unidad")
                    .withValue(Productos.PRECIO, 3.6f)
                    .withValue(Productos.EXISTENCIAS, 60)
                    .build());

            // Inserción Pedidos
            String pedido1 = CabecerasPedido.generarIdCabeceraPedido();
            String pedido2 = CabecerasPedido.generarIdCabeceraPedido();
            ops.add(ContentProviderOperation.newInsert(CabecerasPedido.URI_CONTENIDO)
                    .withValue(CabecerasPedido.ID, pedido1)
                    .withValue(CabecerasPedido.FECHA, fechaActual)
                    .withValue(CabecerasPedido.ID_CLIENTE, cliente1)
                    .withValue(CabecerasPedido.ID_FORMA_PAGO, formaPago1)
                    .build());
            ops.add(ContentProviderOperation.newInsert(CabecerasPedido.URI_CONTENIDO)
                    .withValue(CabecerasPedido.ID, pedido2)
                    .withValue(CabecerasPedido.FECHA, fechaActual)
                    .withValue(CabecerasPedido.ID_CLIENTE, cliente2)
                    .withValue(CabecerasPedido.ID_FORMA_PAGO, formaPago2)
                    .build());

            // Inserción Detalles
            Uri uriParaDetalles = CabecerasPedido.crearUriParaDetalles(pedido1);
            ops.add(ContentProviderOperation.newInsert(uriParaDetalles)
                    .withValue(DetallesPedido.SECUENCIA, 1)
                    .withValue(DetallesPedido.ID_PRODUCTO, producto1)
                    .withValue(DetallesPedido.CANTIDAD, 5)
                    .withValue(DetallesPedido.PRECIO, 2)
                    .build());
            ops.add(ContentProviderOperation.newInsert(uriParaDetalles)
                    .withValue(DetallesPedido.SECUENCIA, 2)
                    .withValue(DetallesPedido.ID_PRODUCTO, producto1)
                    .withValue(DetallesPedido.CANTIDAD, 10)
                    .withValue(DetallesPedido.PRECIO, 3)
                    .build());

            uriParaDetalles = CabecerasPedido.crearUriParaDetalles(pedido2);
            ops.add(ContentProviderOperation.newInsert(uriParaDetalles)
                    .withValue(DetallesPedido.SECUENCIA, 1)
                    .withValue(DetallesPedido.ID_PRODUCTO, producto1)
                    .withValue(DetallesPedido.CANTIDAD, 30)
                    .withValue(DetallesPedido.PRECIO, 5)
                    .build());
            ops.add(ContentProviderOperation.newInsert(uriParaDetalles)
                    .withValue(DetallesPedido.SECUENCIA, 2)
                    .withValue(DetallesPedido.ID_PRODUCTO, producto1)
                    .withValue(DetallesPedido.CANTIDAD, 20)
                    .withValue(DetallesPedido.PRECIO, 3.6f)
                    .build());

            // Eliminación Pedido
            ops.add(ContentProviderOperation
                    .newDelete(CabecerasPedido.crearUriCabeceraPedido(pedido1))
                    .build());

            // Actualización Cliente
            ops.add(ContentProviderOperation.newUpdate(Clientes.crearUriCliente(cliente2))
                    .withValue(Clientes.ID, cliente2)
                    .withValue(Clientes.NOMBRES, "Carlos Alberto")
                    .withValue(Clientes.APELLIDOS, "Villagran")
                    .withValue(Clientes.TELEFONO, "3333333")
                    .build());

            try {
                r.applyBatch(ContratoPedidos.AUTORIDAD, ops);
            } catch (RemoteException e) {
                e.printStackTrace();
            } catch (OperationApplicationException e) {
                e.printStackTrace();
            }

            // [QUERIES]
            Log.d("Clientes", "Clientes");
            DatabaseUtils.dumpCursor(r.query(Clientes.URI_CONTENIDO, null, null, null, null));
            Log.d("Formas de pago", "Formas de pago");
            DatabaseUtils.dumpCursor(r.query(FormasPago.URI_CONTENIDO, null, null, null, null));
            Log.d("Productos", "Productos");
            DatabaseUtils.dumpCursor(r.query(Productos.URI_CONTENIDO, null, null, null, null));
            Log.d("Cabeceras de pedido", "Cabeceras de pedido");
            DatabaseUtils.dumpCursor(r.query(CabecerasPedido.URI_CONTENIDO, null, null, null, null));
            Log.d("Detalles de pedido", "Detalles del pedido #1");
            DatabaseUtils.dumpCursor(r.query(CabecerasPedido.crearUriParaDetalles(pedido1),
                    null, null, null, null));
            Log.d("Detalles de pedido", "Detalles del pedido #2");
            DatabaseUtils.dumpCursor(r.query(CabecerasPedido.crearUriParaDetalles(pedido2),
                    null, null, null, null));

            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");

        new TareaPruebaDatos().execute();
    }

}

Si todo salió bien, verás en en el logcat el dumping de cada una de las tablas después de realizar el batch.

Android Studio: Dumping de cursor de clientes

Conclusión

Crear un ContentProvider con múltiples tablas requiere que comprendas previamente tu modelo de datos y la forma en que estructurarás tus Uris de contenido para proveer datos en tu app.

Al momento de querer operar un conjunto de operaciones en una transacción, usa el método applyBatch() junto a las operaciones prediseñadas representadas por la clase ContentProviderOperation.

Recuerda que este componente se adapta muy bien con los Loaders para cargar datos en un hilo aislado, con el fin de no entorpecer la fluidez de la interfaz. Lo que te vendría bien si necesitas poblar listas a través de recycler views o list views.

  • Salvador Barbosa

    Hola James! tengo varios días viendo tus tutoriales y tengo que decir que son increíblemente detallados y precisos. Dicho esto, quisiera hacer una consulta, si en este tutorial -> http://www.hermosaprogramacion.com/2016/01/base-de-datos-sqlite-en-android-con-multiples-tablas/ creaste una clase para hacer operaciones en la base de datos, pq no usas esa misma clase desde el content provider? Tal vez es algo muy sencillo y lo estoy pasando, gracias y saludos desde ya!

  • Xana Eds

    Hola, gracias por los tutoriales, oye una pregunta, de casualidad has usado ORMlite en Android para las bases de datos? Saludos =)

    • Con gusto Xana.Si claro, estás intentando implementarlo en tu app?, estoy pensando en crear un tuto sobre ese tema pero toca esperar un poco.

  • victor

    Hola James, como estas? qué tal todo? Muchas gracias como siempre, te iba a preguntar si las tablas del esquema de la BD las has pintado con alguna herramienta o a pelo manualmente? Te queda muy chulo!

    Saludos!

    • Hola Victor para ese diagrama usé draw.io

      • victor

        Muchísimas gracias!!

  • victor

    Hola James, una pregunta así a grandes rasgos, ya que como hasta que no haga por mi cuenta un código de un content provider de múltiples tablas no lograré entenderlo, pero para hacerlo, me gustaría saber un poco el porqué haces como una doble id, es decir, generas otro campo id, aparte del suyo, donde es de tipo String con un prefijo de la tabla. Y no sé si es posible contármelo así un poco el motivo de dicho campo en las tablas y si siempre que se tiene más de una tabla que se relacionarán tiene que hacerse de este modo, añadiendo como una segunda id. Ya que veo como lo utilizas, lo entiendo pero como que se me olvida luego y tengo que volver a ver tu código y es porque no logro entender el motivo. Deduzco que es como para tener un control de la id que utilizas, pero claro para que lo hacemos si de forma automática ya se crea una ID única al hacer un insert. Pero luego trabajas con tú id de tipo String y no con la id que se genera automáticamente.

    Muchas gracias por todo James, espero que sea una respuesta sencilla y no liarte, si la respuesta es compleja con una pista o detalle que me ayude a luego ver la respuesta me es suficiente, muchas gracias!

    Saludos!

    • Hola victor, esa id es por si vas a sincronizar sqlite con una base de datos remota en un servidor. El campo “_id” es necesario para operar en SQLite y normalmente es entero autoincrementable, pero no todas las llaves primarias tienen ese formato, así que agrego una que se ajuste a las reglas de negocio del problema.

      Solo es eso.

      • victor

        Vaaleee muchas gracias James, entonces esa id, seria la famosa ID_REMOTA que utilizas en el syncadapter no?

        Gracias por todo crack!

        • victor

          Bueno perdona seria la id_local pero en remota, a la inversa creo

  • Joseph R

    Hola! recien me acabo de suscribir, que gran pagina la verdad!, disculpa con respecto a los diagramas que haces, por casualidad conoces alguna pagina web o algun tutorial bueno para saber manejar estos diagramas y saber como crearlos? muchas gracias! saludos!

  • Luis David Guzman

    Hola, los tutoriales son siempre los mejores, muchas gracias por compartir, y nunca me pierdo cuando sale uno nuevo, Saludos !!!!!

  • Muy detallados los tutoriales, grácias, estoy avanzando en conocimientos sobre Android

  • Hola amigos!

    Les dejo este tutorial para actualizar el articulo anterior sobre múltiples tablas en sqlite con un Content Provider. Espero les sea de utilidad.

    Recuerden avisarme de todos los errores de redacción y codificación que encuentre. Igual si tienen sugerencias u otras formas mas optimizadas de realizar el código.

    Saludos :)