[Spring DB] 데이터 접근 기술

hi·2022년 12월 26일
0

SQLMapper

  • jdbcTemplate
  • MyBatis
  • SQL만 작성하면 결과를 객체로 편리하게 매핑해 줌
  • JDBC 직접 사용시 여러 중복 제거, 편리한 기능 제공

ORM 관련 기술

  • JPA, Hibernate
  • 스프링 데이터 JPA
  • Querydsl
  • 기본적인 SQL은 JPA가 대신 작성하고 처리
  • 객체를 자바 컬렉션에 저장하고 조회하듯 사용하면 ORM 기술이 데이터베이스에 해당 객체를 저장하고 조회
  • JPA는 자바 진영의 ORM 표준, Hibernate는 JPA에서 가장 많이 사용하는 구현체

JdbcTemplate

  • 가장 간단하고 실용적인 방법
  • 동적 쿼리 문제를 해결하지 못하는 단점

주요 기능

JdbcTemplate : 순서 기반 파라미터 바인딩을 지원
NamedParameterJdbcTemplate : 이름 기반 파라미터 바인딩을 지원 (권장)
SimpleJdbcInsert : INSERT SQL을 편리하게 사용 가능
SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출 가능


설정

build.gradle

dependencies {
	
    //JdbcTemplate 추가
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	
    //H2 데이터베이스 추가
	runtimeOnly 'com.h2database:h2'
}
  • org.springframework.boot:spring-boot-starter-jdbc 를 추가하면 JdbcTemplate이 들어있는 spring-jdbc 가 라이브러리에 포함

  • JdbcTemplate 은 데이터소스( dataSource )가 필요
    관례상 생성자에서 의존 관계 주입 받아 JdbcTemplate 생성


사용

template.update()

  • 데이터 변경시 사용 ex) insert, update, delete
  • 영향을 받은 로우 수 반환
  • PK 생성시 identity (auto increment) 방식을 사용하는 경우,
    KeyHolder , connection.prepareStatement(sql, new String[]{"id"}) 를 사용해서 id를 지정하고 쿼리 실행 이후 생성된 ID값 조회 가능

template.queryForObject()

  • 데이터를 하나 조회
    결과 로우가 하나일 때 사용

  • RowMapper는 DB 반환 결과인 ResultSet을 객체로 변환
    결과가 없으면 EmptyResultDataAccessException,
    결과가 둘 이상이면 IncorrectResultSizeDataAccessException 발생

template.query()

  • 결과가 하나 이상일 때 사용
  • RowMapper는 DB 반환 결과인 ResultSet을 객체로 변환
    결과가 없으면 빈 컬렉션 반환

로그 확인

  • JdbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties에 코드 추가
  • main, test 설정 별개
#jdbcTemplate sql log

logging.level.org.springframework.jdbc=debug


파라미터 바인딩

NamedParameterJdbcTemplate 이름 지정 바인딩

"insert into item (item_name, price, quantity) values (:itemName, :price, :quantity)";
  • SQL에서 ? 대신 :파라미터이름 을 받음

  • Map 처럼 key , value 데이터 구조를 만들어 파라미터 전달
    key 는 :파라미터이름 으로 지정한 파라미터의 이름, value 는 해당 파라미터의 값

  • 이렇게 만든 파라미터 param 전달

template.update(sql, param, keyHolder);

이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 크게 3가지

  • Map
  • SqlParameterSource
    MapSqlParameterSource
    BeanPropertySqlParameterSource

Map

Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());

MapSqlParameterSource

  • Map과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능 제공
  • SqlParameterSource 인터페이스의 구현체
  • MapSqlParameterSource 는 메서드 체인을 통해 편리한 사용법도 제공
SqlParameterSource param = new MapSqlParameterSource()
 	.addValue("itemName", updateParam.getItemName())
 	.addValue("price", updateParam.getPrice())
 	.addValue("quantity", updateParam.getQuantity())
 	.addValue("id", itemId); //이 부분이 별도로 필요
