jpa_multiple_databases: 멀티DB 연동 샘플

This commit is contained in:
Bora 2024-07-23 16:37:54 +09:00
parent e030045ac3
commit ccd34cfa5c
36 changed files with 1079 additions and 9 deletions

View File

@ -3,21 +3,20 @@ services:
mariadb: mariadb:
container_name: mariadb container_name: mariadb
image: mariadb:10.11.7 image: mariadb:10.11.7
restart: always
build: build:
context: . context: .
dockerfile: Dockerfiles dockerfile: Dockerfiles
ports: command: --init-file /docker-entrypoint-initdb.d/init.sql
- 3307:3306
volumes: volumes:
- data:/var/lib/mysql # named volume - data:/var/lib/mysql # named volume
- ./conf.d:/etc/mysql/conf.d # volume mount - ./conf.d:/etc/mysql/conf.d # volume mount
- ./init.d:/docker-entrypoint-initdb.d
environment: environment:
- TZ=Asia/Seoul - TZ=Asia/Seoul
- MARIADB_ROOT_PASSWORD=root - MARIADB_ROOT_PASSWORD=root
- MARIADB_DATABASE=admin-system ports:
- MARIADB_USER=admin - 3307:3306
- MARIADB_PASSWORD=1234 restart: always
volumes: volumes:
data: data:

View File

@ -0,0 +1,12 @@
DROP USER IF EXISTS 'admin';
CREATE USER 'admin'@'%';
# create databases
CREATE DATABASE IF NOT EXISTS `admin-system`;
GRANT ALL ON `admin-system`.* TO 'admin'@'%' IDENTIFIED BY '1234';
CREATE DATABASE IF NOT EXISTS ADMIN_DATABASE;
GRANT ALL ON ADMIN_DATABASE.* TO 'admin'@'%' IDENTIFIED BY '1234';
CREATE DATABASE IF NOT EXISTS ADMIN_DATABASE_MASTER;
GRANT ALL ON ADMIN_DATABASE_MASTER.* TO 'admin'@'%' IDENTIFIED BY '1234';

View File

