spring-data-relational icon indicating copy to clipboard operation
spring-data-relational copied to clipboard

Same type (java.sql.Timestamp) turns into different SQL-types

Open olereidar opened this issue 3 years ago • 11 comments

Hello, I am not sure where this happens, but I am having some trouble with ZonedDateTime.

I have read in the documentation that ZonedDateTime is not supported (https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#jdbc.entity-persistence.types), therefore I wrote a converter (ZonedDateTime -> java.sql.Timestamp).

I have the following data class:

data class OrderLineEvent(
  (...) other fields 
  val orderLineEventType: String? = null,
  val travelDate: ZonedDateTime? = null, 
  val timestamp:  LocalDateTime = LocalDateTime.now() // Actually a ZonedDateTime, but using LocalDateTime for illustration purposes 
) {
}

Converter:

import java.sql.Timestamp
import java.time.ZoneOffset
import java.time.ZonedDateTime

@Configuration
@EnableJdbcRepositories
class DataJdbcConfiguration : AbstractJdbcConfiguration() {

  @WritingConverter
  class ZonedDateTimeWriterConverter : Converter<ZonedDateTime, Timestamp> {

    override fun convert(date: ZonedDateTime): Timestamp {
      return Timestamp.valueOf(date.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime())
    }

  }

  @Bean
  override fun jdbcCustomConversions(): JdbcCustomConversions {
    return JdbcCustomConversions(listOf(ZonedDateTimeWriterConverter()))
  }
}

Repository:

@Repository
interface OrderLineEventRepository : CrudRepository<OrderLineEvent, Long>

Ok, here is the issue: I am unable to save this as a timestamp. The error is Translating SQLException with SQL state '42804', error code '0', message [ERROR: column "travel_date" is of type timestamp without time zone but expression is of type character varying which is quite odd.

After investigating this further (and enabling TRACE-logging), it seems like the parameter gets converted into a VARCHAR/string type(?) AND a different type than the `LocalDateTime

Look at logs below:

o.s.jdbc.core.StatementCreatorUtils      : Setting SQL statement parameter value: column index 11, parameter value [123.42], value class [java.lang.String], SQL type 12  
o.s.jdbc.core.StatementCreatorUtils      : Setting SQL statement parameter value: column index 12, parameter value [2021-11-17 08:31:28.731541], value class [java.sql.Timestamp], SQL type 93  
o.s.jdbc.core.StatementCreatorUtils      : Setting SQL statement parameter value: column index 13, parameter value [2021-11-17 08:31:28.73009], value class [java.sql.Timestamp], SQL type 12  

It seems like String and ZonedDateTime turns into the same SQL type.

String -> java.lang.String, SQL type 12
LocalDateTime -> java.sql.Timestamp, SQL type 93
ZonedDateTime -> java.sql.Timestamp, SQL type 12

olereidar avatar Nov 17 '21 09:11 olereidar

Same thing happens with a LocalDateTime converter. It gets the SQL type 12 (same as java.lang.String)

    override fun convert(date: ZonedDateTime): LocalDateTime {
      return date.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
    }

Logs:

o.s.jdbc.core.StatementCreatorUtils      : Setting SQL statement parameter value: column index 11, parameter value [123.42], value class [java.lang.String], SQL type 12  
o.s.jdbc.core.StatementCreatorUtils      : Setting SQL statement parameter value: column index 12, parameter value [2021-11-17 10:51:11.019172], value class [java.sql.Timestamp], SQL type 93  
o.s.jdbc.core.StatementCreatorUtils      : Setting SQL statement parameter value: column index 13, parameter value [2021-11-17T09:51:11.018157], value class [java.time.LocalDateTime], SQL type 12 

olereidar avatar Nov 17 '21 09:11 olereidar

Please provide a Minimimal Reproducable Example, preferable as a Github repository. Make sure to include the database, either as an in memory database or if that is not possible using Testcontainers.

schauder avatar Nov 17 '21 11:11 schauder

Here you go: https://github.com/olereidar/spring-jdbc-help

I used Testcontainers. Run the test via gradle

olereidar avatar Nov 17 '21 13:11 olereidar

We do make a mistake when determining the JdbcType to pass with the Timestamp.

You can work around that by returning a JdbcValue which already contains the JdbcType:

  @WritingConverter
  class ZonedDateTimeWriterConverter : Converter<ZonedDateTime, JdbcValue> {

    override fun convert(date: ZonedDateTime): JdbcValue {
      val valueOf = Timestamp.valueOf(date.withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime())
      return JdbcValue.of(valueOf, JDBCType.TIMESTAMP)

    }
  }

We should try to determine the JdbcType only after the conversion.

schauder avatar Nov 17 '21 15:11 schauder

Thank you, this works. Do you have any plans on fixing this, or do you consider this workaround a permanent solution?

EDIT: It only works partly. If the value is nullable (which it is in this case) it crashes when inserting null. Same error: is of type timestamp without time zone but expression is of type character varying

o.s.jdbc.core.StatementCreatorUtils      : Setting SQL statement parameter value: column index 3, parameter value [null], value class [null], SQL type 12 

Any ideas on workaround here?

olereidar avatar Nov 18 '21 06:11 olereidar

I don't consider this workaround a permanent solution. And I understand where the problem with null comes from, basically null gets a shortcut and doesn't use conversions, but apart from avoiding nulls I don't have a workaround at hand.

schauder avatar Nov 18 '21 07:11 schauder

Ok, thank you anyways! Let me know if you find a more permanent solution. Have a nice day.

olereidar avatar Nov 18 '21 07:11 olereidar

Somewhat related SO question https://stackoverflow.com/questions/62004522/convert-null-to-some-value-in-spring-converter

schauder avatar Nov 24 '21 12:11 schauder

Just stumbled upon the same problem as described by @olereidar .

Did you eventually change the design and use a different datatype in the entity?

huima avatar Dec 02 '22 10:12 huima

We are trying to now use OffsetDateTime in the entity, as the jdbc drivers seem to work properly with them and Spring conversion code is not needed then.

huima avatar Dec 02 '22 10:12 huima

@huima unfortunately no. We ended up using another library. Awesome if its fixed :D

olereidar avatar Dec 05 '22 07:12 olereidar