1cm

자바 프로그래밍_Day_96_Framework : MyBatis / 동적 쿼리 본문

국비지원_Java/Java Programming_2

자바 프로그래밍_Day_96_Framework : MyBatis / 동적 쿼리

dev_1cm 2022. 1. 6. 04:24
반응형

 

2022. 01. 03

새로운 프로젝트를 위한 조가 짜여졌다. 신기하게도 제일 처음 조에서 뵀던 분들 다수가 파이널 프로젝트에서도 같이 진행하게 됐다.ㅎㅎ 세미 프로젝트 보완은 파이널 하면서 틈틈이 하는 것이 목표! 파이널 프로젝트 또한 절대 길게 느껴지진 않겠지만..화이팅이다. 뭐 아무튼 1월 3,4,5일 아주 폭풍같은 시간들이라서 조금 늦은 TIL이 되었는데 이해가 될 때까지 계속 보다보니 시간이 너무 오래걸린다. TIL 글을 올리는 것도 100% 이해한 것도 아니고 한 40% 이해한 부분들만 올리는 듯 싶다. 속상하긴 하지만... TIL 더이상 미룰 수 없어... 아무리 메모에 적어놨지만서도 다시 보니까 새로 보는 것 같은 느낌이어서 강의를 그냥 다시 보면서 타이핑 하는게 속 편한듯.^.^....

 

 

 

 

/mybatis/src/main/java/com/kh/mybatis/member/model/vo/Member.java
	public Member(String id, String password, String name) {
		this.id = id;
		this.password = password;
		this.name = name;
	}

멤버 vo 부분에 id, pwd, name : pk을 받을 수 있도록 설정해준다.

 

 

ValueSource는 파라미터 하나를 넘겨주는 경우 사용

 

 

파라미터 여러 개를 받아오는 경우 CsvSource를 사용해준다. (테스트 할 데이터를 미리 넘겨주는 방법)

Csv는 데이터를 표현하는 방법 중 하나인데 하나의 문자열 안에 콤마로 값을 구분해주는 것을 말한다.

CsvSource를 사용하려면 기존 @Test를 붙여줬던 걸 @ParameterizedTest로 수정해줘야 테스트가 가능하다.

각각 test1 -> id, 1234 -> password, 홍길동 -> name으로 데이터가 대입이 되어 테스트가 진행된다.

그리고 그 다음 줄 테스트("test2, 5678, 동동이")가 수행이 됨

 

insert member 테스트

> MemberServiceTest.java

	public void saveTest(String id, String password, String name) {
		int result = 0;
		Member findMember = null;
		Member member = new Member(id, password, name);
		
		result = service.save(member);
		// 실제로 DB에 Member가 저장되었는지 확인하기 위해서 다시 MEMBER를 조회 (Id로 조회)
		findMember = service.findMemberById(id);
		
		assertThat(result).isGreaterThan(0);
		// 다시 찾지 않고도 insert 후 NO값 받아오기
		assertThat(member.getNo()).isGreaterThan(0);
		// 실제로 데이터 베이스에 저장되어있는지 확인
		assertThat(findMember).isNotNull().extracting("name").isEqualTo(name);
	}

회원 등록 테스트 진행

 

 

 

 

그 다음 update member 테스트

테이블에 이미 존재하는 데이터를 수정해주는 작업을 하는데,

1. id값으로 멤버 조회

2. 그 멤버를 수정(업데이트)

 

 

> MemberServiceTest.java

	@DisplayName("회원 정보 수정 테스트")
	@Order(5)
	public void updateMemberTest(String id, String password, String name) {
		int result = 0;
		Member member = null;
		Member findMember = null;
		
		// 멤버 조회 (id로)
		member = service.findMemberById(id);
		
		// 멤버의 데이터 수정
		member.setPassword(password);
		member.setName(name);
		
		// 업데이트 (save의 리턴 값은 정수형 데이터인데 여기서 정수값이 의미하는 값
		//							-> 쿼리문 수행 후 DB에 영향받은 행의 갯수)
		service.save(member);
		
		// 실제로 DB에 Member가 수정되었는지 확인하기 위해서 다시 MEMBER를 조회 (Id로 조회)
		findMember = service.findMemberById(id);
		
		// 행의 갯수 결과 값이 0보다 클 것이다.라는 검증 테스트
		assertThat(result).isGreaterThan(0);
		// 업데이트 값이 맞게 변경되었는지 확인
//		assertThat(findMember).isNotNull().extracting("name").isEqualTo("staff1");
//		assertThat(findMember).isNotNull().extracting("password").isEqualTo("5678");
		
		assertThat(findMember.getName()).isNotNull().isEqualTo(name);
		assertThat(findMember.getPassword()).isNotNull().isEqualTo(password);
	}

- 1개의 정보를 받아서 업데이트를 하기 때문에 isGreaterThan(0)으로 0보다 클 것이라는 가정을 검증하기 위한 테스트 진행한다.

- 테스트 코드 검증 방법은 여러 가지가 있다. (추가 공부 필요)

 

 

 > MemberService.java

	public int save(Member member) {
		int result = 0;
		SqlSession session = getSession();
		
		if(member.getNo() != 0) {
			// update
			result = dao.updateMember(session, member);
		} else {
			// insert
			result = dao.insertMember(session, member);
		}
		
		if(result > 0) {
			session.commit();
		} else {
			session.rollback();
		}
		
		session.close();
		
		return result;
	}

 

 

 