template.update(sql, param);

BeanPropertySqlParameterSource

  • SqlParameterSource 인터페이스의 구현체
  • 자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성
    ex) getXxx() -> xxx , getItemName() -> itemName
  • 컬럼 이름과 객체의 이름이 다를 때 별칭 as 를 사용
  • 언더스코어 표기법을 카멜로 자동 변환
    ex) select item_name 👉 setItemName()
ex) getItemName() , getPrice() 가 있으면 다음과 같은 데이터를 자동으로 생성

key=itemName, value=상품명 값
key=price, value=가격 값

SimpleJdbcInsert

INSERT SQL를 직접 작성하지 않아도 되도록 기능 제공

this.jdbcInsert = new SimpleJdbcInsert(dataSource)
				.withTableName("item")
 				.usingGeneratedKeyColumns("id");
			   //.usingColumns("item_name", "price", "quantity"); //생략 가능

withTableName : 데이터를 저장할 테이블 명 지정
usingGeneratedKeyColumns : key를 생성하는 PK 컬럼 명 지정
usingColumns : INSERT SQL에 사용할 컬럼 지정 (특정 값만 저장시)


  • SimpleJdbcInsert 는 생성 시점에 데이터베이스 테이블의 메타 데이터를 조회
  • 어떤 컬럼이 있는지 확인 할 수 있으므로 usingColumns 생략 가능
  • 어떤 INSERT SQL을 만들어서 사용하는지 로그로 확인 가능
DEBUG 39424 --- [ main] o.s.jdbc.core.simple.SimpleJdbcInsert : 
Compiled insert object: insert string is [INSERT INTO item (ITEM_NAME, PRICE, 
QUANTITY) VALUES(?, ?, ?)]

save

jdbcInsert.executeAndReturnKey(param)을 사용하여 INSERT SQL을 실행, 생성된 키 값도 매우 편리하게 조회

public Item save(Item item) {
	SqlParameterSource param = new BeanPropertySqlParameterSource(item);
	Number key = jdbcInsert.executeAndReturnKey(param);
	item.setId(key.longValue());
	return item;
}

🔎 참고
스프링 JdbcTemplate 사용 방법 공식 메뉴얼
https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-JdbcTemplate

SimpleJdbcCall
https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-simple-jdbc-call-1


MyBatis

  • 기본적으로 JdbcTemplate이 제공하는 대부분의 기능을 제공
  • SQL을 XML에 작성
  • 동적 쿼리 사용 편리

공식 사이트
https://mybatis.org/mybatis-3/ko/index.html


설정

build.gradle

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
  • 스프링 부트가 관리해주는 공식 라이브러리가 아닌 것들은 뒤에 버전이 붙음

  • 추가되는 라이브러리
    mybatis-spring-boot-starter : MyBatis를 스프링 부트에서 사용
    mybatis-spring-boot-autoconfigure : MyBatis와 스프링 부트 설정
    mybatis-spring : MyBatis와 스프링 연동
    mybatis : MyBatis 라이브러리


application.properties

  • main , test 모두 수정해주어야 함
ex)

mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.repository.mybatis=trace

mybatis.type-aliases-package

  • 타입 정보에 패키지 이름 생략 가능
  • 지정한 패키지와 그 하위 패키지 자동 인식
  • 여러 위치 지정시 , ; 로 구분

mybatis.configuration.map-underscore-to-camel-case

  • 언더바를 카멜로 자동 변경해주는 기능 활성화

logging.level.hello.itemservice.repository.mybatis=trace

  • 실행되는 쿼리 로그를 확인

설정 원리


1. 애플리케이션 로딩 시점에 MyBatis 스프링 연동 모듈은 @Mapper가 붙어있는 인터페이스를 조사
2. 인터페이스가 발견되면 동적 프록시 기술을 사용해서 ItemMapper 인터페이스의 구현체 생성
3. 생성된 구현체를 스프링 빈으로 등록

