spring-projects / spring-data-relational

Spring Data Relational. Home of Spring Data JDBC and Spring Data R2DBC.
https://spring.io/projects/spring-data-jdbc
Apache License 2.0
753 stars 345 forks source link

`io.r2dbc.spi.Parameter` not considered a simple type #1696

Closed rsmidt closed 8 months ago

rsmidt commented 9 months ago

I'm currently struggling to use a simple tsrange in a template entity insert.

Reading is fine, as I can utilize a simple Converter<String, MyTsRangeType>. But doing the same for writing (Converter<MyTsRangeType, String>) does not work:

column "my_column" is of type tsrange but expression is of type character varying

Is there a proper way for dealing with ranges?

mp911de commented 9 months ago

You need to apply casting on the SQL level. Alternatively, you can provide a converter from MyTsRangeType to Parameter if you can provide the Postgres Type OID.

The driver doesn't provide any value objects for ranges, therefore you must bind the value as string and tell the driver to use tsrange

R2DBC's Parameters.in(…) along with the driver's PostgresTypes.lookupType("tsrange") should return you the type descriptor required for Parameters.in(myTsRangeType.toString(), typeDesc).

Let me know whether this helps.

rsmidt commented 9 months ago

Hey @mp911de, thanks for your swift reply.

Ideally, I could avoid doing it on the SQL level and fully rely on the abstractions already there. Following your feedback, I tried the following (excuse the Kotlin):

// Taken from PostgresTypes.from(...).lookupType("tsrange")
private val PG_RANGE_TYPE = PostgresTypes.PostgresType(3908, 3908, 3909, 3909, "tsrange", "R")

@WritingConverter
object TsRangeSerializer : Converter<TsRange, Parameter> {
    override fun convert(source: TsRange): Parameter {
        return Parameters.`in`(PG_RANGE_TYPE, source.toPostgresValue())
    }
}

fun TsRange.toPostgresValue(): String =
    "[\"${start.format(PG_TIMESTAMP_FORMATTER)}\",\"${end.format(PG_TIMESTAMP_FORMATTER)}\"]"

Unfortunately, now it fails with an error I don't fully comprehend:

java.lang.IllegalArgumentException: Cannot encode null parameter of type io.r2dbc.spi.Parameters$InParameter at io.r2dbc.postgresql.codec.DefaultCodecs.encodeNull(DefaultCodecs.java:302)

I don't understand why it tries to bind to null?

mp911de commented 9 months ago

null values are not using the Converter, instead, we use the property (converted) type and not a value.

You could register a BeforeSaveCallback and post-process the OutboundRow to replace the value with a proper Parameters.in(…) null value wrapped in a Spring org.springframework.r2dbc.core.Parameter.

rsmidt commented 9 months ago

Maybe I'm misunderstanding something, but the value is not null in this case. The column itself is actually not nullable. That's why I expect it will never try to bind anything nullish.

mp911de commented 9 months ago

Can you provide the full stack trace and a minimal sample (SQL, domain types, converters, test)? Happy to debug the issue here once I have a bit of code that gets me started.

rsmidt commented 9 months ago

Thank you! Here's the repo: https://github.com/rsmidt/spring-data-r2dbc-range-types

It's written in Kotlin. I can change it to Java if required. It's expecting Postgres running on port 5432, configured in the application.properties (there's also a docker-compose.yml).

Please let me know if I can help in any way.

mp911de commented 8 months ago

I moved this ticket into Spring Data Relational as Spring Data R2DBC is part of the Relational project.

This is a bug where we attempt an entity conversion by extracting the identifier.

mp911de commented 8 months ago

That's fixed now. Care to upgrade to 3.2.1-SNAPSHOT of spring-data-r2dbc and retest whether the fix addresses your problem?

rsmidt commented 8 months ago

Stupid question, but where is the snapshot being published? If I try to override it like this:

    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
    implementation("org.springframework.data:spring-data-r2dbc:3.2.1-SNAPSHOT")

It fails:

Could not find org.springframework.data:spring-data-r2dbc:3.2.1-SNAPSHOT. Required by: project : Could not find org.springframework.data:spring-data-r2dbc:3.2.1-SNAPSHOT. Required by: project : > org.springframework.boot:spring-boot-starter-data-r2dbc:3.2.0

mp911de commented 8 months ago

No worries, the repo is at https://repo.spring.io/snapshot. The Maven declaration would be:

<repository>
  <id>spring-snapshot</id>
  <name>Spring Snapshot Repository</name>
  <url>https://repo.spring.io/snapshot</url>
</repository>
rsmidt commented 8 months ago

Yes! It's working now directly using the entity template.

Thank you very much for the swift fix, it's much appreciated.