> MemberDao.java

	public int updateMember(SqlSession session, Member member) {
		
		return session.update("memberMapper.updateMember", member);
	}

업데이트 하는 쿼리문(memberMapper.updateMember), member

 

 

 

 > member.mapper.xml

	<!-- 회원정보 업데이트 -->
	<update id="updateMember" parameterType="Member">
		UPDATE MEMBER
		SET
			NAME = #{name},
			PASSWORD = #{password},
			PHONE = #{phone},
			EMAIL = #{email},
			ADDRESS = #{address},
			HOBBY = #{hobby},
			MODIFY_DATE = SYSDATE
		WHERE NO = #{no}
	</update>

업데이트 하는 쿼리문 작성

값은 parameter로 넘겨주는 member 객체에서 가지고 옴 -> parameterType에는 필드명으로 가지고 오면 된다.

그리고 나서 테스트 진행

 

 

 

 

- 멤버 삭제 테스트

STATUS를 바꾸는 것이 아닌 행을 지우는 것으로 실습 진행 (no값을 넘겨서 지움)

 

> MemberServiceTest.java

	@ParameterizedTest
	@ValueSource(strings = {"test1", "test2"})
	@Test
	@DisplayName("회원 삭제 테스트")
	@Order(6)
	public void deleteTest(String id) {
		int result = 0;
		Member findMember = null;
		Member member = service.findMemberById(id);
		
		result = service.delete(member.getNo());
		
		// 실제로 DB에 Member가 삭제되었는지 확인하기 위해서 다시 MEMBER를 조회 (Id로 조회)
		findMember = service.findMemberById(id);
		
		assertThat(result).isPositive().isEqualTo(1);
		assertThat(findMember).isNull();
	}

멤버 객체 조회 후(service.findMemberById) -> delete메소드 호출하면서 조회한 객체에서 no값을 넘겨준다.

result : delete 메소드 호출 후 정수값 반환(JDBC를 통해 수행 후 영향받은 행의 갯수 리턴)

assertThat : 결과값은 positive이면서 1과 같을 것이다.

assertThat(findMember).isNull(); : 실제로 DB에서 삭제 되었다면 -> findeMember 조회 시 Null값이 뜨는지 테스트 확인을 위한 구문

 

 

 > MemberService.java

	public int delete(int no) {
		int result = 0;
		SqlSession session = getSession();
		
		result = dao.delete(session, no);
		
		if(result > 0) {
			session.commit();
		} else {
			session.rollback();
		}
		
		session.close();
		
		return result;
	}

Delete 로직 구현

dao.delete : dao에게 삭제 요청

commit/rollback 작업을 if구문으로 작성해줌

 

 

> MemberDao.java

	public int delete(SqlSession session, int no) {

		return session.delete("memberMapper.deleteMember", no);
	}

그리고 MemberDao에도 삭제 구문 추가

session객체에서 delete를 수정할 것이다. -> ("쿼리문이 실제 존재하는 경로.id",no) 전달

 

 

 

쿼리문 생성

 > member-mapper.xml

	<!-- 회원삭제 -->
	<delete id="deleteMember" parameterType="_int">
		DELETE FROM MEMBER WHERE NO = #{no}
	
	</delete>

 

 

테스트 진행 후 등록->업데이트->삭제 순으로 진행이 될 수 있도록 class MemberServiceTest에 @TestMethodOrder(OrderAnnotation.class) 매개값으로 클래스를 받을 수 있게 어노테이션을 추가시켜준 뒤,

각각의 클래스마다 @Order() 숫자를 넣어줌으로써 순서를 정해준다.

 

 

Member관련 전체 코드

 > MemberDao.java

package com.kh.mybatis.member.model.dao;

import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.session.SqlSession;

import com.kh.mybatis.member.model.vo.Member;

public class MemberDao {

	public int getMemberCount(SqlSession session) {
		
		/*
		 * SqlSession 객체가 제공하는 메소드를 통해서 SQL을 실행 시킨다.
		 * 객체 한 개를 조회하기 위해서 SqlSession 객체의 selectOne() 메소드를 사용한다.
		 * 	- 첫 번째 매개 값은 쿼리문이 존재하는 경로이다.("Mapper네임스페이스.쿼리문아이디")
		 * 	- 두 번째 매개 값은 쿼리문에서 사용될 파라미터 객체이다.
		 *  * 두 개 이상 전달해주고 싶을 때 : 배열, List, Map에 여러 개의 값을 담아서 준다. (대부분 Map으로 많이 넘김)
		 */
		
//		selectOne : 객체 하나만 가져오겠다라는 뜻
		return session.selectOne("memberMapper.selectCount");
	}

	public List<Member> findAll(SqlSession session) {
//		session의 selectList 메소드를 통해 ("")안의 값들을 객체로 변환해서 List형태로 반환받는다.
//		조회되는 데이터가 없을 경우 빈 리스트 객체를 만들어서 넘겨주기 때문에 테스트에서는 무조건 통과한다.
		
		return session.selectList("memberMapper.selectAll");
	}

	public Member findMemberById(SqlSession session, String id) {
		
//		만약 조회되는 값이 없으면 null 리턴해준다.
		return session.selectOne("memberMapper.selectMemberById", id);
	}

	public int insertMember(SqlSession session, Member member) {
		
		return session.insert("memberMapper.insertMember", member);
	}

	public int updateMember(SqlSession session, Member member) {
		
		return session.update("memberMapper.updateMember", member);
	}

