APIs XML para Bases de Datos

Nota:

Puedes encontrar la versión original en Inglés de este artículo en javaworld

Para procesar documentos XML, la mayoría de las herramientas XML trabajan con los APIs SAX o DOM. En este artículo, veremos una forma de implementar los mismo APIs directamente sobre una base de datos, permitiendo a las herramientas de XML tratar con bases de datos como si fueran documentos XML. De esta forma, podemos obviar la necesidad de convertir una base de datos.

Veremos una implementación del API SAX para Bases de Datos que debería funcionar con cualquier base de datos con un driver JDBC. Luevo examinaremos una implementación del API DOM para Bases de Datos que usa el API SAX internamente. Para demostrar el API SAX para Bases de Datos, veremos su integración con XT (un procesador XSLT). También veremos un ejemplo de como dicha integración puede crear páginas HTML que incorporan hojas de estilo XSLT directamente desde la base de datos. Finalmente, veremos como el API DOM para bases de datos se integra con un procesador XQL.

En este artículo, haremos uso de las herramientas existentes en vez de crear nuevas herramientas para ilustrar las aplicaciones de los APIs SAX y DOM para bases de datos. Hemos visto como ha crecido el número de herramientas XML disponibles para trabajar con bases de datos. Todas las herramientas XML que mencionamos aquí o son gratuitas o lo són para un uso no-comercial (aunque deberías, por supuesto, comprobar los acuerdos de licencia).

Introducción a los APIs SAX y DOM

SAX es un API para XML basado en eventos. Con él, el analizador SAX reporta los eventos como el inicio o final de los elementos de la aplicación y mientras pasa a través del documento. Como el analizador reporta los eventos mientras visita las diferentes partes del documento, no tiene que construir una estructura interna. Esto reduce los recursos de sistema necesarios, lo que hace a este analizador atractivo para grandes documentos. Para documentos XML recibidos como streams continuos, un API basado en eventos es la única elección.

Por otro lado, el API DOM, sigue una construcción estilo árbol. Los elementos tienen relaciones padre-hijo con otros elementos. Con este API, el analizador construye una estrucura interna por la que una aplicación puede navegar. DOM permite que una aplicación tenga acceso aleatorio al documento estructurado como un árbol, y el coste es el incremento de la memoria utilizada.

APIs de XML para Bases de Datos: Lo Básico

A causa de la estructura altamente regular del almacenamiento de datos en una base de datos, podemos mapearla dentro de documentos XML centrados en datos. Por ejemplo, podemos transformar una tabla de una base de datos en un documento XML con un DTD de la siguiente forma:

<!ELEMENT table rows*>
<!ELEMENT rows (column1, column2, ...)>
<!ELEMENT column1 #PCDATA>
<!ELEMENT column2 #PCDATA>
....

En otras palabras, con un API XML para base de datos, podemos hacer que la base de datos se parezca a un documento XML; estos APIs presentan la base de datos como un documento XML virtual. Tenemos los conceptos más básico del diseño orientado a objetos: es el interface -- no la implementación -- lo que importa. En nuestra situación, las herramientas que usan dichos APIS XML no necesitan tener cuidado de si están operando con una tabla de una base de datos o con un fichero XML.

Un analizador SAX o DOM puede permitir a las herramientas XML trabajar directamente con bases de datos.

Implementar el API SAX para Bases de Datos

Para implementar el API SAX para Bases de Datos, necesitamos implementar un analizador que opere sobre una fuente de datos JDBC, iterar sobre cada fila y columna, y generar los eventos apropiados mientras iteramos. La especificación SAX proporciona la clase org.xml.sax.InputSource que modela la fuente de datos representando una URL o un stream de bytes. Para representar una base de datos, necesitamos una forma especializada que pueda representar una tabla de una base de datos. Por lo tanto implementamos JDBCInputSource, que extiende la clase org.xml.sax.InputSource . Echemos un vistazo en más detalle a la clase JDBCInputSource.java:

package dbxml.sax;

import java.sql.*;
import org.xml.sax.InputSource;

public class JDBCInputSource extends InputSource {
    private String _connectionURL;
    private String _userName;
    private String _passwd;
    private String _tableName;

    public JDBCInputSource(String connectionURL, String userName, 
            String passwd, String tableName) {
        super(connectionURL);
        _connectionURL = connectionURL;
        _userName = userName;
        _passwd = passwd;
        _tableName = tableName;
    }

