Я добавил базу данных MySQL, и теперь тесты извлечения из ОТДЕЛЬНОЙ базы данных H2 завершаются неудачей. Комментирование конфигурации MySQL приводит к прохождению тестов

#mysql #spring-boot #hibernate #jpa #h2

#mysql #весенняя загрузка #спящий режим #jpa #h2

Вопрос:

У меня есть шахматное приложение, написанное с помощью Spring boot. У меня есть две базы данных. Одна H2 база данных для быстрого и краткосрочного извлечения данных (для поддержки сопряжения игр) и MySql база данных для долговременного сохранения данных (хранение шахматных игр). Я использую Hibernate и JPA для своего ORM . Как только я настроил базу данных Mysql и выяснил, как сделать правильный EntityManagerFactory ввод H2AbstractRepo (предполагается, что это тот, который определен в H2Config.java ), я заметил, что могу сохранить, но тесты извлечения из H2 базы данных завершились неудачей.

Чтобы отладить проблему, я создал строку в своем save методе, где после сохранения объекта я немедленно пытаюсь получить его снова (см. save() В H2AbstractRepo ). Это ВСЕГДА успешно.

Но когда save я возвращаюсь к моему H2PlayerRepoTest.findById() методу тестирования и тест пытается извлечь объект из базы данных, чтобы подтвердить, что он был сохранен правильно, результат возвращается null , предполагая, что entity с этим идентификатором не связано, несмотря на то, что я объяснил в параграфе выше. Я просто в замешательстве. ВАЖНО: если я закомментирую содержимое MySqlConfig.java эффективного удаления базы данных MySQL из приложения, проблема исчезнет, предполагая MySql , что виновником может быть база данных. Вот соответствующие конфигурационные файлы и repo классы.

H2Config.java

 @Configuration
@EnableTransactionManagement
@ComponentScan
public class H2Config{

    @Bean("h2DataSource")
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        return builder
                .setType(EmbeddedDatabaseType.H2)
                .generateUniqueName(true)
                .addScript("classpath:sql_scripts/create_dbs.sql")
                .build();
    }

    @Bean("h2TransactionManager")
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager();
    }

    @Bean("h2HibernateProperties")
    public Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        properties.put("hibernate.format_sql", true);
        properties.put("hibernate.show_sql", false);
        properties.put("hibernate.max_fetch_depth", 3);
        properties.put("hibernate.jdbc.batch_size", 10);
        properties.put("hibernate.jdbc.fetch_size", 50);

        return properties;
    }

    @Bean("H2PersistenceUnit")
    public EntityManagerFactory h2EntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setPackagesToScan("com.example.chess.model.entity");
        factoryBean.setDataSource(dataSource());
        factoryBean.setJpaVendorAdapter(jpaVendorAdaptor());
        factoryBean.setJpaProperties(hibernateProperties());
        factoryBean.afterPropertiesSet();

        return factoryBean.getNativeEntityManagerFactory();
    }

    @Bean("h2JpaVendorAdapter")
    public JpaVendorAdapter jpaVendorAdaptor() {
        return new HibernateJpaVendorAdapter();
    }
}
 

MySqlConfig.java

 @Configuration
@EnableTransactionManagement
@ComponentScan
public class MySqlConfig {

    @Primary
    @Bean
    public DataSource dataSource() {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        try {
            dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
        } catch (PropertyVetoException e) {
            Logger logger = Logger.getLogger(getClass().toString());
            logger.warning("PropertyVetoException throw assigning the database driver");
            e.printStackTrace();
        }
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/chesslive");
        dataSource.setUser("springstudent");
        dataSource.setPassword("springstudent");
        dataSource.setInitialPoolSize(5);
        dataSource.setMinPoolSize(5);
        dataSource.setMaxPoolSize(20);
        dataSource.setMaxIdleTime(3000);
        return dataSource;
    }

    @Primary
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new JpaTransactionManager(entityManagerFactory());
    }

    @Primary
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Primary
    @Bean
    public Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
        properties.put("hibernate.format_sql", true);
        properties.put("hibernate.show_sql", false);
        properties.put("hibernate.max_fetch_depth", 3);
        properties.put("hibernate.jdbc.batch_size", 10);
        properties.put("hibernate.jdbc.fetch_size", 50);

        return properties;
    }

    @Primary
    @Bean
    public EntityManagerFactory entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setPackagesToScan("com.example.chess.model.entity");
        factoryBean.setDataSource(dataSource());
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
        factoryBean.setJpaProperties(hibernateProperties());
        factoryBean.afterPropertiesSet();

        return factoryBean.getNativeEntityManagerFactory();
    }
}
 

