Tutorial H2 Database

La base de datos H2 es un base de datos relacional programada integramente en Java. Una de las características más importantes a consecuencia de esto es que podemos integrarla completamente en nuestras aplicaciones Java y acceder a a ella lanzando SQL directamente, sin tener que pasar por una conexión a través de sockets, como ocurirría con MySQL, Oracle, Postgress, etc. (aunque también es posible utilizarla a través de conexiones JDBC externas). Esto implica una mayor velocidad y una mejor integración, como ahora veremos.
Cuando trabajamos con la base de datos dentro de nuestra aplicaciones, se llama modo “embedded” (incrustado). Una vez que nuestra aplicación ha abierto la base de datos para trabajar con ella, esta queda fisicamente bloqueada, no permitiendo que otro motor H2 (en otra aplicación Java por ejemplo) pueda acceder a ella. Esto permite mantener la integridad del fichero físico de la base de datos, ya que si dos motores H2 escribieran a la vez podrían corromperla.

Usar y crear bases de datos incrustadas con H2 desde Java es realmente facil. Primero tenemos que bajarnos el último jar de http://www.h2database.com e incluirlo en nuestro CLASSPATH (o en el directorio WEB-INF/lib de nuestra aplicación WAR).

El siguiente código abre una conexión contra una base de datos, definiendo donde se encuentra físicamente en nuestro disco, sin tener que especificar ningún host, ip o puerto, ya que no hay conexión a través de sockets, sino que se accede directamenete al disco.

import java.sql.*;
public class Test {
  public static void main(String[] a) throws Exception {
    Class.forName("org.h2.Driver");
    Connection conn = DriverManager.getConnection("jdbc:h2:/home/vilches/h2/test", "sa", "");
  }
}

Si al abrir una conexión no existe físicamente los archivos de la base de datos, H2 los creará. Por ejemplo:

/home/vilches/h2/test.2.log.db
/home/vilches/h2/test.data.db
/home/vilches/h2/test.index.db
/home/vilches/h2/test.trace.db

La ruta a la base de datos elegida es /home/vilches/h2/test. Esto significa que se utilizará el directorio /home/vilches/h2/ y que en ese directorio se crearán los ficheros de la base de datos, que empezarán por test.* y tendrán distintas extensiones. De esta manera, cambiando solo el nombre del fichero es posible tener varias bases de datos el el mismo directorio, ya que empezarán con nombres distintos (por ejemplo /home/vilches/h2/test y /home/vilches/h2/otro).

Una conexión por sesión es suficiente

Una de las mayores ventajas de usar H2 de forma embebida es que podemos utilizar conexiones sin necesidad de un pool de conexiones. Podemos, por ejemplo, utilizar una única conexión para toda nuestra aplicación y no cerrarla nunca (o cerrarla al salir de la aplicación). En caso de aplicaciones Web, podemos crear una conexión por sesión con un listener que implemente HtppSessionListener:

package test;
import javax.servlet.*;
import javax.servlet.http.*;
import java.sql.*;
import java.io.*;
public class MySessionListener implements HttpSessionListener {
    public Connection openConnection() throws SQLException, ClassNotFoundException {
        Class.forName("org.h2.Driver");
        return DriverManager.getConnection("jdbc:h2:/home/vilches/h2/test", "", "sa");
    }