@ -8,6 +8,11 @@ INSERT INTO `admin` (`login_id`, `password`, `email`, `name`, `role_id`, `create
VALUES ('admin', '$2a$10$g6UOrQ/OS8o5r5CJk7C5juVFaItQ62U3EIn8zLPzkFplM3wVLvKZ2', 'admin@admin.com', '홍길동', 1, CURDATE(), VALUES ('admin', '$2a$10$g6UOrQ/OS8o5r5CJk7C5juVFaItQ62U3EIn8zLPzkFplM3wVLvKZ2', 'admin@admin.com', '홍길동', 1, CURDATE(),
CURDATE()); CURDATE());
INSERT INTO `admin` (`login_id`, `password`, `email`, `name`, `role_id`, `create_date`, `update_date`)
VALUES ('admin1', '$2a$10$gyIn.eVOD/eFY3cCZGnNau3buR4a./BifPPTbJbfgEvvoQiJlnRYG', 'admin@admin.com', '홍길동', 1, CURDATE(),
CURDATE());
INSERT INTO `menu_group` (`uri`, `name`, `sort_order`, `create_date`, `update_date`) INSERT INTO `menu_group` (`uri`, `name`, `sort_order`, `create_date`, `update_date`)
VALUES ('/main', '메인', 1, CURDATE(), CURDATE()), VALUES ('/main', '메인', 1, CURDATE(), CURDATE()),
('/admin', '관리자 관리', 2, CURDATE(), CURDATE()), ('/admin', '관리자 관리', 2, CURDATE(), CURDATE()),

View File

@ -25,7 +25,9 @@
<div class="container"> <div class="container">
<th:block th:replace="~{layout/lnb :: lnb}"></th:block> <th:block th:replace="~{layout/lnb :: lnb}"></th:block>
<div class="content"> <div class="content">
<th:block layout:fragment="contents"/> <div class="con_wrap">
<th:block layout:fragment="contents"/>
</div>
</div> </div>
</div> </div>
</main> </main>

37
poc/jpa_multiple_databases/.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@ -0,0 +1,56 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.bpgroup.poc'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// spring
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
//jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//querydsl
implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
// dev container
developmentOnly 'org.springframework.boot:spring-boot-devtools'
//lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// db
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
}
tasks.named('test') {
useJUnitPlatform()
}

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
poc/jpa_multiple_databases/gradlew vendored Normal file
View File

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
poc/jpa_multiple_databases/gradlew.bat vendored Normal file
View File

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1 @@
rootProject.name = 'jpa_multiple_databases'

View File

@ -1,4 +1,4 @@
package com.bpgorup.poc.jpa_sp; package com.bpgroup.poc.jpa_multi_db;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter; import lombok.Getter;

View File

@ -0,0 +1,13 @@
package com.bpgroup.poc.jpa_multi_db;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MultipleDbApplication {
public static void main(String[] args) {
SpringApplication.run(MultipleDbApplication.class, args);
}
}

View File

@ -0,0 +1,29 @@
package com.bpgroup.poc.jpa_multi_db.common;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.slf4j.helpers.MessageFormatter;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FormatHelper {
/**
* Slf4J 스타일로 문자열 포멧팅을 해주는 함수
* <p>
* Example:
* <pre>
* String str1 = FormatHelper.format("{} {} {} {}", "1", 2, 3L, true);
* str1.equals("1 2 3 true");
*
* Object[] params = {"1", 2, 3L, true};
* String str2 = FormatHelper.format("{} {} {} {}", params);
* str2.equals("1 2 3 true");
* </pre>
*
* @param pattern 파싱되서 params를 적용해 포맷팅 패턴
* @param params pattern의 {} 치환
* @return 포맷팅 문자열
*/
public static String format(String pattern, Object... params) {
return MessageFormatter.arrayFormat(pattern, params).getMessage();
}
}

View File

@ -0,0 +1,17 @@
package com.bpgroup.poc.jpa_multi_db.common;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class SubStringHelper {
public static String substringInBytes(String original, int byteLength) {
byte[] bytes = original.getBytes(StandardCharsets.UTF_8);
if (bytes.length > byteLength) {
bytes = Arrays.copyOf(bytes, byteLength);
return new String(bytes, StandardCharsets.UTF_8);
}
return original;
}
}

View File

@ -0,0 +1,64 @@
package com.bpgroup.poc.jpa_multi_db.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.HashMap;
@Configuration // 설정파일 어노테이션
@EnableTransactionManagement // 트랜잭션 범위를 활성화 주는 어노테이션
@EnableJpaRepositories(
basePackages = "com.bpgroup.poc.jpa_multi_db.domain",
entityManagerFactoryRef = "primaryEntityManager",
transactionManagerRef = "primaryTransactionManager"
)
//@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class PrimaryConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.hikari.admin")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(primaryDataSource());
em.setPackagesToScan(new String[] {"com.bpgroup.poc.jpa_multi_db.domain"});
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setShowSql(true);
vendorAdapter.setGenerateDdl(true);
em.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> prop = new HashMap<>();
prop.put("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect");
prop.put("hibernate.hbm2ddl.auto", "create-drop");
prop.put("hibernate.format_sql", true);
em.setJpaPropertyMap(prop);
return em;
}
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(primaryEntityManager().getObject());
return transactionManager;
}
}

View File

@ -0,0 +1,35 @@
package com.bpgroup.poc.jpa_multi_db.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuerydslConfig {
/*@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}*/
@PersistenceContext(unitName = "primaryEntityManager")
private EntityManager primaryEntityManager;
@PersistenceContext(unitName = "secondEntityManager")
private EntityManager secondEntityManager;
@Bean
public JPAQueryFactory firstJpaQueryFactory() {
return new JPAQueryFactory(primaryEntityManager);
}
@Bean
public JPAQueryFactory secondJpaQueryFactory() {
return new JPAQueryFactory(secondEntityManager);
}
}

View File

@ -0,0 +1,79 @@
package com.bpgroup.poc.jpa_multi_db.config;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
@Configuration
@EnableJpaRepositories(
basePackages = "com.bpgroup.poc.jpa_multi_db.userdomain",
entityManagerFactoryRef = "secondEntityManager",
transactionManagerRef = "secondTransactionManager"
)
public class SecondConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari.admin-sub")
public DataSource secondDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondEntityManager() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(secondDataSource());
em.setPackagesToScan(new String[] {"com.bpgroup.poc.jpa_multi_db.userdomain"});
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setShowSql(true);
vendorAdapter.setGenerateDdl(true);
em.setJpaVendorAdapter(vendorAdapter);
HashMap<String, Object> prop = new HashMap<>();
prop.put("hibernate.dialect", "org.hibernate.dialect.MariaDBDialect");
prop.put("hibernate.hbm2ddl.auto", "create-drop");
prop.put("hibernate.format_sql", true);
em.setJpaPropertyMap(prop);
return em;
}
@Bean
public PlatformTransactionManager secondTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(secondEntityManager().getObject());
return transactionManager;
}
/**
* H2 DB 초기화 스크립트 실행을 위한
* DataSourceInitializer 정의
* @param datasource
* @return dataSourceInitializer
*/
@Bean
public DataSourceInitializer secondDataSourceInitializer(@Qualifier("secondDataSource") DataSource datasource) {
ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
resourceDatabasePopulator.addScript(new ClassPathResource("data-sub.sql"));
DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
dataSourceInitializer.setDataSource(datasource);
dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator);
return dataSourceInitializer;
}
}

View File

@ -0,0 +1,22 @@
package com.bpgroup.poc.jpa_multi_db.domain;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@Column(name = "create_date")
@CreatedDate
protected LocalDateTime createdDate;
@Column(name = "update_date")
@LastModifiedDate
protected LocalDateTime updatedDate;
}

View File

@ -0,0 +1,7 @@
package com.bpgroup.poc.jpa_multi_db.domain;
public class DomainException extends RuntimeException {
public DomainException(String message) {
super(message);
}
}

View File

@ -0,0 +1,35 @@
package com.bpgroup.poc.jpa_multi_db.domain.admin.entity;
import com.bpgroup.poc.jpa_multi_db.domain.BaseEntity;
import com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity.UserInfo;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@Table(name = "admin")
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 생성자를 통해서 변경 목적으로 접근하는 메시지들 차단
public class Admin extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "login_id", length = 100, nullable = false, unique = true)
private String loginId;
@Column(name = "password", length = 255, nullable = false)
private String password;
@Column(name = "email", length = 100, nullable = false)
private String email;
@Column(name = "name", length = 100, nullable = false)
private String name;
/*@OneToOne(mappedBy = "admin")
private UserInfo userInfo;*/
}

View File

@ -0,0 +1,7 @@
package com.bpgroup.poc.jpa_multi_db.domain.admin.entity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AdminRepository extends JpaRepository<Admin, Long> {
}

View File

@ -0,0 +1,17 @@
package com.bpgroup.poc.jpa_multi_db.domain.admin.service;
import com.bpgroup.poc.jpa_multi_db.domain.admin.entity.AdminRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@Service
@RequiredArgsConstructor
@Validated
@Transactional
public class AdminService {
private final AdminRepository adminRepository;
}

View File

@ -0,0 +1,33 @@
package com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity;
import com.bpgroup.poc.jpa_multi_db.domain.BaseEntity;
import com.bpgroup.poc.jpa_multi_db.domain.admin.entity.Admin;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@Table(name = "user_info")
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 생성자를 통해서 변경 목적으로 접근하는 메시지들 차단
public class UserInfo extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_name", length = 10)
private String userName;
@Column(name = "user_type", length = 10)
private String userType;
@Column(name = "admin_id")
private Long adminId;
/*@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "admin_id", foreignKey = @ForeignKey(name = "none"), nullable = false)
private Admin admin;*/
}

View File

@ -0,0 +1,10 @@
package com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserInfoRepository extends JpaRepository<UserInfo, Long> {
Optional<UserInfo> findByAdminId(Long adminId);
}

View File

@ -0,0 +1,21 @@
package com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.service;
import com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity.UserInfo;
import com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity.UserInfoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Validated
@Transactional
public class UserInfoService {
private final UserInfoRepository userInfoRepository;
public Optional<UserInfo> findByAdminId(Long adminId) { return userInfoRepository.findByAdminId(adminId);}
}

View File

@ -0,0 +1,33 @@
package com.bpgroup.poc.jpa_multi_db.web;
import com.bpgroup.poc.admin.web.multipledb.reqres.UserInfoFind;
import com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity.UserInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/multiDbSample")
public class MultiDbSampleController {
private final MultiDbSampleService multiDbSampleService;
@GetMapping("/searchMultipleDbTest")
public ResponseEntity<?> findUserList(Long adminId){
UserInfo response = multiDbSampleService.findUserInfo(adminId);
return ResponseEntity.ok(response);
}
@GetMapping("/searchMultipleDbJoin")
public ResponseEntity<?> findJoinUserList(Long adminId){
UserInfoFind.Response response = multiDbSampleService.findJoinUserInfo(adminId);
return ResponseEntity.ok(response);
}
}

View File

@ -0,0 +1,44 @@
package com.bpgroup.poc.jpa_multi_db.web;
import com.bpgroup.poc.admin.web.multipledb.reqres.UserInfoFind;
import com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity.UserInfo;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
import static com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity.QUserInfo.userInfo;
@Repository
@RequiredArgsConstructor
public class MultiDbSampleQueryRepository {
@Autowired
@Qualifier("firstJpaQueryFactory")
private final JPAQueryFactory firstJpaQueryFactory;
@Autowired
@Qualifier("secondJpaQueryFactory")
private final JPAQueryFactory secondJpaQueryFactory;
public UserInfo findUserInfo(Long adminId){
return secondJpaQueryFactory.selectFrom(userInfo)
.where(userInfo.adminId.eq(adminId))
.fetchOne();
}
/*public UserInfoFind.Response findJoinUserInfo(Long adminId){
return secondJpaQueryFactory.select( Projections.fields(
UserInfoFind.Response.class,
userInfo.id,
userInfo.userName,
admin.email
)).from(userInfo)
.join(admin).on(userInfo.adminId.eq(admin.id))
.where(userInfo.adminId.eq(adminId))
.fetchOne();
}*/
}

View File

@ -0,0 +1,38 @@
package com.bpgroup.poc.jpa_multi_db.web;
import com.bpgroup.poc.admin.web.multipledb.reqres.UserInfoFind;
import com.bpgroup.poc.jpa_multi_db.domain.admin.entity.Admin;
import com.bpgroup.poc.jpa_multi_db.domain.admin.entity.AdminRepository;
import com.bpgroup.poc.jpa_multi_db.domain.admin.service.AdminService;
import com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.entity.UserInfo;
import com.bpgroup.poc.jpa_multi_db.userdomain.userInfo.service.UserInfoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
@RequiredArgsConstructor
public class MultiDbSampleService {
private final UserInfoService userInfoService;
private final MultiDbSampleQueryRepository multiDbSampleQueryRepository;
private final AdminRepository adminRepository;
public UserInfo findUserInfo(Long adminId){
return userInfoService.findByAdminId(adminId).orElseThrow(() -> new IllegalArgumentException("UserInfo not found"));
}
public UserInfoFind.Response findJoinUserInfo(Long adminId){
UserInfo userInfo = multiDbSampleQueryRepository.findUserInfo(adminId);
Admin admin = adminRepository.findById(adminId).orElseThrow(() -> new IllegalArgumentException("Admin not found"));
return UserInfoFind.Response.builder()
.userInfoId(userInfo.getId())
.userName(userInfo.getUserName())
.email(admin.getEmail())
.build();
}
}

View File

@ -0,0 +1,17 @@
package com.bpgroup.poc.admin.web.multipledb.reqres;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
public class UserInfoFind {
@Getter
@ToString
@Builder
public static class Response {
private Long userInfoId;
private String userName;
private String email;
}
}

View File

@ -0,0 +1,35 @@
spring:
sql:
init:
mode: always
datasource:
hikari:
admin:
jdbc-url: jdbc:mariadb://localhost:3307/ADMIN_DATABASE
username: admin
password: 1234
driver-class-name: org.mariadb.jdbc.Driver
data-locations: data.sql
admin-sub:
jdbc-url: jdbc:mariadb://localhost:3307/ADMIN_DATABASE_MASTER
username: admin
password: 1234
driver-class-name: org.mariadb.jdbc.Driver
data-locations: data-sub.sql
jpa:
hibernate.hbm2ddl.auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
#hibernate:
# ddl-auto: create-drop
defer-datasource-initialization: true
logging:
config: classpath:logback-local.xml
server:
port: 8082

View File

@ -0,0 +1,6 @@
INSERT INTO `user_info` (`user_name`, `user_type`, `admin_id`, `create_date`, `update_date`)
VALUES ('테스1', '관리자', 1, CURDATE(), CURDATE()),
('테스2', '관리자', 2, CURDATE(), CURDATE());

View File

@ -0,0 +1,13 @@
INSERT INTO `admin` (`login_id`, `password`, `email`, `name`, `create_date`, `update_date`)
VALUES ('admin', '$2a$10$g6UOrQ/OS8o5r5CJk7C5juVFaItQ62U3EIn8zLPzkFplM3wVLvKZ2', 'admin@admin.com', '홍길동', CURDATE(),
CURDATE());
INSERT INTO `admin` (`login_id`, `password`, `email`, `name`, `create_date`, `update_date`)
VALUES ('admin1', '$2a$10$gyIn.eVOD/eFY3cCZGnNau3buR4a./BifPPTbJbfgEvvoQiJlnRYG', 'admin@admin.com', '홍길동', CURDATE(),
CURDATE());

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%5p|%d{yyyy-MM-dd HH:mm:ss.SSS}|%X{loginId}|%32X{sessionId}|%32X{transactionId}|%logger:%line|%msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
<logger name="org.springframework" level="INFO" />
<logger name="com.zaxxer.hikari" level="INFO" />
<logger name="org.mariadb.jdbc.client" level="INFO" />
<logger name="org.mariadb.jdbc.message" level="INFO" />
<logger name="jdbc" level="OFF" />
<logger name="log4jdbc" level="INFO" />
<logger name="org.hibernate" level="INFO" />
<logger name="org.thymeleaf" level="INFO" />
<logger name="org.hibernate.orm.jdbc" level="TRACE" />
</configuration>

View File

@ -0,0 +1,11 @@
package jpa_multiple_databases.jpa_multi_db;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@ActiveProfiles("test")
class MultipleDbApplicationTests {
}

View File

@ -1,4 +1,4 @@
package jpa_sp_poc.jpa_sp; package jpa_multiple_databases.jpa_multi_db;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;