매퍼 구현체

  • MyBatis 스프링 연동 모듈이 만들어주는 ItemMapper 의 구현체 덕분에 인터페이스 만으로 편리하게 XML의 데이터를 찾아서 호출
  • MyBatis에서 발생한 예외를 스프링 예외 추상화인 DataAccessException 에 맞게 변환해서 반환

사용

인터페이스

ex)

package hello.itemservice.repository.mybatis;

@Mapper
public interface ItemMapper {

	void save(Item item);
    
	void update(@Param("id") Long id, @Param("updateParam") ItemUpdateDto updateParam);
    
	Optional<Item> findById(Long id);
    
	List<Item> findAll(ItemSearchCond itemSearch);
}
  • 마이바티스 매핑 XML을 호출해주는 매퍼 인터페이스
  • @Mapper 애노테이션 사용
  • 인터페이스의 메서드를 호출하면 xml의 해당 SQL을 실행하고 결과 리턴
  • 파라미터 두개부터 @Param 사용

XML

ex)

src/main/resources/hello/itemservice/repository/mybatis/ItemMapper.xml
  • 경로와 파일 이름에 주의
  • 패키지 위치는 매퍼 인터페이스와 같아야 함

🔎 XML 파일 경로 수정

  • 원하는 위치에 설정 가능

application.properties

mybatis.mapper-locations=classpath:mapper/**/*.xml
  • resources/mapper 를 포함한 그 하위 폴더에 있는 XML을 XML 매핑 파일로 인식.
    이 경우 파일 이름은 자유롭게 설정
  • 테스트의 application.properties 파일도 함께 수정해야 테스트시 인식

ex)

<mapper namespace="hello.itemservice.repository.mybatis.ItemMapper">

    <insert id="save" useGeneratedKeys="true" keyProperty="id"></insert>

    <update id="update"></update>

    <select id="findById" resultType="Item"></select>

    <select id="findAll" resultType="Item"></select>

</mapper>

namespace : 매퍼 인터페이스 지정
id : 매퍼 인터페이스에 설정한 메서드 이름
useGeneratedKeys : 데이터베이스가 키를 생성해 주는 IDENTITY 전략일 때 사용
keyProperty : 생성되는 키의 속성 이름

  • Insert가 끝나면 item 객체의 id 속성에 생성된 값이 입력된다

#{} : 파라미터

  • 매퍼에서 넘긴 객체의 프로퍼티 이름 작성
  • PreparedStatement 를 사용
  • 파라미터 2개부터 @Param 사용
ex) 
insert into item (item_name, price, quantity) 
values (#{itemName}, #{price}, #{quantity})

ex)
update item
set item_name=#{updateParam.itemName},
price=#{updateParam.price},
quantity=#{updateParam.quantity}
where id = #{id}

resultType : 반환 타입

  • application.properties에 mybatis.type-aliases-package 속성을 지정하지 않으면 모든 패키지 명을 다 적어야 한다
  • select SQL의 결과를 객체로 바로 변환해 줌
  • 자바 코드에서 반환 객체가 하나이면 Item Optional<Item>
  • 반환 객체가 하나 이상이면 컬렉션을 사용. 주로 List 사용

동적 쿼리

ex)
  
List<Item> findAll(ItemSearchCond itemSearch);
  

