여기서 진행하는 내용의 DBMS는 mysql 8.x 를 이용했고 로컬에 설치했다.
그래서 url 부분에 serverTimezone=UTC 가 파라미터로 추가되었다.(5.x 이하버전은 안해되 됨)
spring.datasource 를 사용하지 않고 커스텀하게 설정할 것이며 여기서는 alert.datasource 로 생성한다.
우선 application.properties 를 다음과 같이 등록한다
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# 테스트서버
alert.datasource.driverClassName=com.mysql.cj.jdbc.Driver
alert.datasource.jdbcUrl=jdbc:mysql://localhost:3306/alert?serverTimezone=UTC
alert.datasource.username=alert
alert.datasource.password=test1234
Alert Datasource 를 등록할 설정클래스를 생성한다
@EnableJpaRepositories 안에는 레파지토리 경로를,
LocalContainerEntityManagerFactoryBean 메서드 내 package 는 Entity 경로를 적어준다.
@Configuration
@PropertySource(value = "classpath:application.properties")
@EnableJpaRepositories(
basePackages = "kr.jpamulti.jpa.alert.repository",
entityManagerFactoryRef = "alertEntityManagerFactory",
transactionManagerRef = "alertTransactionManager"
)
public class AlertDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "alert.datasource")
public DataSource alertDataSource(){
DataSource build = DataSourceBuilder
.create()
.build();
return build;
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean alertEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("alertDataSource") DataSource dataSource) {
return builder.dataSource(dataSource)
.packages("kr.jpamulti.jpa.alert.entity")
.build();
}
@Bean
@Primary
public PlatformTransactionManager alertTransactionManager(@Qualifier("alertEntityManagerFactory") EntityManagerFactory entityManagerFactory){
return new JpaTransactionManager(entityManagerFactory);
}
}
application.properties 를 보면 url 대신 jdbcUrl을 사용한다.
이름을 변경한 이유는 DataSourceBuilder 를 이용해 생성할 때(아래 AlertDataSourceConfig 클래스 참조), jdbcUrl 를 사용하기 때문이다.
만약 url로 등록하면 다음의 에러를 발생한다
Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName. |
JPA는 HikariCP(DB Connection Pool)를 이용하는데 생성하기 전에 HikariConfig 에서 필요한 값이 있는지 체크한다.
public class HikariConfig implements HikariConfigMXBean
{
...
@SuppressWarnings("StatementWithEmptyBody")
public void validate()
{
if (poolName == null) {
poolName = generatePoolName();
}
else if (isRegisterMbeans && poolName.contains(":")) {
throw new IllegalArgumentException("poolName cannot contain ':' when used with JMX");
}
// treat empty property as null
//noinspection NonAtomicOperationOnVolatileField
catalog = getNullIfEmpty(catalog);
connectionInitSql = getNullIfEmpty(connectionInitSql);
connectionTestQuery = getNullIfEmpty(connectionTestQuery);
transactionIsolationName = getNullIfEmpty(transactionIsolationName);
dataSourceClassName = getNullIfEmpty(dataSourceClassName);
dataSourceJndiName = getNullIfEmpty(dataSourceJndiName);
driverClassName = getNullIfEmpty(driverClassName);
jdbcUrl = getNullIfEmpty(jdbcUrl);
// Check Data Source Options
if (dataSource != null) {
if (dataSourceClassName != null) {
LOGGER.warn("{} - using dataSource and ignoring dataSourceClassName.", poolName);
}
}
else if (dataSourceClassName != null) {
if (driverClassName != null) {
LOGGER.error("{} - cannot use driverClassName and dataSourceClassName together.", poolName);
// NOTE: This exception text is referenced by a Spring Boot FailureAnalyzer, it should not be
// changed without first notifying the Spring Boot developers.
throw new IllegalStateException("cannot use driverClassName and dataSourceClassName together.");
}
else if (jdbcUrl != null) {
LOGGER.warn("{} - using dataSourceClassName and ignoring jdbcUrl.", poolName);
}
}
else if (jdbcUrl != null || dataSourceJndiName != null) {
// ok
}
else if (driverClassName != null) {
jdbcUrl is required with driverClassName.", poolName);
throw new IllegalArgumentException("jdbcUrl is required with driverClassName.");
}
else {
LOGGER.error("{} - dataSource or dataSourceClassName or jdbcUrl is required.", poolName);
throw new IllegalArgumentException("dataSource or dataSourceClassName or jdbcUrl is required.");
}
validateNumerics();
if (LOGGER.isDebugEnabled() || unitTest) {
logConfiguration();
}
}
...
}
변수명을 보면 url 대신 jdbcUrl 을 이용한다. 그래서 jdbcUrl 을 쓰게되면 'else if (jdbcUrl != null || dataSourceJndiName != null) {' 부분에서 OK가 떨어져 다음으로 넘어가지만 url을 쓸 경우 'else if (driverClassName != null) {' 이 부분에서 걸리면서 jdbcUrl is required with driverClassName. 문구를 찍는 것이다. 그래서 jdbcUrl 로 입력해야 한다.
그리고 spring.datasource 를 쓸 경우 url 그대로 쓴다.
살펴볼 것은 둘다 org.springframework.boot.jdbc.DataSourceBuilder 를 쓴다는 점인데, 한쪽은 url을, 한쪽은 jdbcUrl을 쓴다는 점이다.
spring.datasource 를 이용해 생성할 경우 DataSourceConfiguration 클래스에 createDataSource를 이용해 빈을 생성하는데 이때 DataSourceProperties.initializeDataSourceBuilder 를 이용한다. 코드를 살펴보면 spring.datasource 나 alert.datasource 나 둘다 DataSourceBuilder를 이용하지만 spring.datasource 의 경우 서버접속정보를 직접 등록한다.(만약 위의 코드에서 alert.datasource를 직접등록한다면 property의 키값을 임의로 줘도 상관없다는 의미)
(gradle: org.springframework.boot:spring-boot-autoconfigure)
DataSourceConfiguration.class
abstract class DataSourceConfiguration {
...
@SuppressWarnings("unchecked")
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
...
}
(gradle: org.springframework.boot:spring-boot 참조)
org.springframework.boot.jdbc.DataSourceProperties 파일
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
...
/**
* Initialize a {@link DataSourceBuilder} with the state of this instance.
* @return a {@link DataSourceBuilder} initialized with the customizations defined on
* this instance
*/
public DataSourceBuilder<?> initializeDataSourceBuilder() {
return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
.url(determineUrl()).username(determineUsername()).password(determinePassword());
}
...
}
따라가다 보면 등록된 정보중 몇몇개는 다른 이름으로 치환된다.
url => jdbc-url
username => user
public final class DataSourceBuilder<T extends DataSource> {
...
@SuppressWarnings("unchecked")
public T build() {
Class<? extends DataSource> type = getType();
DataSource result = BeanUtils.instantiateClass(type);
maybeGetDriverClassName();
bind(result);
return (T) result;
}
...
private void bind(DataSource result) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(this.properties);
ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
aliases.addAliases("url", "jdbc-url");
aliases.addAliases("username", "user");
Binder binder = new Binder(source.withAliases(aliases));
binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));
}
...
public DataSourceBuilder<T> url(String url) {
this.properties.put("url", url);
return this;
}
...
}
그래서 최종적으로 생성된 dataSource 를 보면 url은 없고 jdbcUrl만 있다.
dataSource = {HikariDataSource@3769} "HikariDataSource (null)"
isShutdown = {AtomicBoolean@3808} "false"
fastPathPool = null
pool = null
catalog = null
connectionTimeout = 30000
validationTimeout = 5000
idleTimeout = 600000
leakDetectionThreshold = 0
maxLifetime = 1800000
maxPoolSize = -1
minIdle = -1
username = "alert"
password = "test1234"
initializationFailTimeout = 1
connectionInitSql = null
connectionTestQuery = null
dataSourceClassName = null
dataSourceJndiName = null
driverClassName = "com.mysql.cj.jdbc.Driver"
jdbcUrl = "jdbc:mysql://localhost:3306/alert?serverTimezone=UTC"
poolName = null
schema = null
transactionIsolationName = null
isAutoCommit = true
isReadOnly = false
isIsolateInternalQueries = false
isRegisterMbeans = false
isAllowPoolSuspension = false
dataSource = null
dataSourceProperties = {Properties@3813} size = 0
threadFactory = null
scheduledExecutor = null
metricsTrackerFactory = null
metricRegistry = null
healthCheckRegistry = null
healthCheckProperties = {Properties@3814} size = 0
sealed = false
Alert 엔티티를 생성한다
package kr.jpamulti.jpa.alert.entity;
import javax.persistence.Entity;
import javax.persistence.Id
@Entity
public class Alert {
@Id
private Long id;
private String content;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
레파지토리를 생성한다
package kr.jpamulti.jpa.alert.repository;
import kr.jpamulti.jpa.alert.entity.Alert;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AlertRepository extends JpaRepository<Alert, Long> {
}
# 테스트 작성
테스트 클래스를 생성한다
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class AlertRepositoryTest {
@Autowired
private AlertRepository alertRepository;
@Test
public void crud(){
Alert alert = new Alert();
alert.setId(1l);
alert.setContent("테스트입니다");
alertRepository.save(alert);
Long count = alertRepository.count();
System.out.println("========================== 카운트: " + count);
assertThat(count).isEqualTo(1);
}
}
에러가 난다. 문구를 살펴보니 기본 dataSource가 없어 에러가 난다.
기본 dataSource 를 생성하지 못했기 때문.
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception; nested exception is org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class |
해결하기 위해선 2가지 선택이 있다.
1) 테스트에 @DataJpaTest 대신 @SpringBootTest를 추가
2) 관련 빈을 사용하는 Configuration 을 Import로 등록
여기서는 후자를 사용해서 Import를 이용했다.
@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(AlertDataSourceConfig.class)
public class AlertRepositoryTest {
...
로그 확인
Hibernate:
select
alert0_.id as id1_0_0_,
alert0_.content as content2_0_0_
from
Alert alert0_
where
alert0_.id=?
2019-08-03 20:49:17.128 INFO 32220 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
Hibernate:
insert
into
Alert
(content, id)
values
(?, ?)
Hibernate:
select
count(*) as col_0_0_
from
Alert alert0_
========================== 카운트: 1
데이터베이스에서 결과 확인
mysql> show tables;
+-----------------+
| Tables_in_alert |
+-----------------+
| Alert |
+-----------------+
1 row in set (0.00 sec)
mysql> select * from Alert;
Empty set (0.00 sec)
테스트이기 때문에 롤백되어 데이터가 없다.
끝.
참고 github
https://github.com/lemontia/jpa-datasource-custom
'공부 > 프로그래밍' 카테고리의 다른 글
[jenkins] Execute Shell 에서 프로세스 이름으로 프로세스 kill 하기 (0) | 2019.08.22 |
---|---|
[spring] @Transactional 작동 안할때 확인해봐야 할 것 (4) | 2019.08.16 |
[shell] 프로세스 실행 중 확인 (ps 명령어) (0) | 2019.07.31 |
[jenkins] 에러로그 Disk Full(DNSQuestion) (0) | 2019.07.28 |
[nodejs] AWS S3 파일업로드 (0) | 2019.07.22 |
댓글