🔷 Runtime 시점에서 사용자의 입력 값에 따라 동적으로 SQL을 생성하여 실행하는 방식
🔷 if
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
🔷 choose (when, otherwise)
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
🔷 trim (where, set)
WHERE
과 SET
을 엘리먼트로 사용할 수 있다.trim
으로 사용할 때는 prefix
속성의 값으로 WHERE
나 SET
을 지정할 수 있고, prefixOverrides
나 suffixOverrides
속성으로 AND
등의 값을 지정하면 반환되는 컨텐츠가 없을 때 해당 접미사나 접두사가 쿼리문에 붙지 않는다.❗
WHERE
와SET
를 굳이 엘리먼트로 사용하는 이유는if
나choose
조건에서 아무 값도 반환되지 않았을 때WHERE
나SET
,AND
등의 키워드만 덜렁 마지막에 적혀 오류를 초래할 수 있다. 물론 이런 오류를 초래할 여지가 없다면 이를 사용하지 않고 쿼리에 적어도 작동엔 지장이 없다.
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
<!-- 위와 같다 -->
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</trim>
</select>
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
<!-- 위와 같다 -->
<update id="updateAuthorIfNecessary">
update Author
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</trim>
where id=#{id}
</update>
🔷 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>
💡 컬렉션 파라미터로 Map이나 배열객체와 더불어 List, Set등과 같은 반복가능한 객체를 전달할 수 있다. 반복가능하거나 배열을 사용할때 index값은 현재 몇번째 반복인지를 나타내고 value항목은 반복과정에서 가져오는 요소를 나타낸다. Map을 사용할때 index는 key객체가 되고 항목은 value객체가 된다.
🔷 동적 쿼리 활용의 대표적인 예로 검색 조건 변경이 있는데 이를 지난번 board 실습에 넣어보자.
SearchCondition
)🖥 SearchCondition
package com.bzeromo.board.model.dto;
public class SearchCondition {
private String key;
private String word;
private String orderBy;
private String orderByDir;
public SearchCondition() {}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
public String getOrderBy() {
return orderBy;
}
public void setOrderBy(String orderBy) {
this.orderBy = orderBy;
}
public String getOrderByDir() {
return orderByDir;
}
public void setOrderByDir(String orderByDir) {
this.orderByDir = orderByDir;
}
}
BoardService
와 BoardServiceImpl
에 SearchCondition
을 매개변수로 받아 List<Board>
타입으로 반환할 search
메서드를 추가한다.🖥 BoardService 중
//검색 버튼을 눌렀을 때 처리할 메서드
List<Board> search(SearchCondition condition);
🖥 BoardServiceImpl 중
@Override
public List<Board> search(SearchCondition condition) {
return boardDao.search(condition);
}
BoardController
에 검색 기능을 추가한다.🖥 BoardController 중
// 검색기능 추가
@GetMapping("search")
public String search(Model model, SearchCondition condition) {
model.addAttribute("list", boardService.search(condition));
return "/board/list";
}
boardMapper.xml
에서 동적 쿼리를 이용한 검색기능 쿼리문을 등록한다. 검색 조건(key)에 따라 해당 키 컬럼에 포함된 값들 중에 word가 포함된 값을 찾을 것이다. 또한 어떤 기준으로 정렬할 것이냐(키, 오름차순&내림차순)에 따라 출력값을 정렬할 것이다.🖥 boardMapper.xml 중
<!-- 검색기능 -->
<select id="search" resultType="Board" parameterType="SearchCondition">
SELECT id, content, writer, title, view_cnt as viewCnt, date_format(reg_date, '%Y-%M-%d') as regDate
FROM board
<!-- 검색 조건 (동적쿼리)-->
<if test="key != 'none'">
WHERE ${key} LIKE concat('%', #{word}, '%')
</if>
<!-- 어떤 기준으로 어떤 방향으로 정렬 -->
<if test="orderBy != 'none'">
ORDER BY ${orderBy} ${orderByDir}
</if>
</select>
💡
$
와#
의 차이
#
은 '' 내에 문자열 형식으로,$
는 parameter의 값으로 그냥 들어온다.
$
은 sql injection의 위험이 커 보안 측면에서 좋지 않다.
list.jsp
에 검색 form을 추가한다. (부트스트랩 적용)/search
를 향해 GET 방식으로 날린다.🖥 list.jsp 중
<!-- action 경로를 작성할 떄는 헷갈리지 않게 풀로 쓰는게 좋을듯 하지만 직접 작성보다는 변수를 활용 -->
<form action="search" method="GET" class="row">
<div class="col-2">
<label>검색기준</label>
<select class="form-select" name="key">
<option value="none">없음</option>
<option value="writer">작성자</option>
<option value="title">제목</option>
<option value="content">내용</option>
</select>
</div>
<div class="col-5">
<label>검색 내용</label>
<input type="text" name="word" class="form-control">
</div>
<div class="col-2">
<label>정렬기준</label>
<select class="form-select" name="orderBy">
<option value="none">없음</option>
<option value="writer">작성자</option>
<option value="title">제목</option>
<option value="view_cnt">조회수</option>
</select>
</div>
<div class="col-2">
<label>정렬방향</label>
<select class="form-select" name="orderByDir">
<option value="asc">오름차순</option>
<option value="desc">내림차순</option>
</select>
</div>
<div class="col-1">
<input type="submit" value="검색" class="btn btn-primary">
</div>
</form>
🖨 결과
제목을 기준으로 작성자명에 "맨"이 들어간 글을 출력
내용으로 "힘내세요"가 들어가있는 글을 출력
🔷 데이터 무결성을 위해서 사용한다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency
root-context.xml
에 트랜잭션 관리자와 어노테이션 기반 트랜잭션을 설정해야한다.<!-- 트랜잭션 매니저를 등록 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
@Transactional
이 선언되어 있으면, AOP를 통해 트랜잭션 처리를 한다.🖥 BoardServiceImpl의 writeBoard() 메서드
@Transactional
@Override
public void writeBoard(Board board) {
System.out.println("게시글을 작성합니다.");
//강제로 ID를 99번으로 결정하겠다.
//board.setId(99);
boardDao.insertBoard(board);
//boardDao.insertBoard(board);
}
🤔 주석 표시 제거 후 트랜잭션 처리 없이 글을 두 번 게시할 때 강제로 ID를 99번으로 만들면 어떻게 될까?
이렇게 터지는데...
하나는 걸렀지만 다른 게시가 들어오고 말았다.
그렇다면 트랜잭션 처리를 했을 때 같은 작업을 하면 어떻게 될까?
마찬가지의 오류가 발생하고....
이번엔 새로 추가되지 않았다!
이를 통해 트랜잭션 처리되어 작업이 ROLLBACK
되었음을 알 수 있다.
이번엔 Java를 이용한 방식을 다룬다.
(굳이 지금 다루는 이유는 곧 사용할 SpringBoot에서 쓰지만 Spring Legacy 버전에서는 안쓰니까...)
👍 이전에 사용했던 방식들과 코드들(Spring DI 게시글) (재활용할 것이다.)
@component
를 달지 않아도 된다.🖥 com.bzeromo.di.ApplicationConfig
package com.bzeromo.di;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class ApplicationConfig {
@Bean
// @Scope("prototype")
public Desktop desktop() {
return new Desktop();
}
@Bean
public Laptop laptop() {
return new Laptop();
}
@Bean
public Programmer programmer() {
// Programmer pr = new Programmer();
// pr.setComputer(desktop()); //설정자 주입
Programmer pr = new Programmer(desktop()); //생성자 주입
return pr;
}
}
@ComponentScan
을 통해 ApplicationConfig.java에서 Bean을 찾아 주입하기@Component
표시를 해야한다.🖥 com.bzeromo.di2.ApplicationConfig
package com.bzeromo.di2;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = {"com.bzeromo.di2"})
public class ApplicationConfig {
}
🖥 Test
AnnotationConfigApplicationContext(ApplicationConfig.class)
이다.package com.bzeromo.di2;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
Desktop d1 = context.getBean("desktop", Desktop.class);
Desktop d2 = context.getBean("desktop", Desktop.class);
System.out.println(d1 == d2);
Programmer p = (Programmer)context.getBean("programmer");
p.coding();
}
}
싱글턴(default)이기 때문에 같은 인스턴스가 반환되는 모습
스프링부트가 다가온다...