	public int delete(SqlSession session, int no) {

		return session.delete("memberMapper.deleteMember", no);
	}

}

 

 

 

> MemberService.java

package com.kh.mybatis.member.model.service;

import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.session.SqlSession;

import static com.kh.mybatis.common.SqlSessionTemplate.getSession;
import com.kh.mybatis.member.model.dao.MemberDao;
import com.kh.mybatis.member.model.vo.Member;

public class MemberService {
	private MemberDao dao = new MemberDao();
	
	// 데이터베이스에서 숫자를 조회 후 리턴해주는 역할
	public int getMemberCount() {
		int count = 0;
		
		SqlSession session = getSession();
		
		count = dao.getMemberCount(session);
		
		session.close();
		
		return 2;
	}

	public List<Member> findAll() {
		List<Member> members = null;
		// SqlSession : 실제 DB와 연결할 수 있고, 실행시켜야 할 쿼리문도 알고 있음
		SqlSession session = getSession();
		
		members = dao.findAll(session);
		
		// 데이터가 잘 넘어오는지 확인
//		System.out.println(members);
		
		// session 조회 후 close 해줌
		session.close();
		
		return members;
	}

	public Member findMemberById(String id) {
		Member member = null;
		SqlSession session = getSession();
		
		member = dao.findMemberById(session, id);
		
		session.close();
		
		return member;
	}

	public int save(Member member) {
		int result = 0;
		SqlSession session = getSession();
		
		if(member.getNo() != 0) {
			// update
			result = dao.updateMember(session, member);
		} else {
			// insert
			result = dao.insertMember(session, member);
		}
		
		if(result > 0) {
			session.commit();
		} else {
			session.rollback();
		}
		
		session.close();
		
		return result;
	}
	
	public int delete(int no) {
		int result = 0;
		SqlSession session = getSession();
		
		result = dao.delete(session, no);
		
		if(result > 0) {
			session.commit();
		} else {
			session.rollback();
		}
		
		session.close();
		
		return result;
	}
}

 

 

 > vo/Member.java

package com.kh.mybatis.member.model.vo;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Member {
	private int no;
	
	private String id;
	
	private String password;
	
	private String role;
	
	private String name;
	
	private String phone;
	
	private String email;
	
	private String address;
	
	private String hobby;
	
	private String status;
	
	private Date enrollDate;
	
	private Date modifyDate;

	public Member(String id, String password, String name) {
		this.id = id;
		this.password = password;
		this.name = name;
	}
	
	
}

 

 

 

 > member-mapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!-- Mapper 설정 파일임을 선언 -->
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace 속성 : 외부에서 mapper에 접근할 수 있는 이름(별칭) -->
<mapper namespace="memberMapper">
	<!-- 중복되는 쿼리 내용을 재사용하기 위한 SQL 조각으로, 사용하려는 태그보다 항상 위에 있어야한다. -->
	<sql id="selectMemberSql">
		SELECT NO,
		       ID,
		       PASSWORD,
		       ROLE,
		       NAME,
		       PHONE,
		       EMAIL,
		       ADDRESS,
		       HOBBY,
		       STATUS,
		       ENROLL_DATE,
		       MODIFY_DATE
		FROM MEMBER
	</sql>
	<!-- 
		resultMap은 DB의 조회 결과와 자바 객체(클래스)를 명시적으로 매핑해줄 때 사용한다. (JDBC 코드를 줄여주는 역할을 한다.)
			- type 속성 : ResultSet의 결과를 담을 자바 객체의 타입(클래스명)을 기술한다. 풀 패키지 명으로 적어주거나 Alias 사용
			- id 속성 : resultMap의 고유 아이디로 SELECT 문에서 resultMap 속성에 사용하게 된다.
	 -->
