Spring Data MongoDB - Criteria already contains 문제
MongoRepository 에서 바로 아래와 같은 인터페이스를 만들고 사용하면, 아래와 같은 예외가 발생합니다.
public interface TodoRepository extends MongoRepository<Todo, String> {
List<Todo> findAllByCreatedAtGreaterThanEqualAndCreatedAtLessThanEqual(LocalDateTime startAt, LocalDateTime endAt);
}
startAt <= createdAt <= endAt
인 부분만 가져오고 싶은 쿼리였는데요.
findByCreatedAtBetween
을 사용하면, startAt < createdAt < endAt
으로 exclusive 한 쿼리가 만들어지므로, 위와 같은 방법을 택했었습니다.
Due to limitations of the org.bson.Document, you can't add a second 'createdAt' expression specified as 'createdAt : Document{{$lte=2023-03-10T18:33}}'; Criteria already contains 'createdAt : Document{{$gte=2023-01-01T00:00}}'
org.springframework.data.mongodb.InvalidMongoDbApiUsageException: ...
에러 메시지를 읽어보면, 문제는 간단합니다.
첫번째 createdAt 조건을 가진 Criteria
가 존재하기 때문에, 두 번째 createdAt 표현문은 추가할 수 없다는 것입니다.
Spring Data MongoDB 가 단순 쿼리메소드로는 위 문법을 지원하지 않아 보입니다.
4가지 해결방법을 찾았는데, 상황에 맞는 쿼리를 사용하면 됩니다.
1. 쿼리메소드 사용 (with Range)
public interface TodoRepository extends MongoRepository<Todo, String> {
List<Todo> findAllByCreatedAtBetween(Range<LocalDateTime> createdAt);
}
Spring Data MongoDB 공식문서 에서 lower / upper bounds 를 키워드로 검색하면, 해법이 제시되어 있습니다.
위처럼 쿼리메소드를 정의한 뒤, 아래 처럼 사용하면 됩니다.
@Test
void findAllByCreatedAtBetween() {
List<Todo> todos = todoRepository.findAllByCreatedAtBetween(Range.closed(LocalDateTime.of(2023, 1, 1, 0, 0, 0), LocalDateTime.of(2023, 3, 10, 18, 33, 0)));
assertThat(todos).isNotEmpty();
}
테스트코드를 실행하면, 테스트가 완료되고 아래와 같은 쿼리가 실행된 것을 볼 수 있습니다.
{ "createdAt" : { "$gte" : { "$date" : "2022-12-31T15:00:00Z"}, "$lte" : { "$date" : "2023-03-10T09:33:00Z"}}}
참고로 MongoDB 로깅을 콘솔에서 보려면 application.yml
에 아래 설정을 추가해주시면 됩니다.
logging:
level:
org:
springframework:
data:
mongodb:
core:
MongoTemplate: DEBUG
2. Query 사용 or Aggregation 사용
나머지 세가지 방법은 비교가 용이하도록 한 번에 보여드리겠습니다.
...
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.query.Criteria;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.match;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
@Repository
@RequiredArgsConstructor
public class TodoCustomRepository {
private final MongoTemplate mongoTemplate;
public List<Todo> findAllByCreatedAtBetween(LocalDateTime startAt, LocalDateTime endAt) {
return mongoTemplate.find(query(where("createdAt").gte(startAt).lte(endAt)), Todo.class);
}
public List<Todo> findAll2ByCreatedAtBetween(LocalDateTime startAt, LocalDateTime endAt) {
return mongoTemplate.find(query(
new Criteria().andOperator(
where("createdAt").gte(startAt),
where("createdAt").lte(endAt)
)
), Todo.class);
}
public List<Todo> findAll3ByCreatedAtBetween(LocalDateTime startAt, LocalDateTime endAt) {
Aggregation aggregation = newAggregation(
match(new Criteria().andOperator(
where("createdAt").gte(startAt),
where("createdAt").lte(endAt))
)
);
return mongoTemplate.aggregate(aggregation, "todo", Todo.class).getMappedResults();
}
}
3개의 메소드 모두 정상 작동 됩니다.
2-1 Query, Criteria 조합
findAllByCreatedAtBetween
은 Query 와 Criteria 를 조합해서 만든 몽고 쿼리입니다.
public List<Todo> findAllByCreatedAtBetween(LocalDateTime startAt, LocalDateTime endAt) {
return mongoTemplate.find(query(where("createdAt").gte(startAt).lte(endAt)), Todo.class);
}
주의해야 할 점은 where("createdAt").gte(startAt).and("createdAt").lte(endAt)
처럼 프로퍼티를 중복으로 and 에 넣으면 작동되지 않는다는 것입니다.
맨 위 오류와 연관된 org.springframework.data.mongodb.core.query.Criteria
의 코드인데요. 두 개 이상의 같은 키를 가진 Criteria 가 있다면 예외를 발생시키고 있는 부분입니다.
2-2 Query, Criteria 조합 (with andOperator)
andOperator 를 사용하면, 두 개 이상의 같은키를 사용해도 예외가 발생하지 않습니다.
public List<Todo> findAll2ByCreatedAtBetween(LocalDateTime startAt, LocalDateTime endAt) {
return mongoTemplate.find(query(
new Criteria().andOperator(
where("createdAt").gte(startAt),
where("createdAt").lte(endAt)
)
), Todo.class);
}
2-3 aggregation, andOperator 사용
public List<Todo> findAll3ByCreatedAtBetween(LocalDateTime startAt, LocalDateTime endAt) {
Aggregation aggregation = newAggregation(
match(new Criteria().andOperator(
where("createdAt").gte(startAt),
where("createdAt").lte(endAt))
)
);
return mongoTemplate.aggregate(aggregation, "todo", Todo.class).getMappedResults();
}
aggregation, match, andOperator 를 잘 조합하면, 원하는 쿼리를 만들 수 있습니다.
마지막으로 세 코드의 테스트 결과와 쿼리 실행 결과를 비교해보겠습니다.
@Test
void findAllByCreatedAtBetween() {
List<Todo> result = todoCustomRepository.findAllByCreatedAtBetween(LocalDateTime.of(2023, 1, 1, 0, 0, 0), LocalDateTime.of(2023, 3, 10, 18, 33, 0));
assertThat(result).hasSize(1);
}
@Test
void findAll2ByCreatedAtBetween() {
List<Todo> result = todoCustomRepository.findAll2ByCreatedAtBetween(LocalDateTime.of(2023, 1, 1, 0, 0, 0), LocalDateTime.of(2023, 3, 10, 18, 33, 0));
assertThat(result).hasSize(1);
}
@Test
void findAll3ByCreatedAtBetween() {
List<Todo> result = todoCustomRepository.findAll3ByCreatedAtBetween(LocalDateTime.of(2023, 1, 1, 0, 0, 0), LocalDateTime.of(2023, 3, 10, 18, 33, 0));
assertThat(result).hasSize(1);
}
테스트는 모두 성공적으로 완료되고, 쿼리 실행 결과는 순서대로 아래와 같습니다.
# findAllByCreatedAtBetween
find using query: { "createdAt" : { "$gte" : { "$date" : "2022-12-31T15:00:00Z"}, "$lte" : { "$date" : "2023-03-10T09:33:00Z"}}} fields: Document{{}} for class: class co.whitetree.javatodo.domain.todo.Todo in collection: todo
# findAll2ByCreatedAtBetween
find using query: { "$and" : [{ "createdAt" : { "$gte" : { "$date" : "2022-12-31T15:00:00Z"}}}, { "createdAt" : { "$lte" : { "$date" : "2023-03-10T09:33:00Z"}}}]} fields: Document{{}} for class: class co.whitetree.javatodo.domain.todo.Todo in collection: todo
# findAll3ByCreatedAtBetween
Executing aggregation: [{ "$match" : { "$and" : [{ "createdAt" : { "$gte" : { "$date" : "2022-12-31T15:00:00Z"}}}, { "createdAt" : { "$lte" : { "$date" : "2023-03-10T09:33:00Z"}}}]}}] in collection todo
상황에 맞는 쿼리를 잘 선택하셔서 사용하시길 바랍니다.
감사합니다.
댓글