Spring Boot 3 + 多个数据源:集成测试无法启动

问题描述 投票:0回答:1

首先,这是关于 Spring Boot 3.1.9、Java Temurin 17。

使用https://www.baeldung.com/spring-boot-configure-multiple-datasources,我已经成功设置了两个数据源(实际上,这是同一个数据库,但对于一个实体,我需要一个单独的 Hikari 连接池)。

下面的设置会导致错误消息

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaSharedEM_entityManagerFactory': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument
- 尽管它应该使用
@Configuration
DefaultJpaConfiguration
/
LockingJpaConfiguration
中的定义。 当我禁用
@EnableJpaRepositories
@EntityScan
(正如我必须为生产代码所做的那样),错误消息不会更改。

我知道,

@DataJpa
测试会忽略任何手工制作的
DataSource
定义,但我在
@SpringBootTest
中没有找到任何这样的声明。所以也许我在这里遗漏了一些东西 - 你能帮我吗?

我已尝试申请

  1. Spring Boot 多数据源测试配置
  2. 使用 Spring Boot liquibase 测试不适用于多个数据源 但这并没有改善任何事情。

我的课程看起来如文章中所述:

一个实体,使用默认连接池:

package com.application.database.model.stock;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Vehicle {

    @Id
    @SequenceGenerator(name = "vehicleSeq", sequenceName = "VEHICLE_ID_SEQ", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vehicleSeq")
    private Long id;
    @Column(name = "color")
    private String color;
    
}

默认实体的存储库:

package com.application.database.repository;

import com.application.database.model.stock.Vehicle;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
@Transactional
public interface VehicleRepository extends JpaRepository<Vehicle, Long> {
    Optional<Vehicle> findVehicleByColor(String color);
}

默认连接池的配置:

package com.application.database.config;

import com.application.database.model.dealer.Dealer;
import com.application.database.model.file.WfsFileImport;
import com.application.database.model.location.Location;
import com.application.database.model.reactivation.ManualReactivation;
import com.application.database.model.stock.Vehicle;
import com.application.database.model.thirdpartyexport.DealerContactDataForThirdPartyExportQueryResult;
import java.util.Objects;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackages = {"com.application.database.repository", "com.application.database.model" },
    entityManagerFactoryRef = "defaultEntityManagerFactory",
    transactionManagerRef = "defaultTransactionManager"
)
@RequiredArgsConstructor
public class DefaultJpaConfiguration {

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean defaultEntityManagerFactory(
        @Qualifier("defaultDataSource") DataSource dataSource,
        EntityManagerFactoryBuilder builder) {
        return builder
            .dataSource(dataSource)
            .packages(Vehicle.class)
            .build();
    }

    @Primary
    @Bean
    public PlatformTransactionManager defaultTransactionManager(
        @Qualifier("defaultEntityManagerFactory") LocalContainerEntityManagerFactoryBean defaultEntityManagerFactory) {
        return new JpaTransactionManager(Objects.requireNonNull(defaultEntityManagerFactory.getObject()));
    }

    @Primary
    @Bean
    public DataSource defaultDataSource() {
        return defaultDataSourceProperties()
            .initializeDataSourceBuilder()
            .build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.default")
    public DataSourceProperties defaultDataSourceProperties() {
        return new DataSourceProperties();
    }
}

现在实体,使用其他连接池(“锁定”):

package com.application.database.locking.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.UuidGenerator;
import org.hibernate.annotations.UuidGenerator.Style;

@Table(name = "lock")
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Lock {

    @Id
    @UuidGenerator(style = Style.RANDOM)
    @Column(name = "id", updatable = false, nullable = false, columnDefinition = "UUID")
    private UUID id;
    @Column(name = "vehicle_id")
    private Long vehicleId;
}

其他实体的存储库:

package com.application.database.locking.repository;

import com.application.database.locking.model.Lock;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface LockRepository extends JpaRepository<Lock, Long> {
    Optional<Lock> findByVehicleId(Long vehicleId);
}

其他连接池的配置:

package com.application.database.config;

import com.application.database.locking.model.Lock;
import java.util.Objects;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackages = {"com.application.database.locking.repository", "com.application.database.locking.model" },
    entityManagerFactoryRef = "lockingEntityManagerFactory",
    transactionManagerRef = "lockingTransactionManager"
)
@RequiredArgsConstructor
public class LockingJpaConfiguration {

    @Bean
    public LocalContainerEntityManagerFactoryBean lockingEntityManagerFactory(
        @Qualifier("lockingDataSource") DataSource dataSource,
        EntityManagerFactoryBuilder builder) {
        return builder
            .dataSource(dataSource)
            .packages(Lock.class)
            .build();
    }

    @Bean
    public PlatformTransactionManager lockingTransactionManager(
        @Qualifier("lockingEntityManagerFactory") LocalContainerEntityManagerFactoryBean lockingEntityManagerFactory) {
        return new JpaTransactionManager(Objects.requireNonNull(lockingEntityManagerFactory.getObject()));
    }