<!-- 	<resultMap type="com.kh.mybatis.member.model.vo.Member" id="memberResultMap"> -->
<!-- 별칭 부여 후 짧아진 코드 확인 가능 -->
	<resultMap type="Member" id="memberResultMap">
		<!-- id : 기본 키 / property : 지정 필드 명 / column : 조회된 컬럼명 -->
		<!-- property : 자바 객체의 필드명을 기술, column : DB에서 select 구문에 지정된 컬럼명을 기술 -->
		<!-- 테이블에서 기본 키를 의미 -->
		<id property="no" column="NO"/>
		
		<!-- 테이블에서 일반 컬럼들을 의미 -->
		<result property="id" column="ID"/>
		<result property="password" column="PASSWORD"/>
		<result property="role" column="ROLE"/>
		<result property="name" column="NAME"/>
		<result property="phone" column="PHONE"/>
		<result property="email" column="EMAIL"/>
		<result property="address" column="ADDRESS"/>
		<result property="hobby" column="HOBBY"/>
		<result property="status" column="STATUS"/>
		<result property="enrollDate" column="ENROLL_DATE"/>
		<result property="modifyDate" column="MODIFY_DATE"/>
	</resultMap>
	
	
	<!-- 
		SELECT 구문
			id 속성 : 쿼리문의 고유 아이디
			resultType 속성 : 쿼리 실행 후 조회 된 결과값의 자료형
	 -->
	<select id="selectCount" resultType="_int">
		SELECT COUNT(*) FROM MEMBER
	</select>
	
	<!-- 
		쿼리문의 수행 결과가 담긴 ResultSet은 resultType의 객체에 자동으로 mapping되어 리턴된다.
		단, 조회 결과의 컬럼명과 자바 클래스에서 필드명이 동일해야 자동으로 mapping이 된다.
		(대소문자는 구분하지 않음 / * enrollDate 와 enroll_date는 필드명이 달라서 조회되지 않음)
		
		해결방법
		1) 쿼리문에서 "as 별칭"으로 조회되는 컬럼명을 변경한다.
			-> 여러 개의 쿼리문일 경우 번거로워 짐. -> 유지보수 문제로 비추천
		2) resultMap을 이용해서 명시적으로 컬럼명과 변수명을 mapping 시켜준다. (추천)
		
		resultMap : 조회된 결과 하나하나의 행을 명시적으로 어떤 필드에 매핑해줄지 지정해주는 것, 보통 상단에 작성해준다.
	 
	<select id="selectAll" resultType="com.kh.mybatis.member.model.vo.Member"> -> 방법 1
	-->
	<select id="selectAll" resultType="memberResultMap">
		<!-- 
		방법 1)
		SELECT NO,
		       ID,
		       PASSWORD,
		       ROLE,
		       NAME,
		       PHONE,
		       EMAIL,
		       ADDRESS,
		       HOBBY,
		       STATUS,
		       ENROLL_DATE AS enrollDate,
		       MODIFY_DATE AS modifyDate
		FROM MEMBER
		WHERE STATUS = 'Y'
		 -->
		
		<!-- 
		방법 2)
		SELECT NO,
		       ID,
		       PASSWORD,
		       ROLE,
		       NAME,
		       PHONE,
		       EMAIL,
		       ADDRESS,
		       HOBBY,
		       STATUS,
		       ENROLL_DATE,
		       MODIFY_DATE
		FROM MEMBER
		WHERE STATUS = 'Y'
		 -->
		 
		<include refid="selectMemberSql" />
		WHERE STATUS = 'Y'
	</select>
	
	<!-- 한 행을 resultMap을 참조해서 resultMap에 있는대로 Member 객체로 mapping 후 결과 리턴 -->
	<!-- parameterType을 map으로 해도 되는 이유? : 변수명을 키값으로 가져오면 Map형태로 바꿔주기 때문 -->
	<!-- 
		외부에서 매개변수로 받는 값이 있으면 해당하는 매개변수의 타입을 지정해야 한다. (parameterType="string")
		외부에서 매개변수로 받는 값은 #{변수명}으로 설정해서 값을 가져온다.
	 -->
<!-- 	<select id="selectMemberById" parameterType="string" resultMap="memberResultMap"> -->
	<select id="selectMemberById" parameterType="map" resultMap="memberResultMap">
		<include refid="selectMemberSql" />
		WHERE ID=#{id} AND STATUS = 'Y'
	</select>
	
	<!--
		데이터를 Insert 후 PK 값을 받아오기 위해서 useGeneratedKeys, keyColumn, keyProperty 속성을 추가한다.
		ex. insert 후 다른 테이블에도 INSERT 해줘야 하는 경우 pk 값이 fk로 들어가야 하기 때문에 재사용이 가능해진다.
		
			- useGeneratedKeys : insert 후에 PK 값을 받아올 수 있도록 허용하는 속성
			- keyColumn : 실제 PK에 해당하는 컬럼 지정하는 속성
			- keyProperty : PK값이 담길 파라미터 객체의 필드명을 지정하는 속성
	 -->
	<insert id="insertMember" parameterType="Member"
			useGeneratedKeys="true" keyColumn="NO" keyProperty="no">
		INSERT INTO MEMBER (
		    NO,
	        ID,
	        PASSWORD,
	        ROLE,
	        NAME,
	        PHONE,
	        EMAIL,
	        ADDRESS,
	        HOBBY,
	        STATUS,
	        ENROLL_DATE,
	        MODIFY_DATE
		) VALUES (
			SEQ_UNO.NEXTVAL,
			#{id},
			#{password},
			DEFAULT,
			#{name},
			#{phone},
			#{email},
			#{address},
			#{hobby},
			DEFAULT,
			DEFAULT,
			DEFAULT
		)
	</insert>
	
	<!-- 회원정보 업데이트 -->
	<update id="updateMember" parameterType="Member">
		UPDATE MEMBER
		SET
			NAME = #{name},
			PASSWORD = #{password},
			PHONE = #{phone},
			EMAIL = #{email},
			ADDRESS = #{address},
			HOBBY = #{hobby},
			MODIFY_DATE = SYSDATE
		WHERE NO = #{no}
	</update>
	
	<!-- 회원삭제 -->
	<delete id="deleteMember" parameterType="_int">
		DELETE FROM MEMBER WHERE NO = #{no}
	
	</delete>
</mapper>

 

 

 

 

 > MemberServiceTest.java

package com.kh.mybatis.member.model.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import com.kh.mybatis.member.model.vo.Member;

@DisplayName("Member 테스트")
@TestMethodOrder(OrderAnnotation.class)
class MemberServiceTest {
	private MemberService service;
	
	@BeforeEach
	public void setUp() {
		service = new MemberService();
	}
	
	@Test
	@Disabled
	public void nothing() {
	}
	
	@Test
	@Disabled
	public void create() {
		assertThat(service).isNotNull();
	}
	
