본문 바로가기
spring/spring jpa

[SpringBoot] Mybatis + JPA + QueryDsl 설정

by moonsiri 2021. 6. 10.
728x90
반응형

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/

https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#_sql_statement_logging

728x90
반응형

댓글