【问题标题】:Insert & fetch java.time.LocalDate objects to/from an SQL database such as H2在 SQL 数据库(例如 H2)中插入和获取 java.time.LocalDate 对象
【发布时间】:2017-08-19 18:13:59
【问题描述】:

如何通过JDBCjava.time 类型如LocalDate 插入和获取到SQL 数据库如H2 Database Engine

使用 PreparedStatement::setDateResultSet::getDate 的旧方法适用于旧的 java.sql.Date 类型。我想避免使用这些麻烦的旧日期时间类。

通过JDBC driver 发送 java.time 类型的现代方式是什么?

【问题讨论】:

    标签: java sql date jdbc h2


    【解决方案1】:

    我们有两种通过 JDBC 交换 java.time 对象的途径:

    • 符合 JDBC 4.2 的驱动程序
      如果您的 JDBC 驱动程序符合 JDBC 4.2 specification 或更高版本,您可以直接处理 java.time 对象。
    • JDBC 4.2 之前的旧驱动程序
      如果您的 JDBC 驱动程序尚不符合 JDBC 4.2 或更高版本,那么您可以将 java.time 对象短暂地转换为其等效的 java.sql 类型,反之亦然.寻找添加到旧类的新转换方法。

    java.util.Datejava.util.Calendar 等遗留日期时间类和java.sql.Date 等相关java.sql 类非常混乱。使用设计不佳的黑客方法构建,它们已被证明是有缺陷的、麻烦的和令人困惑的。尽可能避免使用它们。现在被 java.time 类所取代。

    JDBC 4.2 兼容驱动程序

    H2 的内置 JDBC 驱动程序(截至 2017 年 3 月)似乎符合 JDBC 4.2。

    兼容的驱动程序现在可以识别 java.time 类型。但 JDBC 委员会并没有添加 setLocalDate/getLocalDate 之类的方法,而是添加了 setObject/getObject 方法。

    要将数据发送到数据库,只需将您的 java.time 对象传递给PreparedStatement::setObject。您传递的参数的 Java 类型由驱动程序检测并转换为适当的 SQL 类型。 Java LocalDate 被转换为 SQL DATE 类型。有关这些映射的列表,请参阅JDBC Maintenance Release 4.2 PDF 文档的第 22 节。

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

    要从数据库中检索数据,请调用ResultSet::getObject。我们可以传递一个额外的参数,即我们期望接收的数据类型的Class,而不是强制转换生成的Object 对象。通过指定预期的类,我们获得type-safety 由您的IDE 和编译器检查和验证。

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

    这是一个完整的工作示例应用程序,展示了如何将 LocalDate 值插入和选择到 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 ( );
            }
        }
    }
    

    运行时。

    id_: e856a305-41a1-45fa-ab69-cfa676285461 |日期_:2017-03-26

    id_:a4474e79-3e1f-4395-bbba-044423b37b9f |日期_:2017-03-27

    id_: 5d47bc3d-ebfa-43ab-bbc2-7bb2313b33b0 |日期_:2017-03-28

    不合规的驱动程序

    对于H2,上面显示的代码是我推荐你走的路。但是仅供参考,对于其他不符合 JDBC 4.2 的数据库,我可以向您展示如何在 java.time 和 java.sql 类型之间进行简单转换。如下所示,这种转换代码当然可以在 H2 上运行,但是现在这样做很愚蠢,因为我们有上面显示的更简单的方法。

    要将数据发送到数据库,请使用添加到旧类的新方法将您的 LocalDate 转换为 java.sql.Date 对象。

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

    然后传递给PreparedStatement::setDate 方法。

    preparedStatement.setDate ( 1, mySqlDate );
    

    要从数据库中检索,请调用ResultSet::getDate 以获取java.sql.Date 对象。

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

    然后立即转换为LocalDate。您应该尽可能简短地处理 java.sql 对象。仅使用 java.time 类型完成所有业务逻辑和其他工作。

    LocalDate myLocalDate = mySqlDate.toLocalDate();
    

    这是一个完整的示例应用程序,展示了在 H2 数据库中如何使用 java.sql 类型和 java.time 类型。

    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 ( );
            }
        }
    }
    

    为了好玩,让我们尝试另一个。这次using a DataSource implementation 从中获得连接。这次尝试LocalDate.MIN,这是大约十亿年前在 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 |日期_:-999999999-01-01


    关于java.time

    java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

    要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

    Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

    您可以直接与您的数据库交换 java.time 对象。使用符合JDBC 4.2 或更高版本的JDBC driver。不需要字符串,不需要java.sql.* 类。 Hibernate 5 & JPA 2.2 支持 java.time

    从哪里获取 java.time 类?

    ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

    【讨论】:

    • 即使使用“兼容驱动程序”,存储的日期仍然在 java.sql.date 中,而不是在 LocalDate 中。如果您将 LocalDate.MIN 存储在表中,则检索它的日期将以 'id_: b8d9be62-71cc-4ea9-86e2-cb55138379d1 | 结尾日期_:+169087565-03-15'
    • @Guangliang 不,我没有看到你的问题。存储和检索LocalDate.MIN:-999999999-01-01 时,我得到相同的值。请参阅我附加到答案中的使用 LocalDate.MIN 的新示例应用源代码。
    • 对我来说,setObject 的处理方式存在一个问题,即某处存在不需要的时区转换 - LocalDate 对象表示的日期向后移动了一天(我是在时区 GMT+1)!这是不合规司机的标志吗?还是不兼容的驱动程序根本就没有 setObject 方法?
    • @codeling 不,有 no 正在使用存储在 SQL 标准 DATE 类型列中的 LocalDate 对象进行此类调整.如果您存储 2019 年 1 月 23 日,您绝对会得到相同的回报。您可能会想到可怕的java.sql.Date 类,它假装 表示仅表示日期,但实际上带有时间和UTC。永远不要使用那个遗留类。仅使用 java.time 类进行所有日期到 e 处理。如果您发现一些令人费解的行为,请使用示例代码发布一个新问题。
    • 是的,我的错 - 我错过了数据库列是 DATETIME 类型,而不是 DATE。对不起,噪音!
    猜你喜欢
    • 1970-01-01
    • 2013-11-15
    • 1970-01-01
    • 1970-01-01
    • 2020-03-26
    • 2021-07-25
    • 2022-12-20
    • 2016-12-05
    • 1970-01-01
    相关资源
    最近更新 更多