Insertar y recuperar objetos java.time.LocalDate hacia/desde una base de datos SQL como H2

Resuelto Basil Bourque asked hace 7 años • 1 respuestas

¿Cómo insertar y recuperar tipos java.time , como LocalDatea través de JDBC , en una base de datos SQL como el motor de base de datos H2 ?

La forma antigua de usar PreparedStatement::setDatey ResultSet::getDatefunciona para el java.sql.Datetipo heredado. Quiero evitar el uso de estas antiguas y problemáticas clases de citas.

¿ Cuál es la forma moderna de enviar tipos java.time a través de un controlador JDBC ?

Basil Bourque avatar Mar 27 '17 13:03 Basil Bourque
Aceptado

Tenemos dos rutas para intercambiar objetos java.time a través de JDBC:

  • Controladores compatibles con JDBC 4.2
    Si su controlador JDBC cumple con la especificación JDBC 4.2 o posterior, puede tratar directamente con los objetos java.time.
  • Controladores más antiguos, anteriores a JDBC 4.2
    Si su controlador JDBC aún no cumple con JDBC 4.2 o posterior, convierta brevemente sus objetos java.time a su tipo java.sql equivalente o viceversa. Busque nuevos métodos de conversión agregados a las clases antiguas.

Las clases heredadas de fecha y hora como java.util.Date, java.util.Calendary las java.sqlclases relacionadas como java.sql.Dateson un desastre terrible. Construidos con un enfoque pirateado mal diseñado, han demostrado ser defectuosos, problemáticos y confusos. Evítelos siempre que sea posible. Ahora reemplazado por las clases java.time.

Tabla de tipos de fecha y hora en Java (tanto heredado como moderno) y en SQL estándar

Controladores compatibles con JDBC 4.2

El controlador JDBC integrado para H2 (a partir de 2017-03) parece cumplir con JDBC 4.2.

Los controladores compatibles ahora conocen los tipos java.time. Pero en lugar de agregar setLocalDate/ getLocalDatetipos de métodos, el comité JDBC agregó setObject/ getObjectmétodos.

Para enviar datos a la base de datos, simplemente pase su objeto java.time a PreparedStatement::setObject. El controlador detecta el tipo Java del argumento pasado y lo convierte al tipo SQL apropiado. Un Java LocalDatese convierte a un DATEtipo SQL. Consulte la sección 22 del documento PDF de la versión de mantenimiento de JDBC 4.2 para obtener una lista de estas asignaciones.

myPreparedStatement.setObject ( 1 , myLocalDate ); // Automatic detection and conversion of data type.

Para recuperar datos de la base de datos, llame a ResultSet::getObject. En lugar de convertir el Objectobjeto resultante, podemos pasar un argumento adicional, el Classdel tipo de datos que esperamos recibir. Al especificar la clase esperada, su IDE y compilador verifican y verifican la seguridad de tipos .

LocalDate localDate = myResultSet.getObject ( "my_date_column_" , LocalDate.class ); 

Aquí hay una aplicación de ejemplo funcional completa que muestra cómo insertar y seleccionar LocalDatevalores en una base de datos H2.

package com.example.h2localdate;

import java.sql.*;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.UUID;

/**
 * Hello world!
 */
public class App {
    public static void main ( String[] args ) {
        App app = new App ( );
        app.doIt ( );
    }

