带有测试容器的未终止美元报价初始化脚本

问题描述

我有一个要在 testcontainers (postgres) 启动时执行的 init 脚本。
我的设置是:

  • 弹簧靴
  • spring 数据 jdbc
  • docker 上的测试容器
  • postgresql

截至今天,所有框架都是最新的。

这是脚本的摘录:

create or replace function check_registration_limits() returns trigger
    language plpgsql
as $$
BEGIN
    IF(SELECT count(*) FROM applicationusersubjectregistration ausr WHERE NEW.campus = ausr.campus and NEW.subject = ausr.subject)
        >= (SELECT s.maxparticipants FROM subject s WHERE s.subjectid = NEW.subject) THEN
        RAISE EXCEPTION 'Registration limits reached! No more voting allowed!';
    END IF;
    RETURN NEW;
END
$$;

当我启动我的测试时,会发生此堆栈跟踪:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSourceInitializer' defined in de.thd.awp.TestDataSourceConfiguration: Invocation of init method Failed; nested exception is org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute sql script statement #30 of class path resource [schema.sql]: create or replace function check_registration_limits() returns trigger language plpgsql as $$ BEGIN IF(SELECT count(*) FROM applicationusersubjectregistration ausr WHERE NEW.campus = ausr.campus and NEW.subject = ausr.subject) >= (SELECT s.maxparticipants FROM subject s WHERE s.subjectid = NEW.subject) THEN RAISE EXCEPTION 'Registration limits reached! No more voting allowed!'; nested exception is org.postgresql.util.PsqlException: Unterminated dollar quote started at position 91 in sql create or replace function check_registration_limits() returns trigger language plpgsql as $$ BEGIN IF(SELECT count(*) FROM applicationusersubjectregistration ausr WHERE NEW.campus = ausr.campus and NEW.subject = ausr.subject) >= (SELECT s.maxparticipants FROM subject s WHERE s.subjectid = NEW.subject) THEN RAISE EXCEPTION 'Registration limits reached! No more voting allowed!'. Expected terminating $$
    at org.springframework.beans.factory.support.AbstractAutowireCapablebeanfactory.initializeBean(AbstractAutowireCapablebeanfactory.java:1786) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapablebeanfactory.doCreateBean(AbstractAutowireCapablebeanfactory.java:602) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapablebeanfactory.createBean(AbstractAutowireCapablebeanfactory.java:524) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.Abstractbeanfactory.lambda$doGetBean$0(Abstractbeanfactory.java:335) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.Abstractbeanfactory.doGetBean(Abstractbeanfactory.java:333) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.Abstractbeanfactory.getBean(Abstractbeanfactory.java:208) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.DefaultListablebeanfactory.preInstantiateSingletons(DefaultListablebeanfactory.java:944) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.context.support.AbstractApplicationContext.finishbeanfactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.6.jar:5.3.6]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:782) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:774) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:339) ~[spring-boot-2.4.5.jar:2.4.5]
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:123) ~[spring-boot-test-2.4.5.jar:2.4.5]
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99) ~[spring-test-5.3.6.jar:5.3.6]
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124) ~[spring-test-5.3.6.jar:5.3.6]
    at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124) ~[spring-test-5.3.6.jar:5.3.6]
    at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190) ~[spring-test-5.3.6.jar:5.3.6]
    at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132) ~[spring-test-5.3.6.jar:5.3.6]
    at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:244) ~[spring-test-5.3.6.jar:5.3.6]
    at org.springframework.test.context.junit.jupiter.SpringExtension.postProcesstestInstance(SpringExtension.java:138) ~[spring-test-5.3.6.jar:5.3.6]
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$6(ClassBasedTestDescriptor.java:350) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:355) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$7(ClassBasedTestDescriptor.java:350) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) ~[na:na]
    at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177) ~[na:na]
    at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625) ~[na:na]
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484) ~[na:na]
    at java.base/java.util.stream.AbstractPipeline.wrapAndcopyInto(AbstractPipeline.java:474) ~[na:na]
    at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312) ~[na:na]
    at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:735) ~[na:na]
    at java.base/java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:734) ~[na:na]
    at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:658) ~[na:na]
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:349) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$instantiateAndPostProcesstestInstance$4(ClassBasedTestDescriptor.java:270) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcesstestInstance(ClassBasedTestDescriptor.java:269) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$2(ClassBasedTestDescriptor.java:259) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at java.base/java.util.Optional.orElseGet(Optional.java:362) ~[na:na]
    at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$3(ClassBasedTestDescriptor.java:258) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:101) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:100) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:65) ~[junit-jupiter-engine-5.7.1.jar:5.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$1(NodeTestTask.java:111) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:111) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:79) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na]
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na]
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51) ~[junit-platform-engine-1.7.1.jar:1.7.1]
    at org.junit.platform.launcher.core.EngineExecutionorchestrator.execute(EngineExecutionorchestrator.java:108) ~[junit-platform-launcher-1.7.1.jar:1.7.1]
    at org.junit.platform.launcher.core.EngineExecutionorchestrator.execute(EngineExecutionorchestrator.java:88) ~[junit-platform-launcher-1.7.1.jar:1.7.1]
    at org.junit.platform.launcher.core.EngineExecutionorchestrator.lambda$execute$0(EngineExecutionorchestrator.java:54) ~[junit-platform-launcher-1.7.1.jar:1.7.1]
    at org.junit.platform.launcher.core.EngineExecutionorchestrator.withInterceptedStreams(EngineExecutionorchestrator.java:67) ~[junit-platform-launcher-1.7.1.jar:1.7.1]
    at org.junit.platform.launcher.core.EngineExecutionorchestrator.execute(EngineExecutionorchestrator.java:52) ~[junit-platform-launcher-1.7.1.jar:1.7.1]
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96) ~[junit-platform-launcher-1.7.1.jar:1.7.1]
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75) ~[junit-platform-launcher-1.7.1.jar:1.7.1]
    at com.intellij.junit5.junit5IdeaTestRunner.startRunnerWithArgs(junit5IdeaTestRunner.java:71) ~[junit5-rt.jar:na]
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) ~[junit-rt.jar:na]
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:221) ~[junit-rt.jar:na]
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54) ~[junit-rt.jar:na]
Caused by: org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute sql script statement #30 of class path resource [schema.sql]: create or replace function check_registration_limits() returns trigger language plpgsql as $$ BEGIN IF(SELECT count(*) FROM applicationusersubjectregistration ausr WHERE NEW.campus = ausr.campus and NEW.subject = ausr.subject) >= (SELECT s.maxparticipants FROM subject s WHERE s.subjectid = NEW.subject) THEN RAISE EXCEPTION 'Registration limits reached! No more voting allowed!'; nested exception is org.postgresql.util.PsqlException: Unterminated dollar quote started at position 91 in sql create or replace function check_registration_limits() returns trigger language plpgsql as $$ BEGIN IF(SELECT count(*) FROM applicationusersubjectregistration ausr WHERE NEW.campus = ausr.campus and NEW.subject = ausr.subject) >= (SELECT s.maxparticipants FROM subject s WHERE s.subjectid = NEW.subject) THEN RAISE EXCEPTION 'Registration limits reached! No more voting allowed!'. Expected terminating $$
    at org.springframework.jdbc.datasource.init.ScriptUtils.executesqlScript(ScriptUtils.java:622) ~[spring-jdbc-5.3.6.jar:5.3.6]
    at org.springframework.jdbc.datasource.init.ResourceDatabasePopulator.populate(ResourceDatabasePopulator.java:254) ~[spring-jdbc-5.3.6.jar:5.3.6]
    at org.springframework.jdbc.datasource.init.DatabasePopulatorUtils.execute(DatabasePopulatorUtils.java:49) ~[spring-jdbc-5.3.6.jar:5.3.6]
    at org.springframework.jdbc.datasource.init.DataSourceInitializer.execute(DataSourceInitializer.java:111) ~[spring-jdbc-5.3.6.jar:5.3.6]
    at org.springframework.jdbc.datasource.init.DataSourceInitializer.afterPropertiesSet(DataSourceInitializer.java:96) ~[spring-jdbc-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapablebeanfactory.invokeInitMethods(AbstractAutowireCapablebeanfactory.java:1845) ~[spring-beans-5.3.6.jar:5.3.6]
    at org.springframework.beans.factory.support.AbstractAutowireCapablebeanfactory.initializeBean(AbstractAutowireCapablebeanfactory.java:1782) ~[spring-beans-5.3.6.jar:5.3.6]
    ... 83 common frames omitted
Caused by: org.postgresql.util.PsqlException: Unterminated dollar quote started at position 91 in sql create or replace function check_registration_limits() returns trigger language plpgsql as $$ BEGIN IF(SELECT count(*) FROM applicationusersubjectregistration ausr WHERE NEW.campus = ausr.campus and NEW.subject = ausr.subject) >= (SELECT s.maxparticipants FROM subject s WHERE s.subjectid = NEW.subject) THEN RAISE EXCEPTION 'Registration limits reached! No more voting allowed!'. Expected terminating $$
    at org.postgresql.core.Parser.checkParsePosition(Parser.java:1328) ~[postgresql-42.2.19.jar:42.2.19]
    at org.postgresql.core.Parser.parsesql(Parser.java:1227) ~[postgresql-42.2.19.jar:42.2.19]
    at org.postgresql.core.Parser.replaceProcessing(Parser.java:1179) ~[postgresql-42.2.19.jar:42.2.19]
    at org.postgresql.core.CachedQueryCreateAction.create(CachedQueryCreateAction.java:43) ~[postgresql-42.2.19.jar:42.2.19]
    at org.postgresql.core.QueryExecutorBase.createqueryByKey(QueryExecutorBase.java:337) ~[postgresql-42.2.19.jar:42.2.19]
    at org.postgresql.jdbc.PgStatement.executeCachedsql(PgStatement.java:300) ~[postgresql-42.2.19.jar:42.2.19]
    at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:284) ~[postgresql-42.2.19.jar:42.2.19]
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:279) ~[postgresql-42.2.19.jar:42.2.19]
    at org.springframework.jdbc.datasource.init.ScriptUtils.executesqlScript(ScriptUtils.java:601) ~[spring-jdbc-5.3.6.jar:5.3.6]
    ... 89 common frames omitted