	@Test
	@DisplayName("회원 수 조회 테스트")
	@Order(1)
	public void getMemberCountTest() {
		int count = service.getMemberCount();
		
//		assertThat(count).isGreaterThanOrEqualTo(2);
		assertThat(count).isPositive().isGreaterThanOrEqualTo(2);
	}
	
//	모든 멤버 조회하는 테스트 메소드
	@Test
	@DisplayName("모든 회원 조회 테스트")
	@Order(2)
	public void findAllTest() {
//		결과로 받아올 값은 List 형태로 가져온다.
		List<Member> members = null;
		
		members = service.findAll();
		
		// members는 null이 아니다라는 검증의 테스트
//		assertThat(members).isNotNull()
		
		assertThat(members).isNotNull()
						   .isNotEmpty()
						   .extracting("id")
						   .isNotNull()
						   .contains("mrhong", "admin2");
	}
	
//	JUnit jupiter params에서 제공하는 어노테이션
	@ParameterizedTest
	@ValueSource(strings = {"mrhong", "admin2"})
	@DisplayName("회원 조회 테스트 (ID로 검색)")
	@Order(3)
	public void findMemberByIdTest(String userId) {

		// 회원 하나 조회
		Member member = service.findMemberById(userId);
		
		assertThat(member).isNotNull()
						  .extracting("id")
						  .isEqualTo(userId);
	}
	
	
	@ParameterizedTest
	@CsvSource({
		"test1, 1234, 홍길동",
		"test2, 5678, 동동이"
	})
	@DisplayName("회원 등록 테스트")
	@Order(4)
	public void saveTest(String id, String password, String name) {
		int result = 0;
		Member findMember = null;
		Member member = new Member(id, password, name);
		
		result = service.save(member);
		// 실제로 DB에 Member가 저장되었는지 확인하기 위해서 다시 MEMBER를 조회 (Id로 조회)
		findMember = service.findMemberById(id);
		
		assertThat(result).isGreaterThan(0);
		// 다시 찾지 않고도 insert 후 NO값 받아오기
		assertThat(member.getNo()).isGreaterThan(0);
		// 실제로 데이터 베이스에 저장되어있는지 확인
		assertThat(findMember).isNotNull().extracting("name").isEqualTo(name);
	}
	
	// 멤버 정보를 업데이트하는 서비스
	@ParameterizedTest
	@CsvSource({
		"test1, 5678, staff1",
		"test2, 0000, staff2"
	})
	@DisplayName("회원 정보 수정 테스트")
	@Order(5)
	public void updateMemberTest(String id, String password, String name) {
		int result = 0;
		Member member = null;
		Member findMember = null;
		
		// 멤버 조회 (id로)
		member = service.findMemberById(id);
		
		// 멤버의 데이터 수정
		member.setPassword(password);
		member.setName(name);
		
		// 업데이트 (save의 리턴 값은 정수형 데이터인데 여기서 정수값이 의미하는 값
		//							-> 쿼리문 수행 후 DB에 영향받은 행의 갯수)
		service.save(member);
		
		// 실제로 DB에 Member가 수정되었는지 확인하기 위해서 다시 MEMBER를 조회 (Id로 조회)
		findMember = service.findMemberById(id);
		
		// 행의 갯수 결과 값이 0보다 클 것이다.라는 검증 테스트
		assertThat(result).isGreaterThan(0);
		// 업데이트 값이 맞게 변경되었는지 확인
//		assertThat(findMember).isNotNull().extracting("name").isEqualTo("staff1");
//		assertThat(findMember).isNotNull().extracting("password").isEqualTo("5678");
		
		assertThat(findMember.getName()).isNotNull().isEqualTo(name);
		assertThat(findMember.getPassword()).isNotNull().isEqualTo(password);
	}
	
	@ParameterizedTest
	@ValueSource(strings = {"test1", "test2"})
	@Test
	@DisplayName("회원 삭제 테스트")
	@Order(6)
	public void deleteTest(String id) {
		int result = 0;
		Member findMember = null;
		Member member = service.findMemberById(id);
		
		result = service.delete(member.getNo());
		
		// 실제로 DB에 Member가 삭제되었는지 확인하기 위해서 다시 MEMBER를 조회 (Id로 조회)
		findMember = service.findMemberById(id);
		
		assertThat(result).isPositive().isEqualTo(1);
		assertThat(findMember).isNull();
	}
}

 

 

 

 

 

MyBatis 동적 쿼리(SQL) 란?

일반적으로 검색 기능이나 다중 입력 처리 등을 수행해야 할 경우 SQL을 실행하는 DAO를 여러번 호출하거나 batch 기능을 이용하여 버퍼에 담아서 한번에 실행시키는 방식으로 쿼리를 구현했다면,

MyBatis에서는 이를 동적으로 제어할 수 있는 구문을 제공하여 좀 더 쉽게 쿼리를 구현할 수 있도록 하는 기능을 지원한다.

 

 

 

지원 구문 종류

1. if

2. choose(when, otherwise)

3. trim(where, set)

4. foreach

 

 

MyBatis 동적 SQL - if 구문과 예시

if 구문

 - 동적 쿼리를 구현할 때 가장 기본적으로 사용되는 구문 

 - 특정 조건을 만족할 경우 안의 구문을 쿼리에 포함시킴

 

if 구문 예시

<select id="searchBoard" resultType="arraylist">
	SELECT *
      FROM BOARD
     WHERE WRITER = 'admin'
     <if test="title != null">
     	AND TITLE LIKE = #{title}
     </if>
</select>

 - 넘어오는 파라미터 중에 title이 null이 아니면 <if></if>안에 있는 쿼리문을 WHERE WRITER = 'admin' 뒤에 추가시켜주고, 만족하지 않으면 WHERE WRITER = 'admin'까지만 쿼리문이 만들어져서 실행된다.

