micronaut-projects / micronaut-data

Ahead of Time Data Repositories
Apache License 2.0
462 stars 195 forks source link

CoroutineCrudRepository methods return an implicit null and create NPE #2927

Open sengokudaikon opened 4 months ago

sengokudaikon commented 4 months ago

Expected Behavior

An empty Flow or list must be returned in a situation where a request to the database results in empty rows

Actual Behaviour

NullPointerException can be caused if a method that has a return value of Flow returns null, since (I assume) under the hood it's a java method that processes the data from the source, i.e Hibernate Reactive in my case. The null value is implicit.

Steps To Reproduce

any CoroutineCrudRepository method that's supposed to return a Flow, called when the table in question is empty

Environment Information

macosx, jdk 21

Example Application

Should be:

override fun findAll(): Flow { catch { return super.findAll() }.getOrElse { return flowOf() } }

is: Cannot invoke "kotlinx.coroutines.flow.Flow.collect(kotlinx.coroutines.flow.FlowCollector, kotlin.coroutines.Continuation)" because "$this$toCollection" is null

Version

4.4.1

sdelamo commented 4 months ago

can you provide a sample application which reproduces the issue?

sengokudaikon commented 4 months ago

This is the reproducer https://github.com/sengokudaikon/reproducible-crotoutine-crud-repo-implicit-nulls

@sdelamo

sengokudaikon commented 4 months ago

To add to this, apparently saveAll() method doesn't work on lists or is inconsistent in its operation. Same reproducer. Direct queries like "INSERT INTO table VALUES .... ON CONFLICT DO UPDATE .... RETURNING * are also not being executed, even with "@ Transactional".

Example:

 @Transactional
    open suspend fun insertMany(entities: Iterable<Country>): Flow<Country> {
        return traceAsyncCatch(IO) {
            val query = entityManager.createNativeQuery(
                "INSERT INTO countries (id, name, updated_at, image_path, continent_id) VALUES (:id, :name, :updatedAt, :imagePath, :continent_id) ON CONFLICT (id) DO UPDATE SET name = :name,  image_path =:imagePath, updated_at=:updatedAt, continent_id=:continent_id RETURNING *",
                Country::class.java
            ).setParameter("id", entities.map { it.id })
                .setParameter("name", entities.map { it.name })
                .setParameter("continent_id", entities.map { it.continent.id })
                .setParameter("imagePath", entities.map { it.imagePath })
                .setParameter("updatedAt", entities.map { it.updatedAt })

            query.resultStream.collectAsFlow()
        }.fold(
            { throw it },
            {
                it
            }
        )
    }

won't work, isn't being executed at all. no errors in the callstack or trace.

for comparison, using it.parMap { repository.update(it1) } or it.parMap { repository.save(it1) } works fine.