这可能是一些脚本构建巫毒转义错误吗?
还是我必须逃避脚本的某些部分?
提前致谢

编辑
我尝试使用 Spring Boot 测试资源 schema.sqlDataSourceInitializer 初始化 docker/testcontainers/postgres 数据库

@Bean
    @Primary
    public DataSourceInitializer testDataSourceInitializer() {
        ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
        resourceDatabasePopulator.addScript(new ClassPathResource("/schema.sql"));

        DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
        dataSourceInitializer.setDataSource(testDataSource());
        dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator);
        return dataSourceInitializer;
    }

解决方法

当您使用 postgresql 时,您可以尝试将初始化脚本放在 /docker-entrypoint-initdb.d 下,如here

,

我省略了上面的 DataSourceInitializer bean,因为 testcontainers 也提供了一个 init 脚本。
所以我的解决方案是使用 testcontainer 的 postgres 容器正确配置这个 init 脚本:

@Bean
        @Primary
        DataSource testDataSource() {

            if (POSTGRESQL_CONTAINER == null) {
                PostgreSQLContainer<?> container = new PostgreSQLContainer<>();
                container.withInitScript("schema.sql");
                container.start();
                POSTGRESQL_CONTAINER = container;
            }
            PGSimpleDataSource dataSource = new PGSimpleDataSource();
            dataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl());
            dataSource.setUser(POSTGRESQL_CONTAINER.getUsername());
            dataSource.setPassword(POSTGRESQL_CONTAINER.getPassword());

            return dataSource;


        }

