ebean icon indicating copy to clipboard operation
ebean copied to clipboard

@ElementCollection of @Embeddable that contains @DbArray - NoSuchElementException when empty collection passed (via MultiValueWrapper constructor)

Open npokr opened this issue 4 years ago • 4 comments

Expected behavior

test passed

Actual behavior

java.util.NoSuchElementException at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1000) at io.ebeaninternal.server.persist.MultiValueWrapper.(MultiValueWrapper.java:20) at io.ebeaninternal.api.BindParams.setParameter(BindParams.java:173) at io.ebeaninternal.server.core.DefaultSqlUpdate.setParameter(DefaultSqlUpdate.java:313) at io.ebeaninternal.server.core.DefaultSqlUpdate.setParameter(DefaultSqlUpdate.java:284) at io.ebeaninternal.server.deploy.BeanDescriptor.bindElementValue(BeanDescriptor.java:733) at io.ebeaninternal.server.deploy.BeanDescriptorElementEmbedded.bindElementValue(BeanDescriptorElementEmbedded.java:55) at io.ebeaninternal.server.deploy.BeanPropertyAssocMany.bindElementValue(BeanPropertyAssocMany.java:1029) at io.ebeaninternal.server.persist.SaveManyElementCollection.saveCollection(SaveManyElementCollection.java:50) at io.ebeaninternal.server.persist.SaveManyElementCollection.save(SaveManyElementCollection.java:34) at io.ebeaninternal.server.persist.DefaultPersister.saveMany(DefaultPersister.java:882) at io.ebeaninternal.server.persist.DefaultPersister.saveAssocMany(DefaultPersister.java:876) at io.ebeaninternal.server.persist.DefaultPersister.update(DefaultPersister.java:503) at io.ebeaninternal.server.persist.DefaultPersister.update(DefaultPersister.java:381) at io.ebeaninternal.server.core.DefaultServer.update(DefaultServer.java:1650) at io.ebeaninternal.server.core.DefaultServer.update(DefaultServer.java:1645) at io.ebean.DB.update(DB.java:438)

Steps to reproduce

Insert empty list into @DbArray postresql column in @ElementCollection detail. Entities:

@Entity
class TestArrayMaster(
    @ElementCollection
    @CollectionTable(name = "test_array_detail", joinColumns = [JoinColumn(name = "master_id")])
    var details: MutableList<TestArrayDetail> = mutableListOf()
) {
    @Id
    var id = 0
}

@Embeddable
class TestArrayDetail(
    @DbArray
    val vals: List<String>?
)

SQL:

CREATE TABLE test_array_master(
    id serial PRIMARY KEY
);

CREATE TABLE test_array_detail (
    master_id int REFERENCES test_array_master ON DELETE CASCADE,
    vals text[]
);

test:

   @Test
    fun testArrayInsert() {
        val t = TestArrayMaster().apply {
            details = mutableListOf(TestArrayDetail(mutableListOf()))
            DB.insert(this)
        }
    }

npokr avatar Jan 02 '22 00:01 npokr

The stack trace says at io.ebean.DB.update(DB.java:438) BUT ... the test code performs an insert via DB.insert(this) ?

So there is a disconnect here. It probably would be best to provide an explicit and complete failing test case in Java.

rbygrave avatar Jan 14 '22 03:01 rbygrave

OK, in Java entities look like

package ru.nn;

import io.ebean.annotation.DbArray;

import javax.persistence.*;
import java.util.List;

@Entity
public class TestArrayMaster {
    @Embeddable
    public static class TestArrayDetail {
        @DbArray
        List<String> vals;

        public TestArrayDetail(List<String> vals) {
            this.vals = vals;
        }
    }

    @Id
    int id = 0;

    @ElementCollection
    @CollectionTable(name = "test_array_detail", joinColumns = {@JoinColumn(name = "master_id")})
    List<TestArrayDetail> details;