<select id="findAll" resultType="Item">
    select id, item_name, price, quantity
    from item
    <where>
        <if test="itemName != null and itemName != ''">
            and item_name like concat('%', #{itemName}, '%')
        </if>
        <if test="maxPrice != null">
            and price $lt;= #{maxPrice}
        </if>
    </where>
</select>

if

  • 해당 조건이 만족하면 구문을 추가
  • 내부 문법 OGNL 사용

where

  • where문 생성
  • 예제에서 <if> 가 하나라도 성공하면 처음 나타나는 and 를 where 로 변환

trim

  • where 와 같은 기능
<trim prefix="WHERE" prefixOverrides="AND |OR ">
 ...
</trim>

choose when otherwise

  • 자바의 switch 구문과 유사
<select>
	<choose>
    	<when test=""></when>
        <when test=""></when>
      	<otherwise></otherwise>
    </choose>
</select>

foreach

  • 컬렉션을 반복 처리할 때 사용
  • 파라미터로 List 를 전달하면 된다
<select id="selectPostIn" resultType="domain.blog.Post">
 	SELECT *
 	FROM POST P
 	<where>
 		<foreach item="item" index="index" collection="list"
 		open="ID in (" separator="," close=")" nullable="true">
 			#{item}
 		</foreach>
 	</where>
</select>

특수문자

  • 특수문자 사용 불가
    아래처럼 작성
< : &lt;
> : &gt;
& : &amp;

참고 https://mybatis.org/mybatis-3/ko/dynamic-sql.html


CDATA

  • XML에서 지원하는 구문 문법
  • 특수문자 사용 가능
  • 태그가 단순 문자로 인식되어 <if> , <where> 등 적용 불가
ex)
  
<select id="findAll" resultType="Item">
	select id, item_name, price, quantity
 	from item
 	<where>
 		<if test="itemName != null and itemName != ''">
 			and item_name like concat('%',#{itemName},'%')
 		</if>
 		<if test="maxPrice != null">
 			<![CDATA[
 			and price <= #{maxPrice}
 			]]>
 		</if>
 	</where>
</select>

기타 기능

애노테이션 SQL 작성

@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
  • @Insert , @Update , @Delete , @Select 기능 제공
  • 애노테이션 사용시 XML에 태그 제거
  • 동적 SQL이 해결되지 않으므로 간단한 경우 사용

참고) https://mybatis.org/mybatis-3/ko/java-api.html


${ } 문자열 대체(String Substitution)

  • #{} 문법은 ? 를 넣고 파라미터를 바인딩하는 PreparedStatement를 사용
  • 파라미터 바인딩이 아니라 문자 그대로를 처리하고 싶은 경우 ${} 사용
  • SQL 인젝션 공격 주의 (가급적 사용 X)
ORDER BY ${columnName}


@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

재사용 가능한 SQL 조각

<sql id="userColumns"> 
	${alias}.id,${alias}.username,${alias}.password 
</sql>
<select id="selectUsers" resultType="map">
	select
	  <include refid="userColumns"><property name="alias" value="t1"/></include>,
	  <include refid="userColumns"><property name="alias" value="t2"/></include>
 	from some_table t1
 	cross join some_table t2
</select>
  • <include>를 통해 <sql> 조각을 가져다 사용 가능
  • 프로퍼티 값 전달 가능

Result Maps

<select id="selectUsers" resultType="User">
 	select
 	user_id 		as "id",
 	user_name 		as "userName",
 	hashed_password as "hashedPassword"
 	from some_table
 	where id = #{id}
</select>
  • 결과 매핑시 컬럼며오가 객체의 프로퍼티 명이 다른 경우,
    별칭 as 를 사용
    ex) 테이블은 user_id 이지만 객체는 id

별칭 대신 resultMap 을 선언하여 해결 가능

<resultMap id="userResultMap" type="User">
 	<id property="id" column="user_id" />
 	<result property="username" column="user_name"/>
 	<result property="password" column="hashed_password"/>
</resultMap>

<select id="selectUsers" resultMap="userResultMap">
 	select user_id, user_name, hashed_password
 	from some_table
 	where id = #{id}
</select>

JPA

설정

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  • JPA, 스프링 데이터 JPA를 스프링 부트와 통합

아래 라이브러리가 추가된다
hibernate-core : JPA 구현체인 하이버네이트 라이브러리
jakarta.persistence-api : JPA 인터페이스
spring-data-jpa : 스프링 데이터 JPA 라이브러리


application.properties

logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
  • main, test 모두 추가

