Insertar y recuperar objetos java.time.LocalDate hacia/desde una base de datos SQL como H2
¿Cómo insertar y recuperar tipos java.time , como LocalDate
a través de JDBC , en una base de datos SQL como el motor de base de datos H2 ?
La forma antigua de usar PreparedStatement::setDate
y ResultSet::getDate
funciona para el java.sql.Date
tipo 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 ?
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.Calendar
y las java.sql
clases relacionadas como java.sql.Date
son 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.
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
/ getLocalDate
tipos de métodos, el comité JDBC agregó setObject
/ getObject
mé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 LocalDate
se convierte a un DATE
tipo 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 Object
objeto resultante, podemos pasar un argumento adicional, el Class
del 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 LocalDate
valores 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 LocalDate
en un java.sql.Date
objeto utilizando nuevos métodos agregados a esa clase anterior.
java.sql.Date mySqlDate = java.sql.Date.valueOf( myLocalDate );
Luego pase al PreparedStatement::setDate
método.
preparedStatement.setDate ( 1, mySqlDate );
Para recuperar de la base de datos, llame ResultSet::getDate
para obtener un java.sql.Date
objeto.
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 DataSource
implementación desde la cual obtener una conexión. Y esta vez probando LocalDate.MIN
que 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
, Calendar
y 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.