    public String getTableName() {
        return _tableName;
    }

    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection(_connectionURL, 
		_userName, _passwd);
    }
}

En el código de arriba, el constructor toma la información necesaria para conectar con la base de datos y el nombre de la tabla a analizar. El método getConnection() conecta con la base de datos y devuelve un objeto Connection.

Luego, necesitamos implementar el analizador SAX que usa JDBCInputSource para iterar sobre las filas y columnas de la tabla de la base de datos y generar eventos SAX. Para simplificar el código, hemos creado una clase abstracta ParserBase, que implementa org.xml.sax.Parser y tiene responsabilidad sólo para manejar varios controladores. Después creamos nuestro analizador SAX para la fuente JDBC JDBCSAXParser que extiende la clase ParserBase.

(Aquí puedes ver el código de ParserBase.java.)

JDBCSAXParser.java

package dbxml.sax;
import java.io.IOException;
import java.sql.*;
import org.xml.sax.*;
import org.xml.sax.helpers.AttributeListImpl;

public class JDBCSAXParser extends ParserBase {
    private static final 
    AttributeList _stockEmptyAttributeList = new AttributeListImpl();
	
    //-------------------------------------
    //	Methods from the Parser	interface
    //-------------------------------------

    public void parse (InputSource source) throws SAXException, IOException {
        if (! (source instanceof JDBCInputSource)) {
	throw new SAXException("JDBCSAXParser can work only with source "
				+ "of JDBCInputSource type");
        }
        parse((JDBCInputSource)source);
    }

    public void parse (String systemId) throws SAXException, IOException {

        throw new SAXException("JDBCSAXParser needs more information to "
				+ "connect to database");
    }

    //-------------------------
    // Additional methods 
    //--------------------------

    public void parse(JDBCInputSource source) throws SAXException, IOException {
        try {
            Connection connection = source.getConnection();
	if (connection == null) {
	    throw new SAXException("Could not establish connection with "
							+ "database");
            }

	String sqlQuery = getSelectorSQLStatement(source.getTableName());
	PreparedStatement pstmt = connection.prepareStatement(sqlQuery);

	ResultSet rs = pstmt.executeQuery();
	parse(rs, source.getTableName());
	rs.close();

	connection.close();
        } 
        catch (SQLException ex) {
	throw new SAXException(ex);
        }
    }

    public void parse(ResultSet rs, String tableName) 
	throws SAXException, SQLException, IOException {

        if (_documentHandler == null) {
	return;         // nobody is interested in me, no need to sweat!
        }

        ResultSetMetaData rsmd = rs.getMetaData();
        int numCols = rsmd.getColumnCount();

        String tableMarker = getTableMarker(tableName);
        String rowMarker = getRowMarker();

        _documentHandler.startDocument();
        _documentHandler.startElement(tableMarker,_stockEmptyAttributeList);
        while(rs.next()) {
	_documentHandler.startElement(rowMarker, _stockEmptyAttributeList);
	for (int i = 1; i <= numCols; i++) {
	    generateSAXEventForColumn(rsmd, rs,i);
	}
	_documentHandler.endElement(rowMarker);
        }
        _documentHandler.endElement(tableMarker);
        _documentHandler.endDocument();
    }

    public void parse(String connectionURL, String userName, String passwd,
		String tableName) throws SAXException, IOException {

        parse(new JDBCInputSource(connectionURL, userName, passwd, tableName));
    }

    //--------------------------------------
    // Protected methods that derived classes could override to 
    // customize the parsing.
    //--------------------------------------

    protected void generateSAXEventForColumn(ResultSetMetaData rsmd,
	ResultSet rs, int columnIndex) throws SAXException, SQLException {

        String columnValue = rs.getString(columnIndex);
        if (columnValue == null) {
	return;
        }
        String columnMarker = getColumnMarker(
		rsmd.getColumnLabel( columnIndex));
        char[] columnValueChars = columnValue.toCharArray();
        _documentHandler.startElement(columnMarker, _stockEmptyAttributeList);
        _documentHandler.characters(columnValueChars, 0, columnValueChars.length);
        _documentHandler.endElement(columnMarker);
    }

    protected String getTableMarker(String tableName) {
        return tableName;
    }

    protected String getRowMarker() {
        return "row";
    }

    protected String getColumnMarker(String columnName)	{
        return columnName;
    }

    protected String getSelectorSQLStatement(String tableName) {
        return "select * from " + tableName; 
    }
}