org.hibernate.SQL=DEBUG : 하이버네이트가 생성하고 실행하는 SQL을 확인
org.hibernate.type.descriptor.sql.BasicBinder=TRACE : SQL에 바인딩 되는 파라미터 확인
spring.jpa.show-sql=true : System.out 콘솔을 통해 SQL 출력 (권장 X)


사용

매핑

@Entity

  • JPA가 사용하는 객체(엔티티)이며 애노테이션이 있어야 JPA가 인식

@Id

  • 테이블의 PK와 해당 필드를 매핑

@GeneratedValue(strategy = GenerationType.IDENTITY)

  • PK 생성 값을 데이터베이스에서 생성하는 IDENTITY 방식을 사용
    ex) auto increment

@Column(name = "item_name", length = 10)

  • 객체의 필드를 테이블의 컬럼과 매핑
  • 생략할 경우 필드의 이름을 테이블 컬럼 이름으로 사용
  • 스프링 부트와 통합해서 사용시, 카멜 케이스 -> 언더스코어로 자동 변환
  • name = "item_name" : 객체는 itemName 이지만 테이블의 컬럼은 item_name
  • length = 10 : 컬럼의 길이 값
    ex) varchar 10

@Transactional

  • JPA의 모든 데이터 변경(등록, 수정, 삭제)은 트랜잭션 안에서 이루어져야 한다
  • 일반적으로 비즈니스 로직을 시작하는 서비스 계층에 사용

기능

생성자

  • public 또는 protected 의 기본 생성자가 필수

EntityManager

  • JPA의 모든 동작은 엔티티 매니저를 통해서 이루어짐
  • 엔티티 매니저는 내부에 데이터소스를 가지고 있고, 데이터베이스에 접근 가능

💡 JPA 설정시 EntityManagerFactory, JPA 트랜잭션 매니저, 데이터소스 등 다양한 설정이 필요한데, 스프링 부트는 이 과정을 모두 자동화 한다


save

em.persist()

update

em.find 후 setXXX

  • JPA는 트랜잭션이 커밋되는 시점에, 변경된 엔티티 객체가 있는지 확인
  • 변경된 경우 update SQL 실행

JPQL

public List<Item> findAll(ItemSearchCond cond) {
	String jpql = "select i from Item i";
	//동적 쿼리 생략
	TypedQuery<Item> query = em.createQuery(jpql, Item.class);
	return query.getResultList();
}
  • JPA는 JPQL(Java Persistence Query Language)이라는 객체지향 쿼리 언어 제공
  • 여러 데이터를 복잡한 조건으로 조회할 때 사용
  • 엔티티 객체를 대상으로 SQL 실행
    따라서 from 다음 Item 엔티티 객체 이름 입력
  • JPQL을 실행하면 그 안에 포함된 엔티티 객체의 매핑 정보를 활용해서 SQL을 생성

파라미터 입력

where price <= :maxPrice

파라미터 바인딩

query.setParameter("maxPrice", maxPrice)

동적 쿼리 문제는 Querydsl 기술 활용



예외 변환

  • EntityManager 는 순수한 JPA 기술
  • 따라서 예외가 발생하면 JPA 관련 예외를 발생
  • JPA는 PersistenceException 과 그 하위 예외 발생
    • IllegalStateException , IllegalArgumentException

@Repository의 기능

  • 컴포넌트 스캔의 대상
  • 예외 변환 AOP의 적용 대상
  • 스프링과 JPA를 함께 사용하는 경우 스프링은 JPA 예외 변환기 (PersistenceExceptionTranslator) 를 등록
  • 예외 변환 AOP 프록시는 JPA 관련 예외가 발생시 JPA 예외 변환기를 통해 발생한 예외를 스프링 데이터 접근 예외로 변환

변환 후


Spring Data JPA

설정

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

공통 기능(commons)과 JPA 특화된 기능 추가
(JPA, 하이버네이트, 스프링 데이터 JPA, 스프링 JDBC 관련 기능 모두 포함)