- 여러 케이스들을 하나의 SQL문으로 처리할 수 있다.

 

 

 

MyBatis 동적 SQL - 다중 if 구문과 예시

다중 if 구문

 - 필요로 하는 조건이 1개 이상일 시 if 구문을 여러 개 사용 가능

 

 

다중 if 구문 예시

<select id="searchBoard" resultType="arraylist">
	SELECT *
      FROM BOARD
     WHERE WRITER = 'admin'
     <if test="title != null">
     	AND TITLE LIKE = #{title}
     </if>
     <if test="location != null">
     	AND LOCATION LIKE #{location}
     </if>
</select>

- 두 쿼리 다 만족하면 둘 다 수행하고, 둘 중 하나만 만족한다면 만족하는 쿼리문만 수행한다.

 

 

 

MyBatis 동적 SQL - choose, when, otherwise 구문

choose, when, otherwise 구문

 - 자바의 if-else, switch, JSTL의 choose 구문과 유하사며 주어진 구문 중 한 가지만을 수행하고자 할 때

 

<select id="searchBoard" resultType="arraylist">
	SELECT *
      FROM BOARD
     WHERE COMMENT != ''
     <choose>
     	<when test="조건식">
        	...
        </when>
        <otherwise>
        	...
        </otherwise>
     </choose>
</select>

- 이런 구조라고 보면 된다. 

- choose 구문안에 있는 when절 혹은 otherwise절 둘 중 한 가지만 선택해서 수행된다.

 

 

choose, when, otherwise 구문 예시

<select id="searchBoard" resultType="arraylist">
	SELECT *
      FROM BOARD
     WHERE COMMENT != ''
     <choose>
     	<when test="title != null">
        	AND TITLE LIKE #{title}
        </when>
        <when test="writer != null">
        	AND WRITER LIKE #{writer}
        </when>
        <otherwise>
        	AND FEATURED = 1
        </otherwise>
     </choose>
</select>

- choose 구문 안에 있는 한 개의 절만 선택되어서 수행된다.(위에서 아래로 검증)

 

 

 

동적 SQL의 안 좋은 예시
<select id="searchBoard" resultType="arraylist">
	SELECT *
      FROM BOARD
     WHERE
     <if test="title != null">
        	AND TITLE LIKE #{title}
     </if>
     <if test="writer != null">
        	AND WRITER LIKE #{writer}
     </if>
</select>

 

결과 1 : if 조건 어느 것도 만족하지 못했을 경우 -> WHERE 까지만 나오는 것으로 된다.

결과 2 : 두 번째 if 조건만 만족할 경우 -> WHERE AND WRITER가 되므로 잘못 된 쿼리문이 된다.

 

 

 

MyBatis 동적 SQL - trim, where, set 구문

- <trim> : 쿼리 구문의 특정 부분을 없앨 때 쓰인다.

- <where> : 기존 쿼리의 WHERE 절을 동적으로 구현할 때 쓰인다.

- <set> : 기존의 UPDATE SET 절을 동적으로 구현할 때 쓰인다.

 

 

 

MyBatis 동적 SQL - where 구문과 예시

where 구문

 - 단순히 WHERE 만을 추가하지만, 만약 태그 안의 내용이 'AND'나 'OR'로 시작할 경우 'AND'나 'OR을 제거한다.

 

 

where 구문 예시

<select id="searchBoard" resultType="arraylist">
	SELECT *
      FROM BOARD
    <where>
      <if test="title != null">
      	TITLE LIKE #{title}
      </if>
      <if test="writer != null">
      	AND WRITER LIKE #{writer}
      </if>
    </where>
</select>

- 별도로 WHERE 대신 <where> 태그가 들어가 있다.

- 만약 첫 번째 if 만 만족하는 경우 : WHERE TITLE ~~ 이런식으로 쿼리문이 만들어 지고, 두 번째 if만 만족하는 경우 첫 번째 if문은 넘어가고 두 번째 if문에서 앞부분에 있는 AND 생략 후 WRITER ~~ 쿼리문으로 완성됨

- 두 쿼리문 다 만족하지 않는 경우 : SELECT * FROM BOARD로 끝남

 

 

 

MyBatis 동적 SQL - set 구문과 예시

set 구문

 - UPDATE 하고자 하는 컬럼을 동적으로 포함시키기 위해 사용한다.

 - SET 키워드를 붙이고 불필요한 콤마를 제거한다.

 

 

set 구문 예시

<select id="updateMember">
	UPDATE MEMBER
    <set>
      <if test="userName != null">
      	USER_NAME = #{userName},
      </if>
      <if test="userPwd != null">
      	USER_PWD = #{userPwd},
      </if>
    </set>
</select>

- SET 이하 만족하는 쿼리문들이 붙는다. -> ex. UPDATE MEMBER SET USER_NAME = #{...} (콤마 자동 제거됨)

- WHERE절이 뒤에 없기 때문에 전체가 변경된다고 보면 됨

 

MyBatis 동적 SQL - trim 구문을 통해 where 구현

- 태그 안의 내용이 완성될 때 처음 시작할 단어(prefix)와 시작 시 제거해야 할 단어(prefixOverrides)를 명시하여 where와 같은 내용으로 구현할 수 있다.

 

 

trim/where 구문 예시