Examinemos el código en más detalle. JDBCSAXParser incluye varios metodos parse() sobrecargados. En la lista de abajo, el interface org.xml.sax.Parser requiere que se implementen los métodos parse(InputSource) y parse(String). Los otros métodos parse() simplifican el código y permiten a las clases derivadas sobreescribirlos para modificar el comportamiento del analizador.

  • El método parse(InputSource) llama al método parse(JDBCInputSource) si el argumento es del tipo JDBCInputSource; de otro modo lanza una SAXException ya que no puede tratarlo.
  • El método parse(String) lanza una SAXException si la información suministrada no es suficiente para acceder a la base de datos.
  • El método parse(JDBCInputSource) obtiene un objeto Connection de la fuente de entrada y ejecuta una consulta para obtener un objeto ResultSet. Luego llama a parse(ResultSet) con este objeto.
  • El método parse(ResultSet, String) realiza la lógica principal del análisis. Itera sobre cada fila de la hoja de resultados y sobre cada columna de las filas. El bucle de iteracción de filas está rodeado por llamadas a startElement() y endElement() con un marcador de tabla como argumento element-name. Similarmente, cada bucle de iteracción de columna está rodeado por llamadas a startElement() y endElement() con un marcador de fila como el argumento element-name. En ambos casos una lista de atributo vacía se pasa como el segundo argumento a los métodos startElement(). En cada visita a una columna, se llama al método generateSAXEventForColumn() con argumentos column-name y column-value. Se accede al valor de una columna mediante el método getString() sobre el objeto result-set, porque necesitamos una representación string de los datos de la columna para que sean notificados en el evento SAX characters().
  • El método de conveniencia parse(String, String, String, String) simplemente crea un objeto JDBCInputSource con los argumentos pasados y luego llama al método parse(JDBCInputSource) con él.

Los métodos protected de JDBCSAXParser ofrecen algunas posibilidades de personalización a través de su sobreescritura:

  • El método generateSAXEventForColumn() genera eventos para los datos de columna. Un valor null para una columna en una base de datos tiene diferentes significados para una columna con string vacío. Capturamos la diferencia no disparando ningún evento para las columnas que tienen un valor null. Otra elección para representar un valor null en una base de datos es usar un atributo binario como isNull. Con esta opción, un valor true será configurado para datos nulos; de otro modo será false.
  • Los métodos getTableMarker(), getRowMarker(), y getColumnMarker() devuelven valores por defecto razonables para los marcadores de tabla, fila y columna. Las clases derivadas podrían sobrescribirlos para proporcionar marcas personalizadas.
  • El método getSelectorSQLStatement() devuelve un string "select * from <tableName>". Las clases derivadas pueden sobreescribirlo para proporcionar una consulta select diferente para ofrecer nivel de filtrado ala base de datos.

La clase de conveniencia JDBCSAXUtils proporciona dos métodos para crear un JDBCInputSource: puede hacerse desde un fichero de propiedades o desde un objeto Property. No necesitamos suministrar una larga lista de parámetros que describan una base de datos a una aplicación que use los APIs SAX o DOM para bases de datos. La clase espera que el usuario suministre un fichero apropiado que contenga entradas para una URL de la base de datos, un nombre de usuario y una password para conectar con la base de datos, un driver JDBC para establecer la conexión, y un nombre de tabla. El código de abajo muestra un fichero de propiedades típico:

URL=jdbc:odbc:db1
user=jw
password=jw-passwd
table=portfolio
driver=sun.jdbc.odbc.JdbcOdbcDriver

La historia hasta ahora ...

