SQLMapper
ORM 관련 기술
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()
KeyHolder
, connection.prepareStatement(sql, new String[]{"id"})
를 사용해서 id를 지정하고 쿼리 실행 이후 생성된 ID값 조회 가능template.queryForObject()
데이터를 하나 조회
결과 로우가 하나일 때 사용
RowMapper는 DB 반환 결과인 ResultSet을 객체로 변환
결과가 없으면 EmptyResultDataAccessException,
결과가 둘 이상이면 IncorrectResultSizeDataAccessException 발생
template.query()
#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
Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
MapSqlParameterSource
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
select item_name
👉 setItemName()
ex) getItemName() , getPrice() 가 있으면 다음과 같은 데이터를 자동으로 생성
key=itemName, value=상품명 값
key=price, value=가격 값
INSERT SQL를 직접 작성하지 않아도 되도록 기능 제공
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("item")
.usingGeneratedKeyColumns("id");
//.usingColumns("item_name", "price", "quantity"); //생략 가능
withTableName
: 데이터를 저장할 테이블 명 지정
usingGeneratedKeyColumns
: key를 생성하는 PK 컬럼 명 지정
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(?, ?, ?)]
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
공식 사이트
https://mybatis.org/mybatis-3/ko/index.html
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 라이브러리
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. 생성된 구현체를 스프링 빈으로 등록
매퍼 구현체
인터페이스
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
ex)
src/main/resources/hello/itemservice/repository/mybatis/ItemMapper.xml
application.properties
mybatis.mapper-locations=classpath:mapper/**/*.xml
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
: 생성되는 키의 속성 이름
#{}
: 파라미터
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
: 반환 타입
mybatis.type-aliases-package
속성을 지정하지 않으면 모든 패키지 명을 다 적어야 한다Item
Optional<Item>
동적 쿼리
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
where
<if>
가 하나라도 성공하면 처음 나타나는 and 를 where 로 변환trim
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
choose
when
otherwise
<select>
<choose>
<when test=""></when>
<when test=""></when>
<otherwise></otherwise>
</choose>
</select>
foreach
<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>
특수문자
< : <
> : >
& : &
참고 https://mybatis.org/mybatis-3/ko/dynamic-sql.html
CDATA
<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>
기타 기능
@Select("select id, item_name, price, quantity from item where id=#{id}")
Optional<Item> findById(Long id);
참고) https://mybatis.org/mybatis-3/ko/java-api.html
#{}
문법은 ?
를 넣고 파라미터를 바인딩하는 PreparedStatement를 사용${}
사용ORDER BY ${columnName}
@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);
<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>
조각을 가져다 사용 가능<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
를 사용별칭 대신 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>
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
아래 라이브러리가 추가된다
hibernate-core
: JPA 구현체인 하이버네이트 라이브러리
jakarta.persistence-api
: JPA 인터페이스
spring-data-jpa
: 스프링 데이터 JPA 라이브러리
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
org.hibernate.SQL=DEBUG
: 하이버네이트가 생성하고 실행하는 SQL을 확인
org.hibernate.type.descriptor.sql.BasicBinder=TRACE
: SQL에 바인딩 되는 파라미터 확인
spring.jpa.show-sql=true
: System.out 콘솔을 통해 SQL 출력 (권장 X)
매핑
기능
💡 JPA 설정시 EntityManagerFactory, JPA 트랜잭션 매니저, 데이터소스 등 다양한 설정이 필요한데, 스프링 부트는 이 과정을 모두 자동화 한다
em.persist()
em.find 후 setXXX
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();
}
where price <= :maxPrice
query.setParameter("maxPrice", maxPrice)
동적 쿼리 문제는 Querydsl 기술 활용
변환 후
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
공통 기능(commons)과 JPA 특화된 기능 추가
(JPA, 하이버네이트, 스프링 데이터 JPA, 스프링 JDBC 관련 기능 모두 포함)
공통 인터페이스 설정
JavaConfig 설정
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
공통 인터페이스 적용
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);
}
규칙
조회
COUNT
EXISTS
삭제
DISTINCT
LIMIT
쿼리 메소드 필터 조건
스프링 데이터 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 직접 사용
@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);
예외 변환
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
옵션별 확인 방법
IntelliJ 사용법
콘솔 사용법
./gradlew clean compileJava
삭제
gradle clean 을 수행하면 build 폴더 자체가 삭제
Build -> Build Project
Build -> Rebuild
main() , 또는 test 실행
아무 것이나 실행하면 됨
삭제
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;
}
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;
}
단순하면서 빨리 해결할 수 있는 것..
이후 프로젝트가 커지고 여러 요소가 추가되어 추상화 비용을 넘어설 만큼의 효과가 있을 때 추상화를 도입하는 것이 실용적
상황에 따라 판단하기 !
트랜잭션 매니저
💡 주의
하나의 트랜잭션 안에서 JPA를 통해 데이터 변경 👉 JdbcTemplate 호출시,
변경한 데이터를 읽지 못하는 문제 발생
따라서 JPA 호출이 끝난 시점에 플러시 기능을 사용하여 DB에 반영해주어야 한다