0

我有一个用 Spring Boot 编写的国际象棋应用程序。我有两个数据库。一个H2用于快速和短期数据检索的数据库(以支持游戏配对)和一个MySql用于长期数据持久性的数据库(国际象棋游戏存储)。我正在使用HibernateandJPA为我的ORM. 一旦我配置了 Mysql 数据库并弄清楚如何将正确的EntityManagerFactory注入到H2AbstractRepo(应该是定义的那个H2Config.java),我注意到我可以保存,但是从H2数据库中检索测试失败了。

为了调试问题,我在我的save方法中创建了一行,在保存实体后,我立即尝试再次检索它(参见 参考资料save()H2AbstractRepo。这总是成功的。

但是当save返回到我的H2PlayerRepoTest.findById()测试方法并且测试尝试从数据库中检索对象以断言它已正确保存时,结果返回null表明没有entity与该 Id 相关联,尽管我在上面的段落中进行了解释。我很困惑。重要提示:如果我注释掉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);

}

最后但同样重要的是,我的(部分)日志。请注意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:\Users\super\Documents\SpringProjects\ChessLite)
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
4

1 回答 1

0

在玩弄了之后,MySqlConfig我想出了答案。提示是注释掉MySqlConfig.java所有测试都会通过。罪魁祸首是EntityManagerFactory豆子。我的项目中有两个这种类型的 bean,但是当有多个 bean 时,其中一个MySqlConfig标记为使其成为默认 bean ,@PrimaryEntityManagerFactory

@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定义的 bean 中H2Config.java

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

这个 bean 依赖于EntityManagerFactory. 对我来说,问题是这个 bean 不需要显式注入,所以我完全不知道注入了任何东西。只有在从异常中移除@PrimaryMySqlConfig.transactionManager()看到NoUniqueBeanDefinition异常之后,我才意识到发生了什么。

由于我没有显式地将 a 注入EntityManagerFactory到 中,PlatformTransactionManager因此它通过默认为MySqlConfig.java. 这导致了意外的行为,虽然我不完全理解为什么保存工作但检索没有(在启动保存与读取时使用哪个数据源的东西),但可以预见的是,让这些依赖关系纠缠在一起会导致持久性问题。在任何情况下,解决方案都非常简单:通过手动将EntityManagerFactory定义的注入到同一类中H2Config的bean 中来消除歧义。PlatformTransactionManager

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

现在所有测试都通过了。感谢您来参加我的 TED 演讲!

于 2020-12-12T14:19:54.487 回答