개발블로그

Spring data mongodb nested array 추가/수정/삭제 본문

Spring

Spring data mongodb nested array 추가/수정/삭제

개발자수니 2019. 6. 4. 18:13

그간 RDMBS만 사용해왔어서 mongodb쿼리를 짜는데 어려움이 있었다. 

특히 nested array에 object를 추가/수정/삭제할 때, 내가 원하는 것을 찾기 위해 많은 검색을 했었다. 

다음에는 검색 시간을 단축시키기 위해 정리한다. 


0. Collection 구성

TopicCategory라는 Collection은 다음과 같이 구성되어 있다. 

이 글에서 집중적으로 볼 것은 nested array로 구성된 topics 필드이다. 


1. 추가

특정한 categoryName을 가지는 document의 topics 배열에 하나를 추가하고 싶었다. 

위와 같이 topics 내에 [4]가 추가되길 바랬다. 

 

이를 위해서 다음과 같은 쿼리를 작성하면 된다. (kotlin으로 작성했으나 java와 거의 유사하여 알아보기 쉬울 것이다.)

fun addTopic(categoryName: String, topic: TopicCategory.Topic): Mono<TopicCategory> {
    return mongoTemplate.find(Query(Criteria().andOperator(
            Criteria.where("categoryName").isEqualTo(categoryName),
            Criteria.where("topics").elemMatch(Criteria.where("tid").isEqualTo(topic.tid)))), TopicCategory::class.java)
            .map {
                throw IllegalArgumentException("이미 등록된 tid입니다.")
                it
            }
            .switchIfEmpty(
                    mongoTemplate.findAndModify(Query(Criteria.where("categoryName").isEqualTo(categoryName))
                            , Update().addToSet("topics", topic)
                            , FindAndModifyOptions.options().upsert(true)
                            , TopicCategory::class.java)

            ).toMono()
}

 

switchIfEmpty 내에 있는 쿼리만 보면 된다. 그 외 코드는 validation을 위한 것이다. 

1) categoryName이 input값과 동일한 document를 먼저 찾고,

2) topics 배열에 input받은 topic을 추가한다. 이 때 addToSet을 이용한다는 것이 핵심이다.  

3) FindAndModifyOptions은 옵셔널이다. upsert-true를 주게 되면, 완전히 동일한 topic이 이미 있을 때, 추가적하지 않는다. 

    여기서 완전히 동일하다는 것은 Topic의 요소 값들이 완전히 똑같아야 한다. 하나라도 다르면 추가된다. 

 


2. 수정

특정한 categoryName을 가지는 document의 topics 배열 요소 중 하나를 수정하기 위해 다음과 같이 코드를 작성했다. 

fun modifyTopic(categoryName: String, topic: TopicCategory.Topic) : Mono<TopicCategory> {
    return mongoTemplate.find(Query(Criteria().andOperator(
            Criteria.where("categoryName").isEqualTo(categoryName),
            Criteria.where("topics").elemMatch(Criteria.where("tid").isEqualTo(topic.tid)))), TopicCategory::class.java)
            .switchMap {
                mongoTemplate.findAndModify(
                    Query(Criteria().andOperator(
                    Criteria.where("categoryName").isEqualTo(categoryName),
                    Criteria.where("topics").elemMatch(Criteria.where("tid").isEqualTo(topic.tid))))
                        ,Update().set("topics.$",topic)
                        ,TopicCategory::class.java)

            }
            .switchIfEmpty(
                    Mono.error(IllegalArgumentException("존재하지 않는 tid입니다."))
            ).toMono()
}

 

여기서도 switchMap 내의 쿼리만 보면 된다. 그 외는 validation을 위한 코드이다. 

1) 여기서는 Query 내에 Criteria를 두개 설정했다.

   하나는 원하는 categoryName을 찾기 위한 기준점.

   또 하나는 topics 배열 내에 있는 tid를 입력받은 tid로 찾기 위한 기준점. 이 때에는 elemMatch를 사용한다. 

2) topics에 Query를 통해 검색한 위치에 입력받은 topic을 대체시켜야 한다. 그리고 "검색을 통해 찾은 위치"를 $(달러)로 표시한다. 

 


3. 삭제

특정한 categoryName을 가지는 document의 topics 배열 요소 중 하나를 삭제하기 위해 다음과 같이 코드를 작성했다. 

fun removeTopic(categoryName: String, tid: String) : Mono<Boolean> {
    return mongoTemplate.find(Query(Criteria().andOperator(
            Criteria.where("categoryName").isEqualTo(categoryName),
            Criteria.where("topics").elemMatch(Criteria.where("tid").isEqualTo(tid)))), TopicCategory::class.java)
            .switchMap {
                mongoTemplate.updateMulti(
                        Query(Criteria.where("categoryName").isEqualTo(categoryName))
                        , Update().pull("topics", Query.query(Criteria.where("tid").isEqualTo(tid)))
                        , TopicCategory::class.java)
                        .map {
                            it.modifiedCount == 1L
                        }

            }
            .switchIfEmpty(
                    Mono.error(IllegalArgumentException("존재하지 않는 tid입니다."))
            ).toMono()
}

여기서도 switchMap 내의 쿼리만 보면 된다. 그 외는 validation을 위한 코드이다. 

여기서는 updateMulti 메소드를 이용했다. findAndModify는 document를 반환해주는 반면, updateMulti는 몇개의 document가 변경되었는지 정도 알려준다.

1) 내가 원하는 categoryName을 가진 다큐먼트를 검색하기 위해 Query를 작성한다.

2) 삭제를 할 때에는 pull을 이용하는 것이 핵심이다.

    이 때, key와 value값이 필요한데,  key에는 배열 필드를 입력하고 value에는 검색을 해서 주입해줘야 한다. 

    그래서 또 한번 Query를 사용하게 된다. 이미 categoryName으로 다큐먼트를 골랐고, key값으로 필드를 알려줬으니까

    그 안에서 검색하고자 하는 필드를 기준삼으면 된다. 

 


이 코드는 아직 미완의 코드이다. 

  • validation 처리 시에, 모두 IllegalArgumentException을 던지게 했는데 Exception을 더 구체화 해 줄 필요가 있다. 
  • Repository와 Service 로직이 혼재되어 있다. 그래서 validation 코드가 노출된 것이다. 이를 분리하는 작업이 필요하다. 

Comments