6

I'm having some trouble counting values from my data base with Java.

I'm trying to count some values by date from my DB, but they have hours, minutes and seconds. If I search with java.sql.Date's it just allows me to enter year, month and day, so the values with a date: year"-"month"-31 (hour:minutes:seconds != 00:00:00)" won't be counted.

Is there a way to add hours, minutes, and seconds to a java.sql.Date? If not, what would it be a better way to do it?

My code:

public static int getAmountPos(Connection con, Integer month, Integer year) {

        java.sql.Date date1 = java.sql.Date.valueOf(year+"-"+month+"-01");
        java.sql.Date date2 = java.sql.Date.valueOf(year+"-"+month+"-31");

        Integer amount = null;
        String sql = "SELECT COUNT(*) AS 'cantidad' "
                + " FROM table.point_of_service "
                + " WHERE registration_date between ? AND ?";

        PreparedStatement ps = con.getPreparedStatement(sql);
        ps.setDate(1, date1);
        ps.setDate(2, date2);

        ResultSet rs = ps.executeQuery();

        if(rs.next()) {
            amount = rs.getInt("cantidad");
        }

        return amount;
    }
ges
  • 121
  • 1
  • 1
  • 7

3 Answers3

8

The java.sql.Date class is not intended to hold information about time. All it holds is date (year, month, day). You should use java.sql.Timestamp for your needs.

  • 1
    @Gerard: Find the documentation link to `java.sql.Timestamp` [here](https://docs.oracle.com/javase/7/docs/api/java/sql/Timestamp.html?is-external=true). – Sameer Mirji Feb 16 '16 at 17:07
3

tl;dr

Change the code to use (a) java.time types and (b) the Half-Open approach to span-of-time.

String sql = "SELECT COUNT(*) AS 'count_' "
           + "FROM table_.point_of_service_ "
           + "WHERE registration_date_ >= ? "  // Greater-than-or-equal.
           + "AND WHERE registration_date_ < ? ; ";  // Less-than (not 'or-equal') is Half-Open approach.

PreparedStatement ps = con.getPreparedStatement(sql);
ps.setObject( 1, YearMonth.of( 2016 , Month.FEBRUARY ).atDay( 1 ) );  // Calling `setObject` to handle `LocalDate` in JDBC 4.2 and later.
ps.setObject( 2, YearMonth.of( 2016 , Month.FEBRUARY ).plusMonths( 1 ).atDay( 1 ) );

java.sql

The java.sql types are intended only for moving data in and out of the database. Generally should not be used for business logic or manipulating date-time values.

  • The java.sql.Date class represents date-only values without time-of-day and without time zone.
  • The converse for java.sql.Time, a time-of-day with no date nor time zone.
  • For a date-time, use java.sql.Timestamp. This value is always in UTC.

For the date-time, call getTimestamp on your ResultSet.

java.sql.Timestamp tsWhenRegistered = myResultSet.getTimestamp( "when_registered_" );

Of course, you must be calling upon a column with a matching data type in your database.

java.time

In Java 8 and later, use the java.time framework for your business logic.

Avoid the old java.util.Date/.Calendar classes as they have proven to be troublesome, confusing, and flawed. They have been supplanted by the java.time classes.

Convert from java.sql types to java.time types as soon as you have grabbed them from the database. Hopefully JDBC drivers will be updated to deal with java.time types directly, but until then do the conversion yourself.

Instant

An Instant is a moment on the timeline in UTC.

Instant instant = tsWhenRegistered.toInstant(); // From java.sql to java.time.

Time Zone

Apply a time zone (ZoneId) as makes sense in your context. Let's say your business is in Québec.

ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = instant.atZone( zoneId );

YearMonth

Now, the other way from java.time to java.sql as seen in your Question. Rather than pass Integer objects for year and month, use the YearMonth class. Remember these java.time classes are built into Java now. So you can make use of them throughout your code. This gives you type-safety and compiler checks, and a guarantee of valid values.

YearMonth yearMonth = YearMonth.of( 2016 , Month.FEBRUARY ); // Pass Month enum for type-safety and range-checking.
YearMonth yearMonth = YearMonth.of( 2016 , 2 );  // Or, pass month number 1-12.

Re-do the method shown in Question assuming "when_registered_" is date-time value rather than date-only as you seem to be suggesting in your Question (not quite clear).

public static int getAmountPos( Connection con, YearMonth yearMonth ) {
…

By the way, a tip: Appending an underscore to your database names precludes collisions with keywords in SQL. The SQL standard explicitly promises to never use a trailing underscore.

Half-Open

Do not use the SQL BETWEEN. In date-time work the best practice is the “Half-Open” approach where the beginning is inclusive while the ending is exclusive. The search logic is greater-than-or-equal to the beginning and less-than the ending (not less-than-or-equal the ending). This avoids the problem of determining end-of-day to the split-second.

YearMonthLocalDate

From YearMonth we can get the first of that month and the first of the next month as date-only (LocalDate) values.

LocalDate localDateStart = yearMonthArg.atDay( 1 ); // Get first-of-month.
LocalDate localDateStop = yearMonthArg.plusMonths( 1 ).atDay( 1 ); // Get first of *next* month.

If your driver complies with JDBC 4.2 or later you should be able to pass these LocalDate objects to a PreparedStatement via the setObject method, and getObject for retrieving data.

Here is example code if your database column is defined as a standard SQL DATE type or a similar date-only type (no time-of-day, no time zone).

…
String sql = "SELECT COUNT(*) AS 'count_' "
           + "FROM table_.point_of_service_ "
           + "WHERE registration_date_ >= ? "  // Greater-than-or-equal.
           + "AND WHERE registration_date_ < ?";  // Less-than (not 'or-equal') is Half-Open approach.

PreparedStatement ps = con.getPreparedStatement(sql);
ps.setObject( 1, localDateStart );  // Calling `setObject` to handle `LocalDate` in JDBC 4.2 and later.
ps.setObject( 2, localDateStop );
…

If your driver cannot perform this way, substitute with java.sql.Date objects. The old date-time classes have new methods to facilitate such conversions. Here we need the java.sql.Date.valueOf method. Going the other direction when retrieving data from database, call toLocalDate.

…
ps.setDate( 1, java.sql.Date.valueOf( localDateStart ) );  // Converting `LocalDate` to `java.sql.Date`.
ps.setDate( 2, java.sql.Date.valueOf( localDateStop ) );
…

LocalDate + ZoneIdZonedDateTimejava.sql.Timestamp

If your database column is not defined as a date-only type, but a date-time type, then we need to query with date-time objects rather than the LocalDate objects seen above. That means OffsetDateTime objects if your date is meant for UTC, or ZonedDateTime if your date has meaning in a certain other time zone.

Keep in mind that LocalDate objects have no real meaning as the date varies around the world at any given moment. We must apply a time zone to get the first moment of each day as ZonedDateTime objects. Be aware that the first moment of the day is not always 00:00:00.0 because of Daylight Saving Time and perhaps other anomalies. To convert from java.time to java.sql, we extract Instant objects from the ZonedDateTime objects, and finally convert to java.sql.Timestamp.

public Integer countForYearMonth ( Connection connArg , YearMonth yearMonthArg ) {
    // CAUTION: Pseudo-code. Ignoring real-world issues such as closing resources and handling exceptions. Never run, so never tested.

    LocalDate localDateStart = yearMonthArg.atDay( 1 ); // Get first-of-month.
    LocalDate localDateStop = yearMonthArg.plusMonths( 1 ).atDay( 1 ); // Get first of *next* month.

    // Give those LocalDate objects real meaning by applying a time zone.
    ZoneId zoneId = ZoneId.of( "America/Montreal" );  // Perhaps pass as argument rather than hard-code a particular time zone.
    ZonedDateTime zdtStart = localDateStart.atStartOfDay( zoneId );  // Inclusive of first moment of February… 2016-02-01T00:00:00.0-05:00[America/Montreal]
    ZonedDateTime zdtStop = localDateStop.atStartOfDay( zoneId ); // Inclusive of first moment of March… 2016-03-01T00:00:00.0-05:00[America/Montreal]

    // Business logic is complete. So convert to java.sql for database access.
    java.sql.Timestamp tsStart = java.sql.Timestamp.from ( zdtStart.toInstant() );  // February 1st 2016 at 5 AM UTC.
    java.sql.Timestamp tsStop = java.sql.Timestamp.from ( zdtStop.toInstant() );  // March 1st 2016 at 5 AM UTC.

    Integer amount = null;
    String sql = "SELECT COUNT(*) AS 'count_' "
               + "FROM some_table_ "
               + "WHERE when_registered_ >= ? "  // Greater-than-or-equal.
               + "AND when_registered_ < ? "  // Less-than (not 'or-equal') is Half-Open approach.
               + "; ";

    PreparedStatement ps = connArg.getPreparedStatement(sql);
    ps.setDate( 1, tsStart );
    ps.setDate( 2, tsStop );

    ResultSet rs = ps.executeQuery();

    if( rs.next() ) {
        amount = rs.getInt( "count_" );
    }
    return amount;
}
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
1

I beg to differ with those stating that java.sql.date does not hold the time value. When you pass a long time value in the constructor it will hold that time information. You cannot manipulate that time information as those methods are deprecated, but the time information will persist and is not lost or truncated.

As per the java documentation:

Constructs a Date object using the given milliseconds time value. If the given milliseconds value contains time information, the driver will set the time components to the time in the default time zone (the time zone of the Java virtual machine running the application) that corresponds to zero GMT.

Now in answer to the question, you convert to Timestamp or Instant or to LocalDate to create the full date that will be used in the sql string. See this StackOverflow for conversion details. David Keen has a nice cheat sheet that is useful and Michael Sharhag has java 8 date and time conversion details.

Something like this should work:

String dateString = year.toString() + "-" + month.toString() + "-01T00:00:00.0Z";
Instant dtgInstant = Instant.parse(dateString);
java.sql.Date timestamp = new java.sql.Date(dtgInstant.toEpochMilli());
Community
  • 1
  • 1
Nelda.techspiress
  • 643
  • 12
  • 32
  • Not sure of the point to your Answer. The `java.sql.Date` class, like all of its related date-time classes, is a mess. And in this case in particular is a *bad* hack. This class extends from `java.util.Date` (a date + time-of-day) but its class doc clearly tells you to ignore this fact of inheritance. It, and you, are supposed to *pretend* that it has no time-of-day, while in fact it does have its time-of-day adjusted to first moment of the day in UTC. All of this is neatly encapsulated in the first sentence of the [accepted answer by Lewandowski](http://stackoverflow.com/a/35438625/642706). – Basil Bourque Jul 26 '16 at 22:44
  • 1
    Even though the java.sql.Date clas is a mess, the accepted answer leads one to believe that the time part of the class is not stored - "All it holds is date (year, month, day)." I'm merely pointing out that this statement is incorrect and the documentation indicates that as well. I work with legacy code that uses this practice quite a lot. And my answer shows a way to use the java.sql.Date as another solution to the question. – Nelda.techspiress Jul 27 '16 at 20:00
  • I find your Answer and comment self-contradictory. The `java.sql.Date` class *pretends* to represent a date-only value but as the documentation you yourself quoted says, it is actually a date + time-of-day adjusted to `00:00:00` in UTC time zone. Technically underneath it is a count of milliseconds since the epoch of beginning of 1970, inherited from the `java.util.Date` class. You say the time value passed to constructor is retained, “not lost or truncated” and then quote doc saying that the passed value is indeed modified. I suggest you look at the OpenJDK source code. – Basil Bourque Jul 27 '16 at 20:33
  • Furthermore, your mixing of java.time classes with the old java.sql (or java.util) classes seems silly. If you have java.time classes at your disposal, *use them* as they are meant to supplant the old classes. The old classes java.util.Date, java.util.Calendar, java.sql.Date, java.text.SimpleDateFormat and so on are all legacy now. I don’t mean to beat up your Answer, but it seems to be misguided advice. – Basil Bourque Jul 27 '16 at 20:36