주요 기능

  • 공통 인터페이스 기능
    • JpaRepository 인터페이스를 통해서 기본적인 CRUD 기능 제공
    • 공통화 가능한 기능이 거의 모두 포함
  • 쿼리 메서드 기능

적용

공통 인터페이스 설정

JavaConfig 설정

@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
  • 스프링 부트 사용시 생략 가능
    (스프링 부트가 @SpringBootApplication 위치를 지정)
  • 위치가 달라지면 @EnableJpaRepositories 필요

공통 인터페이스 적용

public interface ItemRepository extends JpaRepository<Member, Long> {
}
  • JpaRepository 인터페이스를 인터페이스 상속 받고, 제네릭에 관리할 <엔티티 타입, 식별자 타입> 를 주면 된다

  • @Repository 생략 가능
    기존 기능을 자동으로 처리 (컴포넌트 스캔, 예외 변환)

  • 인터페이스만 상속받으면 스프링 데이터 JPA가 프록시 기술을 사용해서 구현 클래스를 생성, 구현 클래스의 인스턴스를 만들어 스프링 빈으로 등록

  • 따라서 구현 클래스 없이 인터페이스만 만들면 기본 CRUD 기능을 사용 가능


사용

쿼리 메서드

메서드 이름을 분석해서 쿼리를 자동으로 만들고 실행해주는 기능

ex)

public interface MemberRepository extends JpaRepository<Member, Long> {

	List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
    
}
  • 메서드 이름을 분석해서 필요한 JPQL을 만들고 실행.
    JPQL은 JPA가 SQL로 번역하여 실행
  • 복잡한 조건에는 맞지 않음

규칙

조회

  • find…By , read…By , query…By , get…By
    ex) findHelloBy 처럼 ... 에 식별하기 위한 내용(설명)이 들어가도 된다.

COUNT

  • count…By 반환타입 long

EXISTS

  • exists…By 반환타입 boolean

삭제

  • delete…By , remove…By 반환타입 long

DISTINCT

  • findDistinct , findMemberDistinctBy

LIMIT

  • findFirst3 , findFirst , findTop , findTop3

쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limit-query-result


JPQL 직접 사용

  • 쿼리 메서드 기능 대신에 직접 JPQL을 사용하려면 @Query 와 함께 JPQL을 작성
  • 이때 @Param 명시 필수
@Query("select i from Item i where i.itemName like :itemName and i.price <= :price")
List<Item> findItems(@Param("itemName") String itemName, @Param("price") Integer price);

예외 변환

  • 스프링 예외 추상화를 지원
  • 스프링 데이터 JPA가 만들어주는 프록시에서 이미 예외 변환을 처리하기 때문에, @Repository 와 관계없이 예외가 변환

Querydsl

설정

build.gradle

dependencies {

	implementation 'com.querydsl:querydsl-jpa'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

//Querydsl 추가, 자동 생성된 Q클래스 gradle clean으로 제거
clean {
	delete file('src/main/generated')
}

검증

Q 타입 생성 확인

Preferences - Build, Execution, Deployment - Build Tools - Gradle
옵션별 확인 방법

  • Gradle
  • IntelliJ IDEA

1. Gradle

IntelliJ 사용법

  1. Gradle -> Tasks -> build -> clean
  2. Gradle -> Tasks -> other -> compileJava

콘솔 사용법
./gradlew clean compileJava

  • build -> generated -> sources -> annotationProcessor -> java/main 하위에
    hello.itemservice.domain.QItem 이 생성되어 있어야 함
  • Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋음
  • gradle 옵션 선택시 gradle build 폴더 아래에 생성되는데, 대부분 이 폴더를 git에 포함하지 않기 때문에 해결

삭제
gradle clean 을 수행하면 build 폴더 자체가 삭제


2. IntelliJ IDEA

Build -> Build Project
Build -> Rebuild
main() , 또는 test 실행

아무 것이나 실행하면 됨

  • src/main/generated 하위에 hello.itemservice.domain.QItem 이 생성되어 있어야 함
  • Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋음
    src/main/generated 폴더를 포함하지 않도록 설정

삭제

build.gradle

clean {
	delete file('src/main/generated')
}

gradle 에 해당 스크립트를 추가하면 gradle clean 명령어를 실행할 때 src/main/generated의 파일도 함께 삭제


🔎 IntelliJ가 버전업 하거나 Querydsl의 Gradle 설정이 버전업 하면서 적용 방법이 조금씩 달라지기도 한다. querydsl gradle로 검색


사용

JPAQueryFactory queryFactory = new JPAQueryFactory(em);
  • Querydsl을 사용하려면 JPAQueryFactory 필요
    JPAQueryFactory는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager 필요

  • JPAQueryFactory 를 스프링 빈으로 등록하여 사용 가능

ex)

