본문 바로가기
DB

[MyBatis] selectKey로 다중 컬럼 값 가져오기

by soro.k 2023. 5. 30.

들어가기 전에

JPA에서는 save() 메소드를 호출하면 생성된 객체를 반환해 주는데 MyBatis는 그런 기능이 따로 없다. 그러면 어떻게 구현하면 insert문이 실행되고 저장된 객체 정보를 가져올 수 있을까 고민하다가 selectKey를 사용하게 됐다. 프로젝트에서 어떻게 적용했는지 남겨보고자 한다.

 

selectKey

insert문이 실행되기 전, 후에 원하는 컬럼 값을 가지고 올 수 있는 태그이다.

 

아래처럼 insert 태그 내부에 원하는 속성들을 지정해서 가져오고 싶은 값을 가져온다.

-- 출처 : https://mybatis.org/mybatis-3/sqlmap-xml.html
<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

 

옵션

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
  • keyProperty : 가져오려는 컬럼 명을 입력한다. 여러 개를 입력하려면 콤마를 사용하면 된다.
  • resultType : 반환되는 데이터의 타입을 지정한다.
  • order : BEFOREAFTER 중 선택한다. insert문 실행 전에 값을 가져올 것인지 후에 가져올 것인지를 정한다.
  • statementType : STATEMENT(Statement)PREPARED(PreparedStatement), CALLABLE(callableStatement) 중 선택한다. 기본값은 PreparedStatement이다.

 

프로젝트 적용

나는 네 가지 데이터로만 insert 요청을 보내고 나머지 데이터는 VO 객체에 담아서 가져오려고 했다.

 

 

1. Mapper 클래스에 리턴 타입이 void인 save() 메소드를 생성한다.

컬럼 값을 가져와야 하는데 리턴 타입은 void로 설정했다. 그러면 selectKey로 가져온 컬럼 값들은 어디에서 확인할 수 있을까? 정답은 insert 하기 위해 보낸 VO 객체이다. insert문이 실행되고 나면 전달된 CancelRequest 객체 안에 빈 값으로 존재하던 컬럼 값들이 채워질 것이다.

@Mapper
public interface CancelRequestMapper {
    void save(CancelRequest cancelRequest);
}

 

2. Mapper.xml 파일에 insert와 selectKey 내부 쿼리문을 작성한다.

    <insert id="save" parameterType="CancelRequest">
        INSERT INTO cancel_request(
                                   mentee_schedule_id,
                                   sender_id,
                                   receiver_id,
                                   reason,
                                   created_at,
                                   updated_at)
        VALUES (
                #{menteeScheduleId},
                #{senderId},
                #{receiverId},
                #{reason},
                NOW(),
                NOW()
                )
        <selectKey keyProperty="id,confirmYn,createdAt,updatedAt" resultType="CancelRequest" order="AFTER">
            SELECT id,
                   confirm_yn,
                   created_at,
                   updated_at
            FROM cancel_request
            WHERE mentee_schedule_id = #{menteeScheduleId}
        </selectKey>
    </insert>

 

이때 keyProperty의 컬럼들은 자바 클래스 변수명(카멜 케이스)으로 작성하면 되고 띄어쓰기는 하지 않는다.

<selectKey keyProperty="id,confirmYn,createdAt,updatedAt" resultType="CancelRequest" order="AFTER">
    SELECT id,
           confirm_yn,
           created_at,
           updated_at
    FROM cancel_request
    WHERE mentee_schedule_id = #{menteeScheduleId}
</selectKey>

 

 

3. 서비스 로직을 생성한다.

CancelRequest에 원하는 컬럼 값만 넣어 insert문을 실행하고 나머지 컬럼 값을 받아와 DTO 객체 생성에 활용한다.

@Transactional
public CancelRequestResponseDto saveCancelRequestByMentee(long menteeId, long menteeScheduleId, String reason) {
    ...
    
    CancelRequest cancelRequest = CancelRequest.requestOf(menteeScheduleId, menteeId, 
                                coachSchedule.getCoachId(), reason);
    cancelRequestMapper.save(cancelRequest);

    ...
    return CancelRequestResponseDto.from(cancelRequest);
}

 

 

테스트 코드

저장에 성공하고 생성된 모든 컬럼 값을 가지고 있는 VO 객체가 반환된다는 과정은 어떻게 테스트할 수 있을까? 나는 BDDMockito.doAnswer()을 활용하기로 했다.

 

 

전체 코드

@Test
@DisplayName("유효한 정보가 전달되면 요청이 완료되고 취소 요청 정보를 반환한다.")
void when_allInformationIsValid_expect_returnResponse() {
    MenteeSchedule menteeSchedule = MenteeSchedule.of(1L, 4L);
    CoachSchedule coachSchedule = CoachSchedule.of(1L, 3L, LocalDateTime.now(), false);

    BDDMockito.given(menteeScheduleService.getMenteeSchedule(anyLong(), anyLong()))
        .willReturn(menteeSchedule);
    BDDMockito.given(coachScheduleService.getCoachSchedule(anyLong(), anyBoolean()))
        .willReturn(coachSchedule);
    BDDMockito.doAnswer(invocation -> {
        CancelRequest savedCancelRequest = invocation.getArgument(0);
        BeanUtils.copyProperties(cancelRequest, savedCancelRequest);
        return null;
    }).when(cancelRequestMapper).save(BDDMockito.any(CancelRequest.class));


    CancelRequestResponseDto expectedResponse =
        cancelRequestService.saveCancelRequestByMentee(3L, 1L, "취소 사유");

    Assertions.assertNotNull(expectedResponse);
    Assertions.assertEquals(cancelRequest.getId(), expectedResponse.menteeScheduleId());
}

 

 BDDMockito.doAnswer() 활용 코드

private static CancelRequest cancelRequest;

@BeforeAll
static void setUp() {
    cancelRequest = CancelRequest.of(1L, 1L, 4L, 3L,
            "취소 사유", false, LocalDateTime.now(), LocalDateTime.now());
}

❶ 모든 값들이 담긴 객체를 생성한다.

나는 이미 사용 중인 게 있어서 위의 코드를 활용했지만, 없다면 테스트 코드 내부에 생성해서 사용하면 된다.

 

 BDDMockito.doAnswer(invocation -> {
        CancelRequest savedCancelRequest = invocation.getArgument(0);
        BeanUtils.copyProperties(cancelRequest, savedCancelRequest);
        return null;
    }).when(cancelRequestMapper).save(BDDMockito.any(CancelRequest.class));

❷ 요청을 보냈던 필수 정보만 담긴 객체는 invocations.getArgument(0)로 가져온다.

❸ BeanUtils.copyProperties(source, target)을 사용해 객체를 복사한다.

 

 

참고

MyBats - Mapper XML Files