Ahora tenemos un sencillo analizador que puede generar los eventos SAX apropiados para la información de una tabla de la base de datos. Tiene cuidado con los datos nulos y ofrece alguna personalización de los marcadores. Aunque dichas funcionalidades podrían parecer suficientes para algunas aplicaciones, la solución completa considerará funcionalidades adicionales porque:

  • El analizador no incorpora información relacional. Esto puede resolverse usando un XPointer/XLink para seleccionar la referencia a una clave exterior en una tabla.
  • Una columna de texto en una base de datos podría contener datos marcados. Un analizador SAX para bases de datos también debería analizar aquellos datos y generar los eventos SAX apropiados. Si dicha funcionalidad es importante para una aplicación, podría sobreescribir generateSAXEventForColumn() y analizar el contenido de la columna y generar eventos SAX adicionales.
  • En bases de datos, una tabla contiene una lista desordenada de columnas; el orden en que se almacenan las columnas no es importante. Por otro lado, un DTD XML, no tiene forma de describir una colección de elementos hijos desordenados.

    Podemos tratar este problema de varias formas. Si la tarea es para convertir una base de datos en otro documento XML, digamos una página HTML, la hoja de estilo XSLT escrita para este propósito puede crear la salida en el orden correcto. También podríamos sobreescribir el método getSelectorSQLStatement() para suministrar una lista explícita de columnas en el orden correcto.

  • Es deseable para presentar sólo una parte seleccionada de una tabla como un documento basado en alguna consulta. Como las herramientas pueden hacer filtrado, las bases de datos son mejores para ello. Se puede sobreescribir el método getSelectorSQLStatement() para que devuelva el string de la consulta apropiada.
  • El analizador usa el método getString() del objeto result-set para obtener la representación string del valor de una columna. Esto funciona bien con texto, números, etc, pero no funciona bien con datos binarios. Aunque los datos binarios puedan ser representados como texto, podrían no ser apropiados para ciertas tareas. El analizador tampoco trata con tipos definidos por el usuario disponibles con SQL3/JDBC 2.0.

    Podemos resolver ambos problemas sobreescribiendo el método generateSAXEventForColumn() y proporcionando una implementación adecuada.

Implementar el API DOM para Bases de Datos

Para construir un árbol DOM para una tabla de base de datos, podríamos iterar sobre las filas y columnas y construir nodos para un árbol mientras los visitamos. O podríamos emplear otra librería, como la implementación JAXP de Sun, que construye un árbol DOM desde un stream de eventos SAX. La última aproximación es más simple, y requiere menos codificación porque reutiliza una facilidad existente. Para implementar el API DOM usando dicha aproximación sólo necesitamos una reutilización adecuada del analizador SAX para bases de datos que implementamos anteriormente.

La clase JDBCDOMParser.java implementa el API DOM para bases de datos:

package dbxml.dom;
import java.io.IOException;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import com.sun.xml.tree.XmlDocumentBuilder;
import dbxml.sax.*;

public class JDBCDOMParser {
	
    public static Document createDocument(JDBCInputSource 
		inputSource)throws SAXException, IOException {

        XmlDocumentBuilder documentBuilder = new XmlDocumentBuilder();
        JDBCSAXParser saxParser = new JDBCSAXParser();
        documentBuilder.setParser(saxParser);
        saxParser.parse(inputSource);
        return documentBuilder.getDocument();
    }
}

La implementación de la clase JDBCDOMParser es sencilla. Usa la clase XmlDocumentBuilder proporcionada por JAXP para construir un documento DOM desde un stream de eventos SAX. JDBCDOMParser tiene sólo un método: createDocument(), que toma una fuente de datos JDBC como argumento. El método crea un JDBCSAXParser y selecciona un analizador para el objeto XmlDocumentBuilder. Luego dispara el analizador y devuelve el documento resultante desde el objeto XmlDocumentBuilder. Internamente el objeto XmlDocumentBuilder responde a los eventos SAX generados por el objeto JDBCSAXParser para construir un documento DOM.

Usar el API SAX para Base de Datos

Ya hemos visto un ejemplo de uso del API SAX para bases de datos para implementar un API DOM para bases de datos. Ahora veremos otro ejemplo de uso del API SAX para bases de datos. En esta sección, lo usaremos para integrarlo con XT -- un procesador XSLT escrito en Java. Con dicha integración, podemos aplicar una hoja de estilos XSLT directamente a los documentos XML virtuales almacenados en una base de datos.

Hemos envuelto la lógica de creación de una fuente SAX para una fuente de base de datos dada y la hemos procesado con la hoja de estilo XSLT dada para producir un fichero de salida en la clase JDBCXSLProcessor, que está basada en com.jclark.xsl.sax.Driver de XT. El método principal toma tres argumentos: un fichero de propiedades de una base de datos, un fichero de hoja de extilo XSLT y un fichero de salida.

Como veremos abajo, podemos usar esta aproximación para generar páginas HTML directamente sin incurrir en la penalización de tener que crear el fichero XML intermedio. Además, veremos cómo usar la integración del API SAX para bases de datos y XT para convertir una base de datos en un fichero XML no virtual.

(Aquí puedes ver el código fuente de DBCXSLProcessor.java)

