2개 이상의 DBMS를 JPA를 이용해 접근하는 법 + Querydsy( queryFactory) 사용 방법을 정리한다
한가지 조건이 있다면, 둘다 hibernate 설정이 같아야 한다.(Datasource는 다름)
만약 이부분을 다르게 할거라면 아래 설정중에 LocalContainerEntityManagerFactoryBean 를 설정하는 부분에서 삽입될 properties를 수정하면 된다.
여기서는 mysql을 사용했다.
우선 application.properties에 설정된 값을 먼저 살펴보자.
각자 서버에 맞게 설정하면 되겠다.
spring.jpa.show-sql=true
spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jackson.serialization.write-dates-as-timestamps=false
프로젝트에서 JPA의 entity와 repository 폴더를 분리한다.
내 경우는 alert와 monitor 로 분리했다.
각각 EntityManager를 생성할 때 참조할 위치를 적어줘야 한다.
이번에는 각 접속할 Jpa 설정을 할 차례다.
AlertJpaConfig, MonitorJpaConfig 2개의 파일을 생성한다.
여기서 기본으로 설정할 것을 정해야 한다. 추후사용할땐 패키지 경로나 Bean이름으로 구분한다 하더라도, Spring에서는 기본으로 1개가 무조건 설정되어야 한다. (명시하지 않으면 기본으로 설정된 Config 값이 주입된다)
그래서 @Primary를 사용해 지정하는데, 여기서는 AlertJpaConfig 안의 Bean에 지정했다.
(monitorJpaConfig쪽에는 @Primary 를 제외하고 설정)
AlertJpaConfing.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
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.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Map;
@Configuration
@EnableJpaRepositories(
entityManagerFactoryRef = "alertEntityManagerFactory"
, transactionManagerRef = "alertTransactionManager"
, basePackages = "[레파지토리 패키지 경로]"
)
public class AlertJpaConfig {
@Autowired
private Environment env;
@Autowired
private JpaProperties jpaProperties;
@Autowired
private HibernateProperties hibernateProperties;
@Primary
@Bean
public DataSource alertDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("spring.datasource.driver-class-name"));
dataSource.setUrl(env.getProperty("spring.datasource.url"));
dataSource.setUsername(env.getProperty("spring.datasource.username"));
dataSource.setPassword(env.getProperty("spring.datasource.password"));
return dataSource;
}
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean alertEntityManagerFactory (EntityManagerFactoryBuilder builder) {
Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
jpaProperties.getProperties(), new HibernateSettings());
return builder
.dataSource(alertDataSource())
.packages("[엔티티 패키지 경로]")
.persistenceUnit("alertEntityManager")
.properties(properties)
.build();
}
@Bean
@Primary
public PlatformTransactionManager alertTransactionManager(@Qualifier(value = "alertEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
monitorJpaConfig는 다음과 같이 구성했다.
@Configuration
@EnableJpaRepositories(
entityManagerFactoryRef = "monitorEntityManagerFactory"
, transactionManagerRef = "monitorTransactionManager"
, basePackages = "[레파지토리 패키지 경로]"
)
public class MonitorJpaConfig {
@Autowired
private Environment env;
@Autowired
private JpaProperties jpaProperties;
@Autowired
private HibernateProperties hibernateProperties;
@Bean
public DataSource monitorDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("monitor.datasource.driver-class-name"));
dataSource.setUrl(env.getProperty("monitor.datasource.url"));
dataSource.setUsername(env.getProperty("monitor.datasource.username"));
dataSource.setPassword(env.getProperty("monitor.datasource.password"));
return dataSource;
}
@Bean
public LocalContainerEntityManagerFactoryBean monitorEntityManagerFactory (EntityManagerFactoryBuilder builder) {
Map<String, Object> properties = hibernateProperties.determineHibernateProperties(
jpaProperties.getProperties(), new HibernateSettings());
return builder
.dataSource(monitorDataSource())
.packages("[엔티티 패키지 경로]")
.persistenceUnit("monitorEntityManager")
.properties(properties)
.build();
}
@Bean
public PlatformTransactionManager monitorTransactionManager(@Qualifier(value = "monitorEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
LocalContainerEntityManagerFactoryBean 내에 설정하는 package는 엔티티 모델이 위치하는 곳을 지정해야 하고, EnableJpaRepositories 에 지정하는 basePackage는 Repository파일이 있는 경로를 지정해야 한다. 처음에 이걸 몰라서 한참 해맸다.
만약 LocalContainerEntityManagerFactoryBean안에 basePackage를 잘못 설정하게 된다면 다음 에러를 보게 된다.
org.hibernate.hql.internal.ast.QuerySyntaxException: LastDataMonitor is not mapped |
또는
org.hibernate.UnknownEntityTypeException: Unable to locate persister: entity.Settings |
persistenceUnit은 추후 EntityManager에서 사용할 고유이름이다.
properties는 보면 hibernateProperties와 jpaProperties를 혼합해서 Map으로 만든것인데, 그 안을 보면 다음과 같다.
properties 변수안을 보면 jpaProperties가 제대로 들어간지 의심스럽다. 그래서 determineHibernateProperties 메서드를 살펴보니 다음과 같이 jpaRepository가 아니면 에러가 발생되게 되어있고, 이를 이용해 프로퍼티를 추가한다.
아래는 그 메서드의 세부내용이다.
...
/**
* Determine the configuration properties for the initialization of the main Hibernate
* EntityManagerFactory based on standard JPA properties and
* {@link HibernateSettings}.
* @param jpaProperties standard JPA properties
* @param settings the settings to apply when determining the configuration properties
* @return the Hibernate properties to use
*/
public Map<String, Object> determineHibernateProperties(Map<String, String> jpaProperties,
HibernateSettings settings) {
Assert.notNull(jpaProperties, "JpaProperties must not be null");
Assert.notNull(settings, "Settings must not be null");
return getAdditionalProperties(jpaProperties, settings);
}
...
호출되는 getAdditionalProperties 내부를 보면 다음과 같이 되어있다
...
private Map<String, Object> getAdditionalProperties(Map<String, String> existing, HibernateSettings settings) {
Map<String, Object> result = new HashMap<>(existing);
applyNewIdGeneratorMappings(result);
applyScanner(result);
getNaming().applyNamingStrategies(result);
String ddlAuto = determineDdlAuto(existing, settings::getDdlAuto);
if (StringUtils.hasText(ddlAuto) && !"none".equals(ddlAuto)) {
result.put(AvailableSettings.HBM2DDL_AUTO, ddlAuto);
}
else {
result.remove(AvailableSettings.HBM2DDL_AUTO);
}
Collection<HibernatePropertiesCustomizer> customizers = settings.getHibernatePropertiesCustomizers();
if (!ObjectUtils.isEmpty(customizers)) {
customizers.forEach((customizer) -> customizer.customize(result));
}
return result;
}
...
내용중에 DDL을 수정하는 내용이 있는데, 내 설정은 none이기 때문에 Map 내용에서 remove된다.
만약 값이 다르면 put 으로 들어갈까? 테스트 해보았다.
설정을 update로 변경했더니 변경된 값이 들어갔다.
이로써 application.properties의 값이 제대로 반영됨을 알 수 있다.
다만 이것을 쓰기 힘들다면 다음처럼 Map을 직접 구성하거나 혹은 생략하는 것도 방법인거 같다.
(properties를 설정하지 않아도 간단한 기능은 작동했다. 다만 정확히 파악되는게 아니니 설정을 추천한다)
(그런데... 만약 DBMS가 다르면 어떻게 설정해야하지....?)
아무튼 Map에 추가된 내용은 아래 적어둔다.
{hibernate.id.new_generator_mappings=true
, hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
, hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
, hibernate.archive.scanner=org.hibernate.boot.archive.scan.internal.DisabledScanner}
그럼 이제 Entity와 Repository를 생성해보자.
Entity는 다음과 같이 2개를 설정했다.
alert 서버의 엔터티
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "temp_test")
@NoArgsConstructor
@Getter
public class TempTest {
@Id
private String key;
@Column(name = "value")
private String value;
}
monitor DB의 엔터티
@Entity
@Table(name = "temp_test_monitor")
@NoArgsConstructor
@Getter
public class TempTestMonitor {
@Id
private String key;
@Column(name = "value")
private String value;
}
Repository 역시 각각 만든다.
다만 querydsl을 쓰려면 다음과같이 설정이 필요하다.
@Repository
public class TempTestRepository extends QuerydslRepositorySupport {
public TempTestRepository() {
super(TempTest.class);
}
@Override
@PersistenceContext(unitName="alertEntityManager")
public void setEntityManager(EntityManager entityManager) {
super.setEntityManager(entityManager);
em = entityManager;
this.queryFactory = new JPAQueryFactory(entityManager);
}
private EntityManager em;
private JPAQueryFactory queryFactory;
public List<TempTest> findAll() {
return queryFactory.selectFrom(tempTest).fetch();
}
}
QuerydslRepositorySupport 내에 setEntityManager를 오버라이드 하여 삽입하는 것이고, 그 위에 PersistenceContext에 unitName을 위에서 지정한 alertEntityManager로 입력해줘야 한다. 만약 입력해주지 않는다면 @Primary로 등록한 것이 우선 삽입된다. 그러나 유지보수 면에서 명시하길 추천.
JPAQueryFactory 역시 entityManager를 이용해 생성한다.
querydsl 테스트를 위해 일부로 queryFactory를 이용한 메서드를 만들었다.
Monitor쪽도 생성
@Repository
public class TempTestMonitorRepository extends QuerydslRepositorySupport {
public TempTestMonitorRepository() {
super(TempTestMonitor.class);
}
@Override
@PersistenceContext(unitName="monitorEntityManager")
public void setEntityManager(EntityManager entityManager) {
super.setEntityManager(entityManager);
em = entityManager;
this.queryFactory = new JPAQueryFactory(entityManager);
}
private EntityManager em;
private JPAQueryFactory queryFactory;
public List<TempTestMonitor> findAll() {
return queryFactory.selectFrom(tempTestMonitor).fetch();
}
}
이제 이 둘이 잘 작동하는지 점검해보고자 한다.
테스트를 다음과 같이 작성한다.
@SpringBootTest
public class MultiConnectTest {
@Autowired
private TempTestRepository tempTestRepository;
@Autowired
private TempTestMonitorRepository tempTestMonitorRepository;
@Test
@DisplayName("Mysql 멀티 커넥트 테스트")
void multiConnectActionTest() {
List<TempTest> findAll = tempTestRepository.findAll();
for (TempTest tempTest : findAll) {
System.out.println("tempTest.getValue() = " + tempTest.getValue());
}
List<TempTestMonitor> findAll2 = tempTestMonitorRepository.findAll();
for (TempTestMonitor tempTestMonitor : findAll2) {
System.out.println("tempTestMonitor.getValue() = " + tempTestMonitor.getValue());
}
}
}
결과
Hibernate: select temptest0_.key as key1_1_, temptest0_.value as value2_1_ from temp_test temptest0_
tempTest.getValue() = 좋아요
Hibernate: select temptestmo0_.key as key1_1_, temptestmo0_.value as value2_1_ from temp_test_monitor temptestmo0_
tempTestMonitor.getValue() = 안녕
끝.
참조:
https://mudchobo.github.io/posts/spring-boot-jpa-multiple-database
'공부 > 프로그래밍' 카테고리의 다른 글
[react] 개발 구성 시 필요한 패키지 정리(업데이트중) (0) | 2020.10.13 |
---|---|
[react] checkbox 전체선택하게 하기(장바구니 상품삭제 기능) (0) | 2020.10.07 |
[airflow] GCS(Google Cloud Storage) 파일을 BigQuery에 저장하기 (0) | 2020.09.09 |
[airflow] Mysql의 캐릭터셋(charset)이 euc-kr 일 때(혹은 로드시 can't decode byte 0xb5에러가 날때) (0) | 2020.09.07 |
[python3] 기존에 되던것이 No module named 뜰 때 (0) | 2020.09.03 |
댓글