<select id="searchBoard" resultType="arraylist">
	SELECT *
      FROM BOARD
    <trim prefix="WHERE" prefixOverrides="AND/OR">
      <if test="title != null">
      	TITLE LIKE #{title}
      </if>
      <if test="writer != null">
      	AND WRITER LIKE #{writer}
      </if>
    </trim>
</select>

- prefix : trim 구문에 있는 쿼리문이 만들어질때 처음 시작 될 단어

- prefixOverrides : AND나 OR로 시작하면 AND/OR을 제거해준 뒤 쿼리문 작성될 수 있도록 해준다.

 

 

 

MyBatis 동적 SQL - trim 구문을 통해 set 구현

- WHERE와 흡사하나 suffixOverrides 속성을 ',(콤마)'로 설정하여 구문의 마지막에 제거할 값을 명시해줌

 

 

trim/set 구문 예시

<select id="updateMember">
	UPDATE MEMBER
    <trim prefix="set" suffixOverrides=",">
      <if test="userName != null">
      	USER_NAME = #{userName},
      </if>
      <if test="userPwd != null">
      	USER_PWD = #{userPwd},
      </if>
    </trim>
</select>

 

suffixOverrides : 마지막에 제거될 값

 

 

 

MyBatis 동적 SQL - foreach 구문과 예시

 

foreach 구문

 - 동적 쿼리를 구현할 때 collection에 대한 반복 처리를 제공한다.

 

 

foreach 구문 예시

<select id="searchBoardwords" resultType="arraylist">
	SELECT *
      FROM BOARD
     WHERE TITLE IN
    <foreach item="item" index="index" collection="list" open="(" separator="," close=")">
    	#{item}
    </foreach>
</select>

- collection : 컬렉션 - 반복되면서 요소들이 하나씩 item이라는 변수에 들어가게 된다.

- index : 몇 번째 반복인지 index 정보

- 결과 SQL : SELECT * FROM BOARD WHERE TITLE IN ('xxx', '000', '111', ...)

- open : 쿼리문이 시작할 때 사용

- separator : 쿼리문의 요소들을 구분해줄 때 사용

- close : 쿼리문이 끝날 때 사용

 

 

 

foreach 태그의 속성
속성명 설명
item 반복될 때 접근 가능한 각 객체 변수
index 반복되는 횟수를 가리키는 변수, 0부터 순차적으로 증가
collection 전달받은 인자로 반복에 쓰이는 Collection 객체, List나 Array 형태만 가능
open 해당 구문이 시작될 때 삽입할 문자열
separator 반복되는 객체를 나열할 때 사용할 구분자
close 해당 구문이 종료될 때 삽입할 문자열

 

 

MyBatis 동적 SQL - bind 구문과 예시

 

bind 구문

 - 특정 문장을 미리 생성하여 쿼리에 적용해야 할 경우 사용

 - '_parameter'를 통해 전달받은 값에 접근하여 구문 생성

 

 

bind 구문 예시

<select id="searchBoard" resultType="arraylist">
	<bind name="pattern" value="%' + _parameter.getTitle() + '%" />
	SELECT *
      FROM BOARD
     WHERE TITLE LIKE #{pattern}
</select>

- like pattern 구문에 '%title%' 처럼 적용된다고 보면 됨

 

 

 

com/kh/mybatis/board/model/service/BoardService.java 생성 후 

package com.kh.mybatis.board.model.service;

public class BoardService {
}

 

 

JUnit test case 용으로 BoardServiceTest.java 생성 -> 테스트 진행 후 @Disabled 처리해줌

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import com.kh.mybatis.board.model.vo.Board;

class BoardServiceTest {
	private BoardService service;
	
	@BeforeEach
	public void setUp() throws Exception {
		// @BeforeEach : 아래 테스트 케이스들 작성 시 한번씩 수행
		// 그럴때마다 새로운 service 객체를 생성 후 넘겨주기
		service = new BoardService();
	}

	@Test
	@Disabled
	public void nothing() {
	}
	
	@Test
	@Disabled
	public void create() {
		// 정상적으로 setUp()에서 객체가 만들어져서 대입이 됐는지 확인 후 @Disabled
		assertThat(service).isNotNull();
	}

 

setUp() method : 아래 있는 테스트 케이스 작성 시 한번씩 수행 후 새로운 객체 만들어서 넘겨줄 수 있게 한다.

-> service = new BoardService();

 

 

 

 

 

전체 목록 조회하는 service 만들기

 > BoardServiceTest.java

	@Test
	// Board 객체를 요소로 담고있는 list를 반환해주는 함수
	public void findAllTest() {
		List<Board> list = null;
        
        list = service.findAll();
        
        assertThat(list).isNotNull();
    }

 

 

<Board>에서 발생하는 에러를 없애주기 위해 Create class 'Board'를 클릭,

경로는 mybatis/src/main/java 그리고 model.vo 밑에 만들어 준다.

 

 > Board.java

package com.kh.mybatis.board.model.vo;

public class Board {

}

 

그 다음 findAll에서 발생하는 오류 해결은 method 생성으로 해결

 > BoardService.java

package com.kh.mybatis.board.model.service;

import java.util.List;

import com.kh.mybatis.board.model.vo.Board;

public class BoardService {

	public List<Board> findAll(){
    
   	return null;
	}
}

 

그리고 다시 테스트 시 실패하게 되는데 null을 리턴해주기 때문이다.

null을 리턴해주지 않도록 코드 수정

 

 > BoardService.java

package com.kh.mybatis.board.model.service;

import java.util.List;

import com.kh.mybatis.board.model.vo.Board;

public class BoardService {