H2AbstractRepo.java

 @Transactional
public abstract class H2AbstractRepoImpl<T extends AbstractEntity> implements AbstractRepo<T> {

//    TODO remove this and logging statements
    Logger logger = Logger.getLogger(getClass().toString());
    private final Class<T> clazz;

    protected EntityManager entityManager;

    public H2AbstractRepoImpl(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public void save(T entity) {
        logger.info("Persisting entity: "   entity.toString());
        entityManager.persist(entity);
        logger.info("Attempting to retrieve the entity for verification");
        var result = this.findById(entity.getId());
        if (result == null) {
            throw new RuntimeException("Entity failed to be retrieved after saving");
        } else {
            logger.info("Entity successfully retrieved: "   entity.toString());
        }
    }

    @Override
    public void delete(T entity) {
        if (entityManager.contains(entity)) {
            entityManager.remove(entity);
        } else {
            var newEntity = entityManager.merge(entity);
            entityManager.remove(newEntity);
        }
    }

    @Override
    public T merge(T entity) {
        return entityManager.merge(entity);
    }

    @Override
    public void deleteById(Object id) {
        var entity = entityManager.find(clazz, id);
        entityManager.remove(entity);
    }

    @Override
    public Optional<T> findById(Object id) {
        logger.info("Attempting to find entity with Id: "   id.toString());
        var entity = entityManager.find(clazz, id);
        return Optional.ofNullable(entity);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<T> findAll() {
        return (List<T>) entityManager.createQuery("SELECT e FROM "   clazz.getSimpleName()   " e").getResultList();
    }
// If i dont specify the unitName EntityManagerFactory defined in MySqlConfig takes is injected.
    @PersistenceContext(unitName = "H2PersistenceUnit")
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }
}
 

Тест, о котором идет речь

 @Test
void findById() {
    Player player = new Player();
    UUID id = UUID.randomUUID();
    player.setId(id);
    player.setUsername("dylan");
    playerRepo.save(player);

    Optional<Player> dylanOpt = playerRepo.findById(id);
    if (dylanOpt.isEmpty()) {
        fail();
    } else Assertions.assertTrue(true);

}
 

И последнее, но не менее важное: мои (частичные) журналы. Пожалуйста, обратите внимание на инструкции print из H2AbstractRepo.save метода внизу. Они иллюстрируют, что объект может быть извлечен сразу после сохранения.

