pgjdbc / r2dbc-postgresql

Postgresql R2DBC Driver

Home Page:https://r2dbc.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Kotlin / Spring Data R2DBC PostGIS complains about Nested Entity

pete-setchell-kubra opened this issue · comments

Bug Report

Creating a basic entity with a PostGIS geometry Point type does not function as expected with Spring Boot. A Point can be read from a fixture in the database using the native PostgisGeometryCodec, but cannot be inserted in a new entity.

Versions

  • Driver: 1.0.3.RELEASE (also functioned the same rolling back through 0.8.5.RELEASE)
  • Database: postgres (PostgreSQL) 14.10 (Homebrew)
  • Java: OpenJDK 64-Bit Server VM (build 21.0.1+12-29, mixed mode, sharing)
  • OS: MacOS

Current Behavior

Attempting to set up a minimal mapping of a point with JTS does not work as expected with kotlin and Spring Data R2DBC. Running through a debugger, the PostgisGeometryCodec seems to be dynamically loaded during connection handshake, but this is not done in time to stop the exception being thrown on first write.

Stack trace ``` org.springframework.dao.InvalidDataAccessApiUsageException: Nested entities are not supported at org.springframework.data.r2dbc.convert.MappingR2dbcConverter.writePropertyInternal(MappingR2dbcConverter.java:251) at org.springframework.data.r2dbc.convert.MappingR2dbcConverter.writeProperties(MappingR2dbcConverter.java:218) at org.springframework.data.r2dbc.convert.MappingR2dbcConverter.writeInternal(MappingR2dbcConverter.java:189) at org.springframework.data.r2dbc.convert.MappingR2dbcConverter.write(MappingR2dbcConverter.java:181) at org.springframework.data.r2dbc.convert.MappingR2dbcConverter.write(MappingR2dbcConverter.java:61) at org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy.getOutboundRow(DefaultReactiveDataAccessStrategy.java:177) at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doInsert$6(R2dbcEntityTemplate.java:470) at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:153) at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:63) at reactor.core.publisher.MonoUsingWhen.subscribe(MonoUsingWhen.java:87) at reactor.core.publisher.Mono.subscribe(Mono.java:4512) at kotlinx.coroutines.reactor.MonoKt.awaitSingleOrNull(Mono.kt:47) at org.springframework.aop.framework.CoroutinesUtils.awaitSingleOrNull(CoroutinesUtils.java:42) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:268) at jdk.proxy2/jdk.proxy2.$Proxy69.save(Unknown Source) at com.example.r2dbchibernatepoc.repository.AssetRepositoryTest$testInsert$1.invokeSuspend(AssetRepositoryTest.kt:29) at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:46) at com.example.r2dbchibernatepoc.repository.AssetRepositoryTest$testInsert$1.invokeSuspend(AssetRepositoryTest.kt:29) ```

Table schema

Input Code
CREATE SEQUENCE asset_id_seq;
CREATE TABLE IF NOT EXISTS asset (
    id BIGINT PRIMARY KEY DEFAULT NEXTVAL('asset_id_seq'),
    geom GEOMETRY(Point, 4326)
);

Steps to reproduce

Input Code
import org.locationtech.jts.geom.Geometry

@Table("asset")
data class Asset constructor(
    @Id val id: Long?,
    @Column val geom: Geometry
)

interface AssetRepository : CoroutineCrudRepository<Asset, Long> {
    override suspend fun findById(id: Long): Asset?
}
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.PrecisionModel

@SpringBootTest
class AssetRepositoryTest @Autowired constructor(val userRepository: UserRepository, val assetRepository: AssetRepository) {

    @Test
    fun testInsert() {
        val longitude = 37.7434579544699
        val latitude = -122.44437070120628
        val factory4326 = GeometryFactory(PrecisionModel(PrecisionModel.FLOATING), 4326)
        val p: Geometry = factory4326.createPoint(Coordinate(longitude, latitude)) as Geometry

        runBlocking {
            val a = assetRepository.save(Asset(id=null, geom = p))
            assertNotNull(a.id)
            val aid = a.id!!
            val b = assetRepository.findById(aid)
            assertEquals(a, b)
            assertNotNull(b?.geom)
        }
    }

    @Test
    fun testRead() {
        runBlocking {
            val aid = 1L
            val a = assetRepository.findById(aid)
            assertNotNull(a?.geom)
        }
    }
}

A minimal reproducible version of this bug is available here: https://github.com/pete-setchell-kubra/r2dbcrepro

Expected behavior/code

  • testRead runs and loads an entity from a fixture, mapping the geometry correctly! Yay!
  • testInsert fails with the exception about Nested entity. However Geometry should be a dynamically loaded native driver type, not a nested entity. Boo!

Possible Solution

This may be in issue in spring-data-relational, I'll double file there and update this ticket with an issue number.

Also filed against spring-data-relational

spring-projects/spring-data-relational#1711

InvalidDataAccessApiUsageException comes from Spring.

Agreed! I've now provided an interim fix and PR in the linked spring-data-r2dbc issue.