본문 바로가기
mybatis

mybatis - ResultMap Collection을 통해 데이터 한번에 가져오기

by khds 2023. 6. 18.

 

프로젝트를 진행하던 중 member의 정보와 member가 작성한 article의 리스트, member가 속해 있는 group의 리스트를 가져와야 하는 기능을 구현해야 했다. member와 article와 group는 각각 테이블이고  member와 article는 일대다 관계, member와 group는 다대다 관계이며 중간에 member_group이라는 연결 테이블이 존재한다. 

member 정보 따로, article 리스트 따로, group 리스트 따로 구현하면 3번의 db접근이 일어난다. 그리고 설계를 잘못하면 N+1문제가 발생할 수 있다.  나는 이를 1번의 DB접근으로 표현하고 싶다. JPA에서는 fetch join 혹은 eager 한 방식으로 데이터를 가져오거나 배치사이즈를 조정하여 문제를 해결할 수 있다. 배치사이즈에 대한 내용은 https://khdscor.tistory.com/22를 참고하길 바란다. 

하지만 mybatis에서는 어떻게 할까?

애초에 JPA를 다루다가 mybatis를 공부하면서 가장 크게 생각한 차이는 eager하게 연관관계에 있는 데이터들을 가져올 수 있느냐이다. mybatis에는 fetch join이라는 기능도 없기 때문이다.

 

하지만 이와 비슷하게 구현해주는 기능이 mybatis에도 있었다. 바로 ResultMap에 Collection이다. 

ResultMap은 복잡한 결과 매핑을 간단하게 만들어주는 태그이다.

 

restulMap에 대한 것은 아래의 코드를 봐보자.

<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
    <arg column="username" javaType="String"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <association property="author" javaType="Author"/>
    <collection property="comments" ofType="Comment">
      <id property="id" column="comment_id"/>
    </collection>
    <collection property="tags" ofType="Tag" >
      <id property="id" column="tag_id"/>
    </collection>
    <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultMap="carResult"/>
    <case value="2" resultMap="truckResult"/>
    <case value="3" resultMap="vanResult"/>
    <case value="4" resultMap="suvResult"/>
  </discriminator>
  </collection>
</resultMap>

 

resultMap

 id는 resultMap을 구분하는 id를 뜻하고 type는 리턴할 객체(domain 혹은 dto)의 타입을 뜻한다.

constructor

객체의 인스턴스의 생성자를 에 값을 입력하는 것이다.

idArg는 생성자에 집어넣는 해당 데이터를 구분을 위한 값이다.

arg는 집어넣을 데이터이다.

result

호출 시 내보낼 데이터이다.

association

“has-one”를 가지는 일대일 연관관계의 다른 객체로 호출할 때 사용한다. 

collection

“has-many"를 가지는 일대다 연관관계의 다른 객체의 리스트로 호출할 때 사용한다. 

discriminator

자바의 Switch case 문처럼 동작한다.

자세한 내용은 https://mybatis.org/mybatis-3/ko/sqlmap-xml.html#Parameters를 참고하길 바란다.

 

 

여기서 Collection 태그가 이 글의 핵심이다. 

dto로 리턴하도록 설정 후 dto내에 리스트가 있으면 그 리스트의 값들을 Collection태그 내에 값으로 채우면 된다. 

그 값들은 SELECT 쿼리를 작성할 때 join을 통해 얻은 테이블의 컬럼들을 별칭으로서 표기하여 구분할 수 있다. 그래서 ResultMap 태그 내에 Collection 태그도 포함하여 구현하고 select쿼리에 대입하여 적절히 컬럼들을 넣으면 된다.

이렇게 하여 우리는 1번의 DB접근으로 필요한 정보뿐만 아니라 연관관계에 있는 테이블의 정보들의 리스트들 또한 얻을 수 있다.

그런데 여기서 겪는 문제가 하나있다. 

만약 리스트에 해당하는 값들이 하나도 없다면 어떻게 되는가? 

위의 member와 article만 보면 member이 article을 하나도 가지지 않을 수 도 있다. 