    private void doIt ( ) {
        try {
            Class.forName ( "org.h2.Driver" );
        } catch ( ClassNotFoundException e ) {
            e.printStackTrace ( );
        }

        try (
            Connection conn = DriverManager.getConnection ( "jdbc:h2:mem:trash_me_db_" ) ;
            Statement stmt = conn.createStatement ( ) ;
        ) {
            String tableName = "test_";
            String sql = "CREATE TABLE " + tableName + " (\n" +
                "  id_ UUID DEFAULT random_uuid() PRIMARY KEY ,\n" +
                "  date_ DATE NOT NULL\n" +
                ");";
            stmt.execute ( sql );

            // Insert row.
            sql = "INSERT INTO test_ ( date_ ) " + "VALUES (?) ;";
            try ( PreparedStatement preparedStatement = conn.prepareStatement ( sql ) ; ) {
                LocalDate today = LocalDate.now ( ZoneId.of ( "America/Montreal" ) );
                preparedStatement.setObject ( 1, today.minusDays ( 1 ) );  // Yesterday.
                preparedStatement.executeUpdate ( );
                preparedStatement.setObject ( 1, today );                  // Today.
                preparedStatement.executeUpdate ( );
                preparedStatement.setObject ( 1, today.plusDays ( 1 ) );   // Tomorrow.
                preparedStatement.executeUpdate ( );
            }

            // Query all.
            sql = "SELECT * FROM test_";
            try ( ResultSet rs = stmt.executeQuery ( sql ) ; ) {
                while ( rs.next ( ) ) {
                    //Retrieve by column name
                    UUID id = rs.getObject ( "id_", UUID.class );  // Pass the class to be type-safe, rather than casting returned value.
                    LocalDate localDate = rs.getObject ( "date_", LocalDate.class );  // Ditto, pass class for type-safety.

                    //Display values
                    System.out.println ( "id_: " + id + " | date_: " + localDate );
                }
            }

        } catch ( SQLException e ) {
            e.printStackTrace ( );
        }
    }
}

Cuando corre.

id_: e856a305-41a1-45fa-ab69-cfa676285461 | fecha_: 2017-03-26

id_: a4474e79-3e1f-4395-bbba-044423b37b9f | fecha_: 2017-03-27

id_: 5d47bc3d-ebfa-43ab-bbc2-7bb2313b33b0 | fecha_: 2017-03-28

Conductores no conformes

Para H2, el código que se muestra arriba es el camino que recomiendo tomar. Pero para su información, para otras bases de datos que aún no cumplen con JDBC 4.2, puedo mostrarles cómo convertir brevemente entre los tipos java.time y java.sql. Este tipo de código de conversión ciertamente se ejecuta en H2 como muestro a continuación, pero hacerlo es una tontería ahora que tenemos el enfoque más simple que se muestra arriba.

Para enviar datos a la base de datos, conviértalo LocalDateen un java.sql.Dateobjeto utilizando nuevos métodos agregados a esa clase anterior.

java.sql.Date mySqlDate = java.sql.Date.valueOf( myLocalDate );

Luego pase al PreparedStatement::setDatemétodo.

preparedStatement.setDate ( 1, mySqlDate );

Para recuperar de la base de datos, llame ResultSet::getDatepara obtener un java.sql.Dateobjeto.

java.sql.Date mySqlDate = myResultSet.getDate( 1 );

Luego conviértalo inmediatamente a LocalDate. Debe manejar los objetos java.sql lo más brevemente posible. Realice toda su lógica de negocios y otros trabajos utilizando solo los tipos java.time.

LocalDate myLocalDate = mySqlDate.toLocalDate();

Aquí hay una aplicación de ejemplo completa que muestra este uso de tipos java.sql con tipos java.time en una base de datos H2.

package com.example.h2localdate;

import java.sql.*;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.UUID;

/**
 * Hello world!
 */
public class App {
    public static void main ( String[] args ) {
        App app = new App ( );
        app.doIt ( );
    }

    private void doIt ( ) {
        try {
            Class.forName ( "org.h2.Driver" );
        } catch ( ClassNotFoundException e ) {
            e.printStackTrace ( );
        }

        try (
            Connection conn = DriverManager.getConnection ( "jdbc:h2:mem:trash_me_db_" ) ;
            Statement stmt = conn.createStatement ( ) ;
        ) {
            String tableName = "test_";
            String sql = "CREATE TABLE " + tableName + " (\n" +
                "  id_ UUID DEFAULT random_uuid() PRIMARY KEY ,\n" +
                "  date_ DATE NOT NULL\n" +
                ");";
            stmt.execute ( sql );

            // Insert row.
            sql = "INSERT INTO test_ ( date_ ) " + "VALUES (?) ;";
            try ( PreparedStatement preparedStatement = conn.prepareStatement ( sql ) ; ) {
                LocalDate today = LocalDate.now ( ZoneId.of ( "America/Montreal" ) );
                preparedStatement.setDate ( 1, java.sql.Date.valueOf ( today.minusDays ( 1 ) ) );  // Yesterday.
                preparedStatement.executeUpdate ( );
                preparedStatement.setDate ( 1, java.sql.Date.valueOf ( today ) );  // Today.
                preparedStatement.executeUpdate ( );
                preparedStatement.setDate ( 1, java.sql.Date.valueOf ( today.plusDays ( 1 ) ) );  // Tomorrow.
                preparedStatement.executeUpdate ( );
            }

            // Query all.
            sql = "SELECT * FROM test_";
            try ( ResultSet rs = stmt.executeQuery ( sql ) ; ) {
                while ( rs.next ( ) ) {
                    //Retrieve by column name
                    UUID id = ( UUID ) rs.getObject ( "id_" );  // Cast the `Object` object to UUID if your driver does not support JDBC 4.2 and its ability to pass the expected return type for type-safety.
                    java.sql.Date sqlDate = rs.getDate ( "date_" );
                    LocalDate localDate = sqlDate.toLocalDate ();  // Immediately convert into java.time. Mimimize use of java.sql types.

                    //Display values
                    System.out.println ( "id_: " + id + " | date_: " + localDate );
                }
            }

        } catch ( SQLException e ) {
            e.printStackTrace ( );
        }
    }
}