Generar páginas HTML directamente desde una base de datos usando una hoja de estilo XSLT

Aquí vemos una sencilla hoja de estilo que formatea un documento XML altamente regular representado por una tabla de base de datos. La tabla de la base de datos es formateada como una tabla HTML. Se puede usar la hoja de estilo genérica createTable.xsl para transformar cualquier documento XML con una estructura parecida a una tabla. La hoja de estilo usa nombres de marcas para columnas como cabeceras de tablas.

(Aquí puedes ver el código de la hoja de estilo createTable.xsl

Convertir una base de datos a XML con una hoja de estilo XSLT

Aunque la mayoría de las aplicaciones funcionan con los APIs SAX o DOM, podríamos necesitar obtener un fichero XML en algunas situaciones. Por ejemplo, necesitamos un documento XML para usar una herramienta que no funciona con ninguno de estos APIs. Aquí, sugerimos una forma de convertir una base de datos en un fichero XML. Con esta aproximación, escribimos una hoja de estilo XSLT para hacer la transformación de identidad. Usando dicha hoja de estilo con el API SAX para bases de datos se construirá un documento XML que representa una tabla en la base de datos. Hemos proporcionado una hoja de estilo -- identity.xsl -- para transformación de identidad que funciona con la implementación actual de XT.

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="@*|*">
    <xsl:copy>
  <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>
</xsl:stylesheet>

Observa que aunque una hoja de estilo XSLT realiza fácilmente la transformación de identidad, no es muy eficiente y va a través de la lógica compleja de propósito general para aplicar toda la hoja de estilo. Esta ineficacia puede ser un problema para tablas de bases de datos con un gran número de registros. Una alternativa es escribir una aplicación que realice la transformación de identidad. Dicha aplicación escucharía los eventos SAX y crearía elementos XML y datos en respuesta a esos eventos, resultando en un documento XML que representa la tabla de la base de datos.

Usar el API DOM para Bases de Datos

Para la mayoría de las situaciones, el API SAX para bases de datos es más eficiente que el API DOM. Sin embargo, algunas aplicaciones necesitan acceso aleatorio a documentos XML y por lo tanto requieren la estructura en forma de árbol que ofrece el API DOM para bases de datos.

Integrar el API DOM para Bases de Datos con el Procesador XQL

XML Query Language (XQL), un lenguaje de consultas para documentos XML, tiene una síntaxis similar a los patrones Xpath. Aquí, hemos integrado nuestro analizador DOM para bases de datos con GMD-IPSI's XQL Engine. Con dicha integración, podemos realizar consultas al estilo SQL sobre el documento XML que representa la tabla de la base de datos.

Como un ejemplo de integración, proporcionamos un sencillo shell para consultar el documento XML obtenido de la tabla de la base de datos. La clase JDBCXQLProcessor crea un entorno "shell", que toma las consultas del usuario e imprime el documento resultante. El método processQueries() puede trabajar con cualquier objeto Document -- no sólo con objetos creados por el JDBCDOMParser. Lee el string de consulta desde System.in, e imprime el resultado sobre System.out. El método main() crea un objeto JDBCInputSource a partir de su argumento y lo usa con JDBCDOMParser para obtener un objeto Document que corresponde con la tabla de la base de datos.

Aquí tienes el código fuente de JDBCXQLProcessor.java

Como nota lateral, escribir una conversión base-de-datos-a-XML es fácil con XMLWriter y JDBCDOMParser. Sólo obtenemos el objeto Document desde JDBCDOMParser y escribimos el fichero deseado con XMLWriter.write(Document).

Conclusión

En este artículo, hemos explicado el uso de los APIS de XML para bases de datos para atrapar la información residente en bases de datos. Con estos APIs, podemos evitar el coste asociado de convertir una base de datos en documentos XML y el coste de mantenerlos sincronizados. Presentamos una implementación Java de los APIs SAX y DOM para bases de datos, que pueden trabajar con cualquier base de datos con un driver JDBC. Hemos presentado una integración del API SAX para bases de datos con XT. Hemos ilustrado el uso de dicha integración para crear directamente páginas HTML desde una base de datos y convertir las bases de datos en documentos XML. Finalmente, hemos integrado el API DOM para bases de datos con un procesador XQL.

Desde aquí puedes bajarte un fichero zip con todos los códigos fuentes de este artículo: jw-01-dbxml.zip

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
ARTÍCULO ANTERIOR

SIGUIENTE ARTÍCULO