공부/프로그래밍

[springboot] jpa 2개이상 DB사용(querydsl 설정 포함)

demonic_ 2020. 9. 17. 13:52

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

 

Spring Boot JPA - multiple database 설정. - mudchobo devlog

리플리케이션 되어 있는 디비에서 서비스쪽에서는 전부 마스터, 통계와 같이 슬로우쿼리가 걸리는 애들은 슬레이브로 쿼리를 날리기 위해서 여기까지 왔다. 역시 이 방법이 제일 맘에 드는 것 �

mudchobo.github.io

https://www.4te.co.kr/892

 

멀티 DataSource 접속 방법 정리

한 개의 프로젝트에서 하나의 Database에만 접속하는 경우가 대부분이지만, 2개 이상의 데이터 베이스에 접속하는 경우도 발생하게 된다. 이럴 경우 어떻게 셋팅을 해야 하는지 정리한다. application

www.4te.co.kr