 2020-12-11 15:49:56.317  INFO 14504 --- [           main] c.e.c.db.repo.impl.h2.H2PlayerRepoTest   : Starting H2PlayerRepoTest using Java 11.0.1 on DESKTOP-G477CDL with PID 14504 (started by super in C:UserssuperDocumentsSpringProjectsChessLite)
2020-12-11 15:49:56.322  INFO 14504 --- [           main] c.e.c.db.repo.impl.h2.H2PlayerRepoTest   : No active profile set, falling back to default profiles: default
2020-12-11 15:49:57.602  INFO 14504 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFERRED mode.
2020-12-11 15:49:57.651  INFO 14504 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 33 ms. Found 0 JPA repository interfaces.
2020-12-11 15:49:58.184  INFO 14504 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler@6920b0bc' of type [org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-12-11 15:49:58.219  INFO 14504 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'methodSecurityMetadataSource' of type [org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-12-11 15:49:58.972  INFO 14504 --- [           main] o.s.j.d.e.EmbeddedDatabaseFactory        : Starting embedded database: url='jdbc:h2:mem:f236f47e-b2c4-4baa-9f91-02be17045b54;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
2020-12-11 15:49:59.544  INFO 14504 --- [g-Init-Reporter] com.mchange.v2.log.MLog                  : MLog clients using slf4j logging.
2020-12-11 15:49:59.689  INFO 14504 --- [           main] com.mchange.v2.c3p0.C3P0Registry         : Initializing c3p0-0.9.5.5 [built 11-December-2019 22:18:33 -0800; debug? true; trace: 10]
2020-12-11 15:49:59.995  INFO 14504 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-12-11 15:50:00.092  INFO 14504 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.4.23.Final
2020-12-11 15:50:00.327  INFO 14504 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2020-12-11 15:50:00.604  INFO 14504 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2020-12-11 15:50:01.804  INFO 14504 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-12-11 15:50:01.819  INFO 14504 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-12-11 15:50:01.866  INFO 14504 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-12-11 15:50:01.909  INFO 14504 --- [           main] c.m.v.c.i.AbstractPoolBackedDataSource   : Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, contextClassLoaderSource -> caller, dataSourceName -> 1hge1a3aen6qdyf17t752r|3b7c80c6, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.cj.jdbc.Driver, extensions -> {}, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, forceSynchronousCheckins -> false, forceUseNamedDriverClass -> false, identityToken -> 1hge1a3aen6qdyf17t752r|3b7c80c6, idleConnectionTestPeriod -> 0, initialPoolSize -> 5, jdbcUrl -> jdbc:mysql://localhost:3306/chesslive, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 3000, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 20, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 5, numHelperThreads -> 3, preferredTestQuery -> null, privilegeSpawnedThreads -> false, properties -> {password=******, user=******}, propertyCycle -> 0, statementCacheNumDeferredCloseThreads -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, userOverrides -> {}, usesTraditionalReflectiveProxies -> false ]
2020-12-11 15:50:02.498  INFO 14504 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
2020-12-11 15:50:02.664  INFO 14504 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-12-11 15:50:02.664  INFO 14504 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-12-11 15:50:02.811  INFO 14504 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'clientInboundChannelExecutor'
2020-12-11 15:50:02.835  INFO 14504 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'clientOutboundChannelExecutor'
2020-12-11 15:50:03.517  INFO 14504 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure Ant [pattern='/img.chesspieces.wikipedia/**'] with []
2020-12-11 15:50:03.517  INFO 14504 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure Ant [pattern='/libraries/**'] with []
2020-12-11 15:50:03.517  INFO 14504 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure Ant [pattern='/css/**'] with []
2020-12-11 15:50:03.525  INFO 14504 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure Ant [pattern='/js/**'] with []
2020-12-11 15:50:03.525  INFO 14504 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure Ant [pattern='/webjars/**'] with []
2020-12-11 15:50:03.802  INFO 14504 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@182fd26b, org.springframework.security.web.context.SecurityContextPersistenceFilter@6c1a63f7, org.springframework.security.web.header.HeaderWriterFilter@534d0cfa, org.springframework.security.web.csrf.CsrfFilter@6d946eee, org.springframework.security.web.authentication.logout.LogoutFilter@1317ac2c, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@53917c92, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@8ee1404, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@5dc120ab, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@b5311cb, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@8636cf4, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@49c4118b, org.springframework.security.web.session.SessionManagementFilter@1d33e72e, org.springframework.security.web.access.ExceptionTranslationFilter@59ec5a0b, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@f287a4e]
2020-12-11 15:50:03.920  INFO 14504 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'messageBrokerTaskScheduler'
2020-12-11 15:50:04.058  INFO 14504 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'brokerChannelExecutor'
2020-12-11 15:50:04.710  INFO 14504 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : Starting...
2020-12-11 15:50:04.710  INFO 14504 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@15c96f24]]
2020-12-11 15:50:04.718  INFO 14504 --- [           main] o.s.m.s.b.SimpleBrokerMessageHandler     : Started.
2020-12-11 15:50:04.719  INFO 14504 --- [           main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
2020-12-11 15:50:04.720  INFO 14504 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2020-12-11 15:50:04.746  INFO 14504 --- [           main] c.e.c.db.repo.impl.h2.H2PlayerRepoTest   : Started H2PlayerRepoTest in 8.958 seconds (JVM running for 10.755)
2020-12-11 15:50:05.036  INFO 14504 --- [           main] c.e.chess.db.repo.impl.h2.H2PlayerRepo   : Persisting entity: Player{id=679f9535-2d1e-4be6-9335-8d75ffda15c3, username='dylan', gameList=null, joinDate=null} com.example.chess.model.entity.Player@6eeeb9da
2020-12-11 15:50:05.069  INFO 14504 --- [           main] c.e.chess.db.repo.impl.h2.H2PlayerRepo   : Attempting to retrieve the entity for verification
2020-12-11 15:50:05.069  INFO 14504 --- [           main] c.e.chess.db.repo.impl.h2.H2PlayerRepo   : Attempting to find entity with Id: 679f9535-2d1e-4be6-9335-8d75ffda15c3
2020-12-11 15:50:05.078  INFO 14504 --- [           main] c.e.chess.db.repo.impl.h2.H2PlayerRepo   : Entity successfully retrieved: Player{id=679f9535-2d1e-4be6-9335-8d75ffda15c3, username='dylan', gameList=null, joinDate=null} com.example.chess.model.entity.Player@6eeeb9da
2020-12-11 15:50:05.090  INFO 14504 --- [           main] c.e.chess.db.repo.impl.h2.H2PlayerRepo   : Attempting to find entity with Id: 679f9535-2d1e-4be6-9335-8d75ffda15c3
 

Комментарии:

1. Здесь много конфигурации. Возможно, начните с добавления @ActiveProfiles("test") в ваши тестовые классы и аннотирования сервисов, с которыми вы хотите избежать тестов @Profile("!test")

2. На самом деле это не проблема тестирования как таковая. Тесты просто выявляют ошибку. Обе базы данных входят в производственный продукт, поэтому, если они мешают друг другу, это необходимо исправить.

3. Хорошо, тогда я неправильно понял проблему, извините.

Ответ №1:

Поиграв с MySqlConfig , я понял ответ. Подсказка заключается в том, что комментирование MySqlConfig.java делает все тесты проходными. Виновный компонент — это EntityManagerFactory компонент. В моем проекте есть два компонента этого типа, но один из MySqlConfig них помечен @Primary как компонент по умолчанию, когда имеется более одного компонента EntityManagerFactory ,

 @Primary
@Bean
public EntityManagerFactory entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
    factoryBean.setPackagesToScan("com.example.chess.model.entity");
    factoryBean.setDataSource(dataSource());
    factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
    factoryBean.setJpaProperties(hibernateProperties());
    factoryBean.afterPropertiesSet();

    return factoryBean.getNativeEntityManagerFactory();
}
 

