https://moonsiri.tistory.com/53 기존에 설정해둔 DB Configuration에 설정을 추가하겠습니다.
Spring Boot에서 MyBatis, JPA, 그리고 QueryDSL을 함께 사용하는 설정 방법을 소개합니다.
1. JPA 설정
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
DatabaseConfig.java
@Configuration
@EnableJpaRepositories(
basePackages = "com.moonsiri.**.repository",
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "jpaTxManager"
)
@MapperScan(
basePackages = "com.moonsiri.**.dao",
sqlSessionFactoryRef = "sqlSessionFactory"
)
public class DatabaseConfig {
/**
* hikari config
*/
@Bean(name= "hikariConfig")
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariConfig hikariConfig() {
return new HikariConfig();
}
/**
* datasource
*/
@Bean(name= "dataSource")
public HikariDataSource dataSource(@Qualifier("hikariConfig") HikariConfig hikariConfig) {
return new HikariDataSource(hikariConfig);
}
@Bean(name= "jpaDataSource")
public HikariDataSource jpaDataSource(@Qualifier("hikariConfig") HikariConfig hikariConfig) {
return new HikariDataSource(hikariConfig);
}
/**
* sessionfactory
*/
@Bean(name= "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactoryBean.setMapperLocations(resolver.getResources("classpath:sqlmapper/*.xml")); //mapper path
Objects.requireNonNull(sessionFactoryBean.getObject()).getConfiguration().setMapUnderscoreToCamelCase(true); //camelCase
return sessionFactoryBean.getObject();
}
/**
* sqlsession
*/
@Bean(name= "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* jpa entityManagerFactory
*/
@Bean(name = "entityManagerFactory")
public EntityManagerFactory entityManagerFactory(@Qualifier("jpaDataSource") DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setPackagesToScan("com.moonsiri.**.domain");
factory.setDataSource(dataSource);
factory.setPersistenceUnitName("entityManager");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setShowSql(true);
factory.setJpaVendorAdapter(vendorAdapter);
Map<String, Object> properties = new HashMap<>();
properties.put(AvailableSettings.HBM2DDL_AUTO, "none");
properties.put(AvailableSettings.DIALECT, "org.hibernate.dialect.MySQL5Dialect"); // springboot3 부터는 org.hibernate.dialect.MySQLDialect
properties.put(AvailableSettings.FORMAT_SQL, true);
properties.put(AvailableSettings.SHOW_SQL, false); // sql은 log4j로 출력 org.hibernate.SQL=DEBUG
properties.put(AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, true); // 예약어 컬럼명 사용 허용
factory.setJpaPropertyMap(properties);
factory.afterPropertiesSet();
return factory.getObject();
}
/**
* transaction manager
*/
@Bean(name= "txManager")
public PlatformTransactionManager txManager(@Qualifier("dataSource") DataSource dataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
dataSourceTransactionManager.setNestedTransactionAllowed(true); // nested
return dataSourceTransactionManager;
}
@Bean(name= "jpaTxManager")
public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
return jpaTransactionManager;
}
}
JpaRepository의 경우 `EnableJpaRepositories`에 등록된 트랜잭션을 사용하기 때문에 선언적 트랜잭션을 사용하지 않아도 됩니다.
단, JpaRepository에 새로 추가한 queryMethod나 QueryDSL에서 update/delete 쿼리 사용 시 반드시 선언적 트랜잭션(@Transactional(value = "jpaTxManager")을 사용해야 합니다.
또한 entity가 영속 상태일 경우 트랜잭션의 commit 시점에 영속성 컨텍스트에 있는 정보들이 DB에 쿼리로 날아가는데, 이 경우도 선언적 트랜잭션이 필요합니다.
hibernate.show_sql 설정을 true로 해놓으면 Hibernate가 DB에 수행하는 모든 쿼리문을 콘솔에 출력합니다.
show_sql은 System.out으로 출력하므로 false로 설정하고, 쿼리 확인이 필요한 환경에서만 org.hibernate.SQL을 debug로 설정하는 것이 좋습니다.
1.1. JPA 기본 구조
UserEntity.java
@Entity
@DynamicUpdate
@Table(name="user", indexes = {
@Index(name = "UQ_userId", columnList = "user_id", unique = true)
})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserEntity {
@Id
@Column(columnDefinition = "BIGINT", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
private Long id;
@Column(name="user_id", nullable = false)
private String userId;
@Column(name="user_name", nullable = false)
private String userName;
private int age;
@Builder
public UserEntity(String userId, String userName, int age) {
this.userId = userId;
this.userName = userName;
this.age = age;
}
}
UserRepository.java
public interface UserRepository extends JpaRepository<UserEntity, Long> {
}
UserDTO.java
public class UserDTO {
@Getter
@Setter
@NoArgsConstructor
public static class AddReq {
@NotEmpty
private String userId;
@NotEmpty
private String userName;
private int age;
public UserEntity toEntity() {
return UserEntity.builder()
.userId(userId)
.userName(userName)
.age(age)
.build();
}
}
}
UserService.java
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public void addUser(UserDTO.AddReq param) {
userRepository.save(param.toEntity());
}
}
1.2. Auditing 적용
생성일, 작성자, 수정일, 수정자 insert 자동화
AuditingConfiguration.java
@Configuration
@EnableJpaAuditing // JPA Auditing 어노테이션들을 모두 활성화
public class AuditingConfiguration {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(SecurityContextHolder.getContext().getAuthentication().getName());
}
}
BaseEntity.java
@Getter
@MappedSuperclass // JPA Entity 클래스들이 BaseEntity를 상속할 경우 필드들도 컬럼으로 인식하도록 함.
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedBy
@Column(name="reg_emp_id", length = 20, updatable = false)
private String regEmpId;
@CreatedDate
@Column(name="reg_ymdt", updatable = false)
private LocalDateTime regYmdt;
@LastModifiedBy
@Column(name="mod_emp_id", length = 20)
private String modEmpId;
@LastModifiedDate
@Column(name="mod_ymdt")
private LocalDateTime modYmdt;
}
Entity에 BaseEntity 클래스를 상속하면 등록일, 등록자 값을 넣어주지 않더라도 자동으로 값이 들어갑니다.
@Getter
@Entity
@Table(name="user")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserEntity extends BaseEntity {
// ...
}
2. QueryDsl 설정
pom.xml
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
<!-- JDK 17 이상인 경우 추가 -->
<!-- <classifier>jakarta</classifier> -->
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<!-- JDK 17 이상인 경우 추가 -->
<!-- <classifier>jakarta</classifier> -->
</dependency>
</dependencies>
<!-- 버전에 따라 필요시 설정 -->
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
`com.querydsl.apt.jpa.JPAAnnotationProcessor`는 `javax.persistence.Entity annotation`이 추가된 도메인 타입을 찾아 Q-타입(쿼리타입)을 생성해줍니다. Q-타입 entity를 생성하기위해 `maven install` 혹은 `compile`이 필요합니다.
Spring Boot 2.0 이상과 QueryDSL 4.0 이상을 사용하면 별도의 `apt-maven-plugin` 설정 없이도 JPAAnnotationProcessor가 자동으로 동작하여 Q-타입을 생성합니다.
QueryDslConfiguration.java
@Configuration
public class QueryDslConfiguration {
@PersistenceContext(unitName = "entityManager")
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
UserRepositoryCustom.java
@Repository
@Transactional(value = "jpaTxManager", readOnly = true)
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
@Resource
private JPAQueryFactory queryFactory;
private static final QUserEntity userEntity = QUserEntity.userEntity;
public long count(UserDTO.GetListReq param) {
return queryFactory.selectFrom(userEntity)
.where(this.where(param))
.fetchCount(); // springboot2.6이상부터 deprecated
}
/* springboot2.6이상부터 fetchCount는 deprecated 되어 아래와 같이 사용
public long count(UserDTO.GetListReq param) {
return queryFactory.select(userEntity.count())
.from(userEntity)
.where(this.where(param))
.fetchOne();
}*/
public List<UserEntity> findAllOrderByIdDesc(UserDTO.GetListReq param) {
return queryFactory.selectFrom(userEntity)
.where(this.where(param))
.orderBy(userEntity.id.desc())
.offset(param.getLimitOffSet())
.limit(param.getRecordCountPerPage())
.fetch();
}
private BooleanBuilder where(UserDTO.GetListReq param) {
BooleanBuilder where = new BooleanBuilder();
if (StringUtils.isNotBlank(param.getUserName()) {
where.and(userEntity.userName.eq(param.getUserName()));
}
return where;
}
}
Spring Boot 2.6 이상에서 `fetchCount()`가 deprecated 되었으므로 `select(userEntity.count()).fetchOne()`을 사용하여 카운트를 조회해야합니다.
[Reference]
https://blog.naver.com/myh814/221643516923
https://docs.spring.io/spring-data/commons/docs/2.5.1/reference/html/
https://docs.spring.io/spring-data/jpa/docs/2.5.1/reference/html/
'spring > spring jpa' 카테고리의 다른 글
[QueryDSL] FullText 검색을 위한 Match ... against 절 사용법 (0) | 2023.08.24 |
---|---|
[SpringBoot] QueryDSL org.apache.jasper.JasperException: Unable to compile class for JSP: (0) | 2023.08.24 |
[SpringBoot2] JPA Master/Slave 구조 분기 처리 (2) | 2023.08.23 |
[SpringBoot] JPA Projection (0) | 2021.06.23 |
[Spring] JPA 연관 관계 매핑 및 제거 (0) | 2021.06.18 |
댓글