public List<Item> findAllOld(ItemSearchCond cond) {

    String itemName = cond.getItemName();
    Integer maxPrice = cond.getMaxPrice();

    QItem item = QItem.item;
    BooleanBuilder builder = new BooleanBuilder();
    if (StringUtils.hasText(itemName)) {
        builder.and(item.itemName.like("%" + itemName + "%"));
    }
    if (maxPrice != null) {
        builder.and(item.price.loe(maxPrice));
    }

    List<Item> result = query
            .select(item) //QItem.item 에서 QItem -> static import
            .from(item)
            .where(builder)
            .fetch();

    return result;
}
  • BooleanBuilder 를 사용해서 원하는 where 조건들을 넣음
ex 2) findAllOld 리팩토링

public List<Item> findAll(ItemSearchCond cond) {

    String itemName = cond.getItemName();
    Integer maxPrice = cond.getMaxPrice();

    return query
            .select(item) 
            .from(item)
            .where(likeItemName(itemName), maxPrice(maxPrice))
            .fetch();
}

private BooleanExpression likeItemName(String itemName) {
    if (StringUtils.hasText(itemName)) {
        return item.itemName.like("%" + itemName + "%");
    }
    return null;
}

private BooleanExpression maxPrice(Integer maxPrice) {
    if (maxPrice != null) {
        return item.price.loe(maxPrice);
    }
    return null;
}
  • where(A,B)에 다양한 조건들을 직접 넣을 수 있는데, 이렇게 넣으면 AND 조건으로
    처리
  • where() 에 null 을 입력하면 해당 조건은 무시
  • likeItemName, maxPrice 처럼 메서드를 만들어 재사용 가능

예외 변환

  • 별도의 스프링 예외 추상화를 지원하지 않음
  • @Repository에서 스프링 예외 추상화를 처리

💡 추상화 vs 단순화 ?

단순하면서 빨리 해결할 수 있는 것..
이후 프로젝트가 커지고 여러 요소가 추가되어 추상화 비용을 넘어설 만큼의 효과가 있을 때 추상화를 도입하는 것이 실용적

상황에 따라 판단하기 !


기술 조합

  • JPA, 스프링 데이터 JPA, Querydsl을 기본으로 사용,
    복잡한 쿼리의 경우 JdbcTemplate 또는 MyBatis 함께 사용 권장

트랜잭션 매니저

  • JpaTransactionManager는 DataSourceTransactionManager 가 제공하는 기능도 대부분 제공
  • JpaTransactionManager만 등록하면 JPA, JdbcTemplate, MyBatis 모두를 하나의 트랜잭션으로 묶어서 사용 가능

💡 주의

  • JPA와 JdbcTemplate을 함께 사용시 JPA의 플러시 타이밍에 주의
  • JPA는 트랜잭션이 커밋되는 시점에 변경 사항을 데이터베이스에 반영

하나의 트랜잭션 안에서 JPA를 통해 데이터 변경 👉 JdbcTemplate 호출시,
변경한 데이터를 읽지 못하는 문제 발생

따라서 JPA 호출이 끝난 시점에 플러시 기능을 사용하여 DB에 반영해주어야 한다

0개의 댓글