и еще один, определенный H2Config.java в.

 @Bean("H2PersistenceUnit")
public EntityManagerFactory h2EntityManagerFactory() {
    LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
    factoryBean.setPackagesToScan("com.example.chess.model.entity");
    factoryBean.setDataSource(dataSource());
    factoryBean.setJpaVendorAdapter(jpaVendorAdaptor());
    factoryBean.setJpaProperties(hibernateProperties());
    factoryBean.afterPropertiesSet();

    return factoryBean.getNativeEntityManagerFactory();
}
 

Удаление @Primary раскрывает конфликт, который был замаскирован. Где проявляется конфликт? В PlatformTransactionManager компоненте, определенном в H2Config.java .

 @Bean("h2TransactionManager")
public PlatformTransactionManager transactionManager() {
    return new JpaTransactionManager();
}
 

Этот компонент зависит от EntityManagerFactory . Проблема для меня заключалась в том, что этот компонент не нужно вводить явно, поэтому я совершенно не знал, что что-то было введено. Только после удаления @Primary MySqlConfig.transactionManager() и просмотра NoUniqueBeanDefinition исключения я понял, что происходит.

Поскольку я явно не вводил a EntityManagerFactory в PlatformTransactionManager , он извлек саму зависимость, по умолчанию используя «основной» компонент, определенный в MySqlConfig.java . Это вызвало неожиданное поведение, и, хотя я не совсем понимаю, почему сохранения работали, а извлечения — нет (что-то, с каким источником данных используется при инициировании сохранения или чтения), предсказуемо, что запутывание этих зависимостей приведет к проблемам с сохранением. В любом случае решение довольно простое: удалите двусмысленность, вручную вставив EntityManagerFactory defined в H2Config в PlatformTransactionManager компонент в том же классе.

 @Bean("h2TransactionManager")
public PlatformTransactionManager transactionManager() {
    return new JpaTransactionManager(h2EntityManagerFactory());
}
 

Теперь все тесты проходят. Спасибо, что пришли на мой доклад на TED!