mirromutth/r2dbc-mysql

Mapping Result of Primitive Type Field when using Native Query

btakeya opened this issue ยท 5 comments

Hi @mirromutth,
I faced this situation when I map result to object from native query.

Environment

  • Spring Boot 2.3.4.RELEASE
  • Kotlin (java compat w/ 1.8)
  • R2DBC-MySQL 0.8.2 (dev.miku:r2dbc-mysql:0.8.2.RELEASE)
  • if any other is needed, just let me know :)

Observation

  • My DB has a table as below:
field1 (varchar) field2 (varchar) field3 (double)
first row 1.0
second row 2.0
third row null
  • Query I used is as below:
SELECT field1, field2, field3
FROM my_table
...

and mapped into object with its result like this:

data class ResultObject(val f1: String, val f2: String, val f3: Double?)

val ro: ResultObject = connection.createStatement(query).execute()
  .map { row, _ -> ResultObject(
    row.get(0, String::class.java)!!,
    row.get(1, String::class.java)!!,
    row.get(2, Double::class.java)!!,
  ) }

And I got an error java.lang.IllegalArgumentException: Cannot decode null for type 5.

Conjecture

  • From my analysis of code, r2dbc (this library) gets its type information from db (type 5 - DataTypes.DOUBLE) and guesses "this field should be mapped into proper java type (Double.class)" -- maybe around this line (DefaultCodecs.java)
  • Next, when this field (Double.class in above) is primitive, do decodePrimitive(). -- this line
  • Comment on decodePrimitive() is // If value is null field, then primitive codec should throw an exception instead of return null.. But I'd like to ask about this -- when type of corresponding field is wrapper type, could be nullable though it is primitive(or numeric) type? In my understanding, not only Kotlin, but also Java, wrapper type is nullable and could have null value.
  • Note that, my workaround is mapping last field of ResultObject using row.get(2, Any::class.java) as Double?.
  • I'd happy if I could hear your thoughts on this :)
    (if you agree with my opinion -- wrapper type could have null value; I'll consider how it could be resolved ๐Ÿ˜ƒ)

R2DBC doesn't support primitive values as T always has to be an object, therefore use the wrapper type class.

Thanks @mp911de.
If I understand correctly, null value should be allowed during decodePrimitive() -- I'll work on this if it is OK.
(I thought I had read it wrong, and I deleted the previous comment. Sorry for confusion)

if null should be Mono.empty() so that we can use Mono.defaultIfEmpty(1)

After applying #187, I can find null is returned on here, and this causes NPE on FluxHandle.next().
This should be a problem when single value is returned, as well as, not a problem if there is a something like container class contains null (as seen in Observation).

Hi, @btakeya,

Great thanks for @mp911de suggestion. Apologies for the late reply.

Well, this should be a little difference between Java and Kotlin.

In Java, double.class is a primitive class, which is different from Double.class, a boxed type. So that's why I want to throw an exception when calling double someData = row.get(0, double.class) in Java. If I do not, it will be an NPE and thrown by unboxing instead of driver-side.

In Kotlin/JVM, Double::class.java is a primitive class, and Kotlin/JVM blurs the difference between boxed type and primitive type at the usage level, because Kotlin is a multi-platform language (e.g. Kotlin/Native, Kotlin/JS). And if Kotlin/JVM wants to do the same behavior as the Java boxed type, it should be java.lang.Double::class.java instead.

Actually, yes, considering cross-language compatibility, even if the type is a primitive class, it should return null. And then, if the Java code executes int someData = row.get(0, int.class) and the field is null, it will be NPE (thrown by unboxing) instead of IllegalArgumentException (thrown by r2dbc-mysql).

I will merge your PR soon.

Great regards,