    public void sessionCreated(HttpSessionEvent se) {
        try {
            se.getSession().setAttribute("h2.connection", openConnection());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void sessionDestroyed(HttpSessionEvent se) {
        try {
            Connection con = (Connection)se.getSession().getAttribute("h2.connection");
            con.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Para que funcione el listener es necesario darlo de alta en el web.xml:

<listener>
    <listener-class>test.MySessionListener</listener-class>
</listener>

Ahora, desde nuestros servlets o JSPs, podemos acceder a la conexión con:

Connection con = (Connection)se.getSession().getAttribute("h2.connection");

y podremos utilizarla sin preocuparnos por cerrarla.

Abriendo la base de datos al arrancar la aplicación

Una buena practica es abrir por lo menos una conexión al arrancar la aplicación, de esta manera nos aseguramos de que H2 abre fisicamente la base de datos en el disco y la bloquea para el resto de procesos. Para hacer esto, podemos ampliar el listener anterior para que implemente también ServletContextListener, el cual tiene dos métodos que se ejecutan cuando se crea y se destruye el contexto (es decir, se arranca y se para la aplicación web).

Además, vamos a definir la ruta de la base de datos como un parámetro del contexto en el WEB-INF/web.xml llamado “h2.database.path” que leeremos en el arranque (si el parámetro no existe, cogeremos la home del usuario actual con System.getProperty("user.home")) y utilizaremos para crear la url jdbc :

package test;
import javax.servlet.*;
import javax.servlet.http.*;
import java.sql.*;
import java.io.*;

public class MySessionListener implements ServletContextListener,
    HttpSessionListener {

    // Constante para el nombre de los ficheros
    private final static String DBFILENAME = "test";

    String url;
    Connection globalConnection;

    public void contextInitialized(ServletContextEvent sce) {
        // Leemos el parametro del contexto
        String spath = sce.getServletContext().getInitParameter("h2.database.path");
        if (spath == null) {
            // Si no existe, es la home del usuario
            spath = System.getProperty("user.home");
        }
        try {
            Class.forName("org.h2.Driver");
            File dbfile = new File(spath, DBFILENAME);
            url = "jdbc:h2:file:" + dbfile.getAbsolutePath().replaceAll("\\\\", "/");
            globalConnection = openConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Connection openConnection() throws SQLException {
        return DriverManager.getConnection(url, "", "sa");
    }

    public void contextDestroyed(ServletContextEvent sce) {
        try {
            globalConnection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void sessionCreated(HttpSessionEvent se) {
        try {
            se.getSession().setAttribute("h2.connection", openConnection());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void sessionDestroyed(HttpSessionEvent se) {
        try {
            Connection con = (Connection)se.getSession().getAttribute("h2.connection");
            con.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Para que funcione el listener es necesario darlo de alta en el web.xml y definir el parámetro de contexto con la ruta :

<context-param>
    <param-name>h2.database.path</param-name>
    <param-value>/home/vilches/h2</param-value>
</context-param>
<listener>
    <listener-class>test.MySessionListener</listener-class>
</listener>

Creando tablas al arrancar por primera vez

Ahora supongamos que queremos crear una serie de tablas cuando hemos arrancado la base de datos, pero solo la primera vez, cuando la base de datos está vacía. Para hacer esto vamos a verificar, antes de abrir la primera conexión, si la base de datos existe. Y si no existe, crearemos las tablas (después de abrir la primera conexión). Modificaremos el método contextInitialized para que quede así:

public void contextInitialized(ServletContextEvent sce) {
    // Leemos el parametro del contexto
    String spath = sce.getServletContext().getInitParameter("h2.database.path");
    if (spath == null) {
        // Si no existe, es la home del usuario
        spath = System.getProperty("user.home");
    }

    // Comprobamos si no existe la base de datos
    boolean exists = new File(spath, DBFILENAME+".data.db").exists();

    try {
        Class.forName("org.h2.Driver");
        File dbfile = new File(spath, DBFILENAME);
        url = "jdbc:h2:file:" + dbfile.getAbsolutePath().replaceAll("\\\\", "/");
        globalConnection = openConnection();

        // Si no existe, llamamos al metodo initDb()
        if (!exists) {
            initDb();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Ahora solo queda implementar initDb() a nuestro gusto.

Ejecutando un script SQL desde Java

Una buena practica es crear un fichero de script con las sentencias SQL y ejecutarlo directamente, sin tener que implementar, una a una, las sentencias de creación de tablas en nuestro código. El siguiente código lee un fichero de script llamado squema-h2.sql.txt situado dentro de la carpeta de de clases (es decir en /WEB-INF/classes/squema-h2.sql.txt) y lo ejecuta. Dentro de este script podemos incluir sentencias para crear tablas y añadir registros. El script necesita que cada sentencia acabe en punto y coma “;”, permite partir las sentencias en más de una línea e ignora los comentarios (líneas que empiezan por “–“, “#” y “//”) y líneas en blanco.

private void initDb() throws SQLException, IOException {
    InputStream is = getClass().getClassLoader().getResourceAsStream("squema-h2.sql.txt");
    BufferedReader br = new BufferedReader(new InputStreamReader(is));
    Connection connection = null;
    try {
        connection = openConnection();
        String line = br.readLine();
        StringBuilder statement = new StringBuilder();
        while (line != null) {
            line = line.trim();
            if (!line.startsWith("--") && !line.startsWith("#") && !line.startsWith("//")) {
                statement.append(line);
                if (line.endsWith(";")) {
                    executeLine(connection, statement.toString());
                    statement = new StringBuilder();
                }
            }
            line = br.readLine();
        }
        if (statement.length() > 0) {
            executeLine(connection, statement.toString());
        }
    } finally {
        try {
            br.close();
        } catch (Exception e) {;}
        try {
            if (connection != null) connection.close();
        } catch (Exception e) {;}
    }
}

private void executeLine(Connection connection, String statement) throws SQLException {
    PreparedStatement pstmt = connection.prepareStatement(statement);
    pstmt.execute();
    pstmt.close();
    System.out.println("Ejecutando "+statement);
}

El script /WEB-INF/classes/squema-h2.sql.txt podría ser:

CREATE TABLE USUARIO (
	ID INT AUTO_INCREMENT,
	LOGIN VARCHAR(25) NOT NULL UNIQUE,
	PASSWORD VARCHAR(25) NOT NULL,
	NOMBRE VARCHAR(100) NOT NULL,
	EMAIL VARCHAR(50),
	WEB	VARCHAR(100),

	PRIMARY KEY(ID)
);

-- Esto es un comentario
// Esto también
# Y esto también lo es

insert into usuario values (null, 'admin','admin123','Alberto Vilches',
	'email@sitio.com', 'http://albertovilches.com');

(Para más información sobre la sintaxis SQL de creación de tablas de H2, consultar H2 SQL Grammar reference)

Finalmente, creamos un JSP de ejemplo que liste los registros de la tabla para ver si todo funciona correctamente:

<%@ page import="java.sql.*" %>
<html>
  <body>
  <%
      Connection con = (Connection) request.getSession().getAttribute("h2.connection");

      PreparedStatement stmt = con.prepareStatement("SELECT * FROM USUARIO");
      ResultSet rst = stmt.executeQuery();
      while (rst.next()) {
      %>

  <%=rst.getString("LOGIN")%>, mailto:<%=rst.getString("EMAIL")%><br/>

      <%
      }

  %>
  </body>
</html>

Resumen

Con esto ya tendríamos un acceso a base de datos muy ligero y rápido disponible en nuestras aplicaciones Web. La ruta de la base de datos irá definida en el parámetro “h2.database.path” en el web.xml. El listener se encargará de crear la base de datos con las todas las tablas y mantenerla siempre abierta mientras esté activa la aplicación Web. También tendremos en la sesión siempre una conexión disponible lista para usar.

Bájate el código de ejemplo completo: War de ejemplo con H2 incrustado

3 thoughts on “Tutorial H2 Database

  1. Excelente explicación. Estoy por comenzar una aplicación que utiliza H2 y la verdad no sabía absolutametne nada de la base de datos y ahora con esto ya me hice una idea.

  2. Gracias por la publicacion, me ha sido de gran ayuda, estoy empezando con H2.
    Tengo una pregunta, el tema es que creo una base de datos, y si creo las tablas e inserto datos desde codigo todo OK, pero si lo hago con la consola esta que lleva h2 para manejar las tablas no me funciona.Es como si lo que creo por consola sólo apareciera por consola y lo que creo con java sólo me apareciera con java. Mi idea es crear una base de datos y desde la consola insertar productos y luego desde java hacer las consultas pertinentes a estos. Sabrías indicarme cómo hacerlo?Muchas Gracias! :D

  3. Ya está claro, me acaba de salir :p Resulta que al entrar en la consola me faltaba una carpeta en el path por lo que realmente tenia otra base de datos diferente. jeje.

Comments are closed.