    public TestArrayMaster(List<TestArrayDetail> details) {
        this.details = details;
    }
}

and test class looks like

package ru.nn;

import io.ebean.DB;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Arrays;
import java.util.Collections;

@SpringBootTest
class TestArrayMasterTest {
    @Test
    void testArrayInsert() {
        var t = new TestArrayMaster(Arrays.asList(new TestArrayMaster.TestArrayDetail(Collections.emptyList())));
        DB.insert(t);
    }
}

Full stack trace is

java.util.NoSuchElementException
	at java.base/java.util.Collections$EmptyIterator.next(Collections.java:4210)
	at io.ebeaninternal.server.persist.MultiValueWrapper.<init>(MultiValueWrapper.java:20)
	at io.ebeaninternal.api.BindParams.setParameter(BindParams.java:173)
	at io.ebeaninternal.server.core.DefaultSqlUpdate.setParameter(DefaultSqlUpdate.java:313)
	at io.ebeaninternal.server.core.DefaultSqlUpdate.setParameter(DefaultSqlUpdate.java:284)
	at io.ebeaninternal.server.deploy.BeanDescriptor.bindElementValue(BeanDescriptor.java:733)
	at io.ebeaninternal.server.deploy.BeanDescriptorElementEmbedded.bindElementValue(BeanDescriptorElementEmbedded.java:55)
	at io.ebeaninternal.server.deploy.BeanPropertyAssocMany.bindElementValue(BeanPropertyAssocMany.java:1029)
	at io.ebeaninternal.server.persist.SaveManyElementCollection.saveCollection(SaveManyElementCollection.java:50)
	at io.ebeaninternal.server.persist.SaveManyElementCollection.save(SaveManyElementCollection.java:34)
	at io.ebeaninternal.server.persist.DefaultPersister.saveMany(DefaultPersister.java:882)
	at io.ebeaninternal.server.persist.DefaultPersister.saveAssocMany(DefaultPersister.java:876)
	at io.ebeaninternal.server.persist.DefaultPersister.insert(DefaultPersister.java:473)
	at io.ebeaninternal.server.persist.DefaultPersister.insert(DefaultPersister.java:419)
	at io.ebeaninternal.server.core.DefaultServer.insert(DefaultServer.java:1687)
	at io.ebeaninternal.server.core.DefaultServer.insert(DefaultServer.java:1679)
	at io.ebean.DB.insert(DB.java:381)
	at ru.nn.TestArrayMasterTest.testArrayInsert(TestArrayMasterTest.java:15)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
	at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.stop(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
	at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:133)
	at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:71)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
	at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

npokr avatar Jan 14 '22 07:01 npokr

This issue is specific to @ElementCollection of @Embeddable that contains @DbArray. The issue internally is that element collections don't use our BeanDescriptor based insert, update, delete persister but instead are SqlUpdate based - so they make good use of the internal ScalarType ... which has the nice cross database platform handling of @DbArray.

That is, before fixing this we probably need to try to adjust the internals such that element collection persisting uses BeanDescriptor based insert, update, delete persister.

rbygrave avatar Jan 20 '22 03:01 rbygrave

Hi,

Same issue using DB.sqlUpdate and setArrayParameter (which use MulValueWrapper under the hood). For example :

public class TableName {
[...]
    @DbArray
    @Column(nullable = false)
    private Set<Enum> columnName;
[...]
String updateStr = "UPDATE table_name SET column_name=:column_name WHERE id=:id"
SqlUpdate update = DB.sqlUpdate(updateStr);
update.setArrayParameter("column_name", Collections.emptySet());

Our current workaround :

String updateStr = "UPDATE table_name SET column_name=ARRAY[:column_name]::varchar[] WHERE id=:id"
SqlUpdate update = DB.sqlUpdate(updateStr);
update.setParameter("column_name", Collections.emptySet());

Yours faithfully, LCDP