#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!