Por diversión, probemos con otro. Esta vez usando una DataSourceimplementación desde la cual obtener una conexión. Y esta vez probando LocalDate.MINque es una constante desde hace unos mil millones de años en ISO 8601, -999999999-01-01.

package work.basil.example;

import java.sql.*;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.UUID;

public class LocalDateMin
{
    public static void main ( String[] args )
    {
        LocalDateMin app = new LocalDateMin();
        app.doIt();
    }

    private void doIt ()
    {
        org.h2.jdbcx.JdbcDataSource ds = new org.h2.jdbcx.JdbcDataSource();
        ds.setURL( "jdbc:h2:mem:localdate_min_example_db_;DB_CLOSE_DELAY=-1" );
        ds.setUser( "scott" );
        ds.setPassword( "tiger" );

        try (
                Connection conn = ds.getConnection() ;
                Statement stmt = conn.createStatement() ;
        )
        {
            String tableName = "test_";
            String sql = "CREATE TABLE " + tableName + " (\n" +
                    "  id_ UUID DEFAULT random_uuid() PRIMARY KEY ,\n" +
                    "  date_ DATE NOT NULL\n" +
                    ");";
            stmt.execute( sql );

            // Insert row.
            sql = "INSERT INTO test_ ( date_ ) " + "VALUES (?) ;";
            try ( PreparedStatement preparedStatement = conn.prepareStatement( sql ) ; )
            {
                LocalDate today = LocalDate.now( ZoneId.of( "America/Montreal" ) );
                preparedStatement.setObject( 1 , LocalDate.MIN );  // MIN =
                preparedStatement.executeUpdate();
            }

            // Query all.
            sql = "SELECT * FROM test_";
            try ( ResultSet rs = stmt.executeQuery( sql ) ; )
            {
                while ( rs.next() )
                {
                    //Retrieve by column name
                    UUID id = rs.getObject( "id_" , UUID.class );  // Pass the class to be type-safe, rather than casting returned value.
                    LocalDate localDate = rs.getObject( "date_" , LocalDate.class );  // Ditto, pass class for type-safety.

                    //Display values
                    System.out.println( "id_: " + id + " | date_: " + localDate );
                }
            }

        } catch ( SQLException e )
        {
            e.printStackTrace();
        }
    }
}

id_: 4b0ba138-d7ae-469b-854f-5cbe7430026f | fecha_: -999999999-01-01


Acerca de java.time

El marco java.time está integrado en Java 8 y versiones posteriores. Estas clases reemplazan a las antiguas y problemáticas clases de fecha y hora heredadas , como java.util.Date, Calendary SimpleDateFormat.

Para obtener más información, consulte el Tutorial de Oracle . Y busque Stack Overflow para obtener muchos ejemplos y explicaciones. La especificación es JSR 310 .

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes. Hibernate 5 & JPA 2.2 support java.time.

Where to obtain the java.time classes?

  • Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
    • Java 9 brought some minor features and fixes.
  • Java SE 6 and Java SE 7
    • Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
  • Android
    • Later versions of Android (26+) bundle implementations of the java.time classes.
    • For earlier Android (<26), the process of API desugaring brings a subset of the java.time functionality not originally built into Android.
      • If the desugaring does not offer what you need, the ThreeTenABP project adapts ThreeTen-Backport (mentioned above) to Android. See How to use ThreeTenABP….

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque avatar Mar 27 '2017 06:03 Basil Bourque