[MyBatis] <foreach> 문을 활용한 Bulk Insert

안녕하세요.

 

오늘은 MyBatis에서 여러건의 Record를 삽입하기 위해서 쓸 수 있는 <foreach> 태그에 대해 공유하고자 합니다.

 

우선 insert 쿼리문을 보면

 

INSERT INTO ROOM(type, deal_type, status) VALUES ('원룸', '월세', '광고중');

이런 형태로 native 쿼리를 작성해야 할텐데요.

 

JPA 처럼 saveAll() 기능을 제공하게 된다면 매우 편하게 삽입작업을 할 수 있겠지만 MyBatis는 쿼리문을 직접 작성하기 때문에 List의 갯수만큼 insert mapper를 호출하거나 해야했을 것입니다.(이렇게 썻다면 정말 안타까운 상황이네요ㅠㅠ)

 


예컨데 

 

v1
public class Service {

	public void addRoom(String type, String dealType, String status) {
    	mapper.insertRoom(type, dealType, status);
    }
    
}

public class Mapper {

	int insertRoom(
	    @Param("type") String type, 
        @Param("dealType") String dealType, 
        @Param("status") String status);

}

다음과 같은 로직이 있었을때, Service 클래스에서 addRooms라는 여러건의 room을 추가하는 로직이 들어가게 된다면...!

 

v2
public class Service {

	public void addRoom(String type, String dealType, String status) {
    	mapper.insertRoom(type, dealType, status);
    }
    
    public void addRooms(List<RoomRequest> requests) {
    	requests.forEach(request -> {
        	mapper.insertRoom(request.getType(), request.getDealType(), request.getStatus());
        });
    }
}

public class Mapper {

	int insertRoom(
	    @Param("type") String type, 
        @Param("dealType") String dealType, 
        @Param("status") String status);

}

자바에서 이런식으로 불필요하게 mapper를 호출해야 처리가 가능하겠지만

 

v3
public class Service {

	public void addRoom(String type, String dealType, String status) {
    	mapper.insertRoom(type, dealType, status);
    }
    
    public void addRooms(List<RoomRequest> requests) {
        mapper.insertRooms(requests);
    }
}

public class Mapper {

	int insertRoom(
	    @Param("type") String type, 
        @Param("dealType") String dealType, 
        @Param("status") String status);

	int insertRooms(@Param("requests") List<RoomRequest> requests);
}

-- mapper xml 

<insert id = "insertRoom">
	INSERT INTO ROOM(type, deal_type, status) VALUES (#{type}, #{dealType}, #{status});
</insert>

<insert id = "insertRooms">
	INSERT INTO ROOM(type, deal_type, status) VALUES
	<foreach collection="requests" item="email" separator=",">
        (
            #{type},
            #{dealType},
            #{status}
        )
    </foreach>;
</insert>

이제는  insertRooms 처럼 foreach 태그로 아래와 같은 쿼리를 만들어낼 수 있다.

 

INSERT INTO ROOM(type, deal_type, status) 
VALUES ('원룸', '월세', '광고중'), ('원룸', '월세', '광고중'), ('원룸', '월세', '광고중');

List의 size() 만큼 record를 삽입할 수 있게된다.

 

주의사항

 

여기서 주의할 점은 list의 size가 처리할 수 없는 임계 사이즈를 초과하게 된다면 쿼리를 실행하는 런타임 환경에서 에러를 맞딱뜨릴 수 있으니 해당 size를 고정하는 설정을 처리하거나

 

프로젝트별 임계 사이즈를 정하여 Bulk Insert 쿼리를 작성할 때 해당 에러가 발생할 수 있음을 인지하고 서비스 로직에 다음과 같은 처리를 하면 좋다.

 

public class Service {

	public void addRoom(String type, String dealType, String status) {
    	mapper.insertRoom(type, dealType, status);
    }
    
    // TODO 이부분..!
    public void addRooms(List<RoomRequest> requests) {
    	int skip = 0;
        int limit = 500;
        while (skip < requests.size()) {
        	final List<String> perRequests = requests
                            .stream()
                            .skip(skip)
                            .limit(countPerLoop)
                            .collect(toList());
                            
            skip += limit;
            mapper.insertRooms(perRequests); // 500건씩 처리.
        }
    }
}

 

이 부분은 넘어오는 List의 사이즈가 가늠하기 어려운 로직이라면 추가되어야하겠지만 화면단 혹은 bean validation 단계에서 max가 일정 갯수로 고정되어있는 경우라면 굳이 사용할 필요는 없다.

 

해당 케이스는 주로 특정 조건에 해당하는 N개의 레코드셋을 가져와서 어떤 Bulk 처리를 할때 활용될 수 있다. 

댓글

Designed by JB FACTORY