container.withInitScript("schema.sql"); 完成这项工作。请注意 schema.sql 必须放在 .\src\test\resources

,

添加第二个触发器时,事情变得很奇怪:

create or replace function check_registration_limits() returns trigger
    language plpgsql
as $$
BEGIN
    IF(SELECT count(*) FROM applicationusersubjectregistration ausr WHERE NEW.campus = ausr.campus and NEW.subject = ausr.subject)
        >= (SELECT s.maxparticipants FROM subject s WHERE s.subjectid = NEW.subject) THEN
        RAISE EXCEPTION 'Registration limits reached! No more voting allowed!';
    END IF;
    RETURN NEW;
END
$$;

这会导致另一个 Unterminated dollar quote,即使我对第一个触发器使用相同的表示法,一切正常。

所以在这里找到了这个答案:https://stackoverflow.com/a/55596701/7320372

使用 ' 作为 $$ 替换后,我的问题解决了:

create or replace function check_registration_limits() returns trigger
as
'
BEGIN
    IF (SELECT count(*)
        FROM applicationusersubjectregistration ausr
        WHERE NEW.campus = ausr.campus
          and NEW.subject = ausr.subject)
        >= (SELECT s.maxparticipants FROM subject s WHERE s.subjectid = NEW.subject) THEN
        RAISE EXCEPTION ''Registration limits reached! No more voting allowed!'';
    END IF;
    RETURN NEW;
END
' language plpgsql;