	public List<Board> findAll(){
    
   	return new ArrayList<>();
	}
}

 

 

 > Board.java

package com.kh.mybatis.board.model.vo;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Board {
	private int no;
	
	private int rowNum;
	
	private int writerNo;
	
	private String writerId;
	
	private String title;
	
	private String content;
	
	private String originalFileName;
	
	private String renamedFileName;
	
	private int readCount;
	
	private String status;
	
//	private List<Reply> replies;
	
	private Date createDate;
	
	private Date modifyDate;
	
}

 

 

그 다음 BoardService.java

public class BoardService {
	// Dao 객체에게 데이터 조회해달라고 요청
	private BoardDao dao = new BoardDao();
	
	public List<Board> findAll(String writer, String title, String content) {
		// Board객체를 가지는 list를 만들겠다- 선언
		List<Board> list = null;
		SqlSession session = SqlSessionTemplate.getSession();
		
		// dao에게서 리스트를 받을 것임
		list = dao.findAll(session, writer, title, content);
		
		session.close();
		
		return list;
	}

}

 

 

 

Board.Dao 만들어주고 에러처리

 

테스트 하면 실패함 -> findAll(session)로 넣어주고, ctrl+1 ->  change SqlSession 클릭해주면 BoardDao.java에 SqlSession이 들어가는 걸 확인할 수 있음

 

member-mapper만든 것 처럼 resource에 board-mapper.xml 만들어준다.

 

 

SQLsession 객체에서 사용할 있게 config.xml 파일에 mapper 추가시켜준다.

 

mapper.xml 선언 넣어주고 boardMapper넣어준다.

 

테스트는 실패한다 -> 컬럼명과 지정필드명이 다르기 때문

—> 다음시간에 이어서

 

 

 

전체 코드

 

mybatis.config.xml 에는 board-mapper 추가만 해줌

	<!-- DB에서 사용하는 쿼리문들을 담는 mapper 파일을 등록하는 영역 -->
	<mappers>
		<mapper resource="mappers/member/member-mapper.xml"/>
		<mapper resource="mappers/board/board-mapper.xml"/>
	</mappers>

 

 

BoardService.java

package com.kh.mybatis.board.model.service;

import java.util.ArrayList;
import java.util.List;

import org.apache.ibatis.session.SqlSession;

import com.kh.mybatis.board.model.dao.BoardDao;
import com.kh.mybatis.board.model.vo.Board;
import com.kh.mybatis.common.SqlSessionTemplate;

public class BoardService {
	// Dao 객체에게 데이터 조회해달라고 요청
	private BoardDao dao = new BoardDao();
	
	public List<Board> findAll() {
		// Board객체를 가지는 list를 만들겠다- 선언
		List<Board> list = null;
		SqlSession session = SqlSessionTemplate.getSession();
		
		// dao에게서 리스트를 받을 것임
		list = dao.findAll(session);
		
		session.close();
		
		return list;
	}

}

 

 

 

Board.java

package com.kh.mybatis.board.model.vo;

import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Board {
	private int no;
	
	private int rowNum;
	
	private int writerNo;
	
	private String writerId;
	
	private String title;
	
	private String content;
	
	private String originalFileName;
	
	private String renamedFileName;
	
	private int readCount;
	
	private String status;
	
//	private List<Reply> replies;
	
	private Date createDate;
	
	private Date modifyDate;
	
}

 

 

> BoardDao.java

package com.kh.mybatis.board.model.dao;

import java.util.List;

import org.apache.ibatis.session.SqlSession;

import com.kh.mybatis.board.model.vo.Board;

public class BoardDao {

	public List<Board> findAll(SqlSession session) {
		
		return session.selectList("boardMapper.selectAll");
	}

}

 

 

> board-mapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!-- Mapper 설정 파일임을 선언 -->
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
<mapper namespace="boardMapper">
	<select id="selectAll" resultType="com.kh.mybatis.board.model.vo.Board">
		SELECT  B.NO, 
			B.TITLE, 
			M.ID, 
			B.READCOUNT, 
			B.ORIGINAL_FILENAME, 
			B.RENAMED_FILENAME, 
			B.CONTENT, 
			B.CREATE_DATE, 
			B.MODIFY_DATE
		FROM BOARD B
		JOIN MEMBER M ON(B.WRITER_NO = M.NO)
		WHERE B.STATUS = 'Y'
	</select>
</mapper>

 

 

> BoardServiceTest.java

package com.kh.mybatis.board.model.service;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import com.kh.mybatis.board.model.vo.Board;

class BoardServiceTest {
	private BoardService service;

	@BeforeEach
	public void setUp() throws Exception {
    	// @BeforeEach : 아래 테스트 케이스들 작성 시 한번씩 수행
	// 그럴때마다 새로운 service 객체를 생성 후 넘겨주기
		service = new BoardService();
	}

	@Test
	@Disabled
	public void nothing() {
	}
	
	@Test
	@Disabled
	public void create() {
    	// 정상적으로 setUp()에서 객체가 만들어져서 대입이 됐는지 확인 후 @Disabled
		assertThat(service).isNotNull();
	}
	
	@Test
    	// Board 객체를 요소로 담고있는 list를 반환해주는 함수
	public void findAllTest() {
		List<Board> list = null;
		
		list = service.findAll();
		
		System.out.println(list);
		
		assertThat(list).isNotNull();
	}
}

 

반응형
Comments