    @Bean
    public DataSource lockingDataSource() {
        return lockingDataSourceProperties()
            .initializeDataSourceBuilder()
            .build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.locking")
    public DataSourceProperties lockingDataSourceProperties() {
        return new DataSourceProperties();
    }
}

配置属性:

spring:
  datasource:
    default:
      url: jdbc:postgresql://127.0.0.1/postgres
      username: postgres
      password: postgres
      driver-class-name: org.postgresql.Driver
    locking:
      url: jdbc:postgresql://127.0.0.1/postgres
      username: postgres
      password: postgres
      driver-class-name: org.postgresql.Driver

我必须调整 Application 类(用于启动应用程序),我删除了

@EnableJpaRepositories
@EntityScan
,我能够启动应用程序:

package com.application.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.integration.annotation.IntegrationComponentScan;

@ComponentScan(basePackages = "com.application")
@IntegrationComponentScan(basePackages = "com.application")
//@EnableJpaRepositories(basePackages = "com.application") <- had to be disabled/removed, to get application starting
//@EntityScan(basePackages = "com.application") <- had to be disabled/removed, to get application starting
@SpringBootApplication
public class Application {

    public static void main(final String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

现在,我们有集成和

@DataJpa
测试。
@DataJpa
测试如下所示:

package com.application.database.repository;

import static java.time.ZoneOffset.UTC;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@DataJpaTest
@ActiveProfiles("test")
@TestPropertySource(locations = "classpath:application-test.yml")
class LockRepositoryTest {

    /* Test code omitted */

    @EnableJpaRepositories(basePackages = "com.application")
    @EntityScan(basePackages = "com.application")
    @SpringBootApplication
    static class TestConfiguration {}
}

此测试启动应用程序并成功结束。

但是,当运行集成测试时,情况有所不同,例如这个:

package com.application.database.repository;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@SpringBootTest
@ExtendWith(SpringExtension.class)
@TestPropertySource(locations = "classpath:application-test.yml")
@ActiveProfiles("test")
//@ContextConfiguration( <- leads to NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder' available
//    classes = { DefaultJpaConfiguration.class, LockingJpaConfiguration.class },
//    loader= AnnotationConfigContextLoader.class
//)
class QueryRepositoryTest {

    /* Test code omitted */

    @EnableJpaRepositories(basePackages = "com.application") <- disabling it changes nothing
    @EntityScan(basePackages = "com.application") <- disabling it changes nothing
    @ComponentScan(basePackages = "com.application")
    @SpringBootApplication
    static class TestConfiguration {}
}

如果您需要任何进一步的信息/代码和要尝试的东西,请随时与我联系。

谢谢。 刀

spring-boot hibernate integration-testing
1个回答
0
投票

我能够解决这个问题。 我们在同一模块中进行了更多集成测试(我们不在模块之间共享测试代码),所有测试都具有以下(可比较的)设置:

@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
@SpringBootTest
class IntegrationTestClass {

    // some tests here...

    @ComponentScan(basePackages = "com.application")
    @EnableJpaRepositories(basePackages = "com.application")
    @EntityScan(basePackages = "com.application")
    @SpringBootApplication
    @PropertySource("classpath:application-test.yml")
    static class TestConfiguration {}

}

此设置是在集成测试期间采用的(因为它搜索了所有

@Configuration
类。

我通过将测试类更改为

解决了这个问题
@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
@SpringBootTest
@Import({ IntegrationTestClass.TestConfiguration.class })
class IntegrationTestClass {

    // some tests here...

    @TestComponentScan
    @SpringBootApplication
    @PropertySource("classpath:application-test.yml")
    static class TestConfiguration {}

}

并添加 TestComponentScan 注释:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ComponentScan(basePackages = "com.application",
    excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = {
        ".*TestConfiguration"
    }))
public @interface TestComponentScan {}

重要提示:不能有任何类仍然使用定义上下文的原始方式。

此外,在手动定义ConnectionPools时,SpringBoot似乎没有对新的ConnectionPools使用spring.jpa.generate-ddl设置。因此,我必须使用 Hibernate 6 的配置选项来设置此设置。

JpaConfiguration(两者相同)(只是更改的部分):

@Configuration
@EnableConfigurationProperties({JpaConfigurationProperties.class})
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackages = {"com.application.database.repository", "com.exxeta.dfstp.geofence.core.database.model" },
    entityManagerFactoryRef = "defaultEntityManagerFactory",
    transactionManagerRef = "defaultTransactionManager"
)
@RequiredArgsConstructor
public class DefaultJpaConfiguration {

    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean defaultEntityManagerFactory(
        @Qualifier("defaultDataSource") DataSource dataSource,
        JpaConfigurationProperties jpaConfig,
        EntityManagerFactoryBuilder builder) {

        return builder
            .dataSource(dataSource)
            .packages("com.application.database.repository", "com.application.database.model")
            .properties(jpaConfig.getProperties())
            .build();
    }

使用新类 JpaConfigurationProperties:

@Getter
@ConfigurationProperties(value = "spring.jpa", ignoreInvalidFields = true)
public class JpaConfigurationProperties {
    private final Map<String, String> properties = new HashMap<>();
}

以及 application-test.yml 中的配置设置

spring:
  jpa:
    properties:
      jakarta.persistence.schema-generation.database.action: drop-and-create # set to none for productive code
    generate-ddl: true
    # ignored due to two connection pools, replaced by spring.jpa.properties.jakarta.persistence.schema-generation.database.action
    # hibernate:
    #   ddl-auto: create-drop

也许这可能对其他人有帮助......

感谢您的支持。

© www.soinside.com 2019 - 2024. All rights reserved.