이런 상황에서 member와 member가 작성한 article리스트를 리턴하는 쿼리를 호출하면 article리스트에는 알 수 없는 값이 넣어진다. 프로젝트 진행 중에는 article리스트와 group리스트가 모두 비어있을 때 article리스트에는 null이 들어갔고 group리스트에는 member의 id와 imageUrl 값이 group의 id와 name에 자동으로 채워지며 각각 1개씩 생겨났다. 즉, 내용이 없어도 1개의 객체는 계속 리스트에 들어가는 것이다. 아래는 응답된 dto이다.

 

그렇다면 이러한 문제는 어떻게 해결할까? 바로 Collection의 속성 중 notNullColumn 속성을 추가해주기만 하면 된다. 

이 속성은 속성으로 지정된 id 값이 존재하지 않으면 리스트에 객체가 생성되지 않도록하는 것이다. 아래는 속성을 추가했을 때 응답 dto이다.

 

아래는 프로젝트에서 작성한 메서드 코드이다. DB는 mysql이다. 

<select id="findMyPageDetails" resultMap="myPageDetails">
    SELECT m.id as memberId, m.image_url as imageUrl, m.nick_name as nickName,
    m.email as email, m.join_date as visitDate,
    a.id as articleId, a.title as title, a.create_date as createDate, count(distinct al.id) as totalLikes,
    count(distinct c.id) as totalComments, g.id as groupId, g.name as name
    FROM member m LEFT JOIN article a ON m.id = a.member_id LEFT JOIN comment c ON a.id = c.article_id
    LEFT JOIN article_like al ON a.id = al.article_id LEFT JOIN member_group mg ON m.id = mg.member_id
    LEFT JOIN group_table g ON mg.group_id = g.id WHERE m.id = #{memberId} GROUP BY a.id
  </select>
  <resultMap id="myPageDetails" type="MyPageResponse">
    <id property="memberId" column="memberId"/>
    <result property="imageUrl" column="imageUrl"/>
    <result property="nickName" column="nickName"/>
    <result property="email" column="email"/>
    <result property="visitDate" column="visitDate"/>
    <collection property="myArticles" notNullColumn="articleId"
      ofType="foot.footprint.domain.member.dto.MyArticleResponse">
      <id property="articleId" column="articleId"/>
      <result property="title" column="title"/>
      <result property="createDate" column="createDate"/>
      <result property="totalLikes" column="totalLikes"/>
      <result property="totalComments" column="totalComments"/>
    </collection>
    <collection property="myGroups" notNullColumn="groupId"
      ofType="foot.footprint.domain.group.dto.GroupSummaryResponse">
      <id property="groupId" column="groupId"/>
      <result property="name" column="name"/>
    </collection>
  </resultMap>

 

 

 

참고

https://velog.io/@godkimchichi/MyBatis-resultMap-%EC%97%90%EC%84%9C-collection-null-%EC%B2%98%EB%A6%AC

 

[MyBatis] resultMap 에서 collection null 처리

210531 예외는 아닌데 속이 터진다

velog.io

 

https://blog.naver.com/PostView.nhn?blogId=joonbread&logNo=222343940865 

 

MyBatis(마이바티스) - constructor

MyBatis(마이바티스) - constructor - 마이바티스에선 DB에서 사용했던 쿼리문을 대부분 사용할 수 있...

blog.naver.com

 

https://mybatis.org/mybatis-3/ko/sqlmap-xml.html#Parameters

 

MyBatis – 마이바티스 3 | 매퍼 XML 파일

Mapper XML 파일 마이바티스의 가장 큰 장점은 매핑구문이다. 이건 간혹 마법을 부리는 것처럼 보일 수 있다. SQL Map XML 파일은 상대적으로 간단하다. 더군다나 동일한 기능의 JDBC 코드와 비교하면

mybatis.org

 

https://velog.io/@hanblueblue/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B82-3.-Join%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%ED%95%9C-%EB%B2%88%EC%97%90-%EC%97%B0%EA%B4%80%EA%B0%9D%EC%B2%B4-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0

 

[프로젝트2] 3. Join을 이용하여 한 번에 연관객체 가져오기

jpaRepository를 mapper로 변경한다. myBatis가 제공하는 resultMap을 이용하여 필요한 데이터를 한번에 가져온다.

velog.io