[센서 모니터링 시스템] 7. Repository 개발
Intro
이제 Entity를 저장하고 불러오는 Repository를 만들어야 한다.
프로젝트에 Spring Data JPA를 적용시켰기 때문에 간단하게 만들 수 있다.
Spring Data JPA Repository 생성
ClientRepository.java
package com.example.sensormonitoringserver.repository;
import com.example.sensormonitoringserver.entity.Client;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ClientRepository extends JpaRepository<Client, Long> {
}
클라이언트는 기본 제공되는 함수들로 충분하지만, 센서는 검색 조건에 따라 동적으로 쿼리를 날려야 하기 때문에 QueryDSL을 사용한 조회 함수가 별도로 필요하다.
우선 기본 Repository를 생성하고, 동적 조회 함수가 포함될 Interface를 상속받도록 한다.
SensorRepository.java
package com.example.sensormonitoringserver.repository;
import com.example.sensormonitoringserver.entity.Sensor;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SensorRepository extends JpaRepository<Sensor, Long>, SensorRepositoryCustom {
}
동적 조회를 위한 Custom Repository 생성
SensorRepository에서 상속받았던 조회 함수가 포함된 Interface를 생성한다.
이 함수를 호출할 땐 SensorSearch라는 검색 정보가 포함된 DTO를 통해 호출할 것이다.
SensorRepositoryCustom.java
package com.example.sensormonitoringserver.repository;
import com.example.sensormonitoringserver.dto.SensorSearch;
import com.example.sensormonitoringserver.entity.Sensor;
import java.util.List;
public interface SensorRepositoryCustom {
List<Sensor> search(Long clientId, SensorSearch sensorSearch);
}
그 다음 검색 정보가 포함된 SensorSearch DTO를 만들어준다.
센서 데이터가 업로드 된 시간에 따라 조회하도록 할것이다.
SensorSearch.java
나중에 Controller에서 Parameter로 받아와야 하기 때문에 @DateTimeFormat 어노테이션을 추가했다.
만약 없으면 Request 시 Format 에러가 난다.
package com.example.sensormonitoringserver.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
public class SensorSearch {
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime from;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime to;
}
이제 QueryDSL을 사용하여 동적 쿼리를 구현하면 되는데,
하기 전에 JPAQueryFactory를 Bean으로 등록할 것이다.
JPAQueryFactory는 EntityManager로 생성할 수 있어 EntityManager를 Autowired 시킨 후 new로 새로 생성하면 되지만, 별로 안이쁘기 때문에 따로 Bean으로 등록할 것이다.
SpringConfig.java
package com.example.sensormonitoringserver;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
public class SpringConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
이제 동적 조회 함수를 구현하는 구현체를 작성하면 끝난다.
SensorRepositoryImpl.java
package com.example.sensormonitoringserver.repository;
import com.example.sensormonitoringserver.dto.SensorSearch;
import com.example.sensormonitoringserver.entity.Sensor;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import static com.example.sensormonitoringserver.entity.QClient.*;
import static com.example.sensormonitoringserver.entity.QSensor.*;
@Repository
@RequiredArgsConstructor
public class SensorRepositoryImpl implements SensorRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<Sensor> search(Long clientId, SensorSearch sensorSearch) {
return queryFactory
.selectFrom(sensor)
.leftJoin(sensor.client, client)
.where(
clientIdEq(clientId),
dateAfter(sensorSearch.getFrom()),
dateBefore(sensorSearch.getTo())
)
.fetch();
}
private BooleanExpression clientIdEq(Long id) {
return id != null ? sensor.client.id.eq(id) : null;
}
private BooleanExpression dateBefore(LocalDateTime dateTime) {
return dateTime != null ? sensor.createdDate.before(dateTime) : null;
}
private BooleanExpression dateAfter(LocalDateTime dateTime) {
return dateTime != null ? sensor.createdDate.after(dateTime) : null;
}
}
Repository 테스트
여태 만든 Repository가 잘 동작하는지 테스트 코드를 통해 확인해보자.
SensorRepositoryTest.java
package com.example.sensormonitoringserver.repository;
import com.example.sensormonitoringserver.dto.SensorSearch;
import com.example.sensormonitoringserver.entity.Client;
import com.example.sensormonitoringserver.entity.Coord;
import com.example.sensormonitoringserver.entity.Sensor;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
class SensorRepositoryTest {
@PersistenceContext EntityManager em;
@Autowired SensorRepository sensorRepository;
@Test
public void save() {
//given
Client client = new Client("client1");
em.persist(client);
Sensor sensor1 = new Sensor(client, 1, 1, 3, new Coord());
Sensor sensor2 = new Sensor(client, 1, 2, 3, new Coord());
Sensor sensor3 = new Sensor(client, 1, 3, 3, new Coord());
sensorRepository.save(sensor1);
sensorRepository.save(sensor2);
sensorRepository.save(sensor3);
//when
List<Sensor> result = sensorRepository.findAll();
//then
assertThat(result).extracting("eco2").containsExactly(1, 2, 3);
}
@Test
public void search() throws Exception {
//given
Client client = new Client("client1");
em.persist(client);
LocalDateTime from = LocalDateTime.now();
Thread.sleep(5);
Sensor sensor1 = new Sensor(client, 1, 1, 3, new Coord());
Sensor sensor2 = new Sensor(client, 1, 2, 3, new Coord());
Sensor sensor3 = new Sensor(client, 1, 3, 3, new Coord());
sensorRepository.save(sensor1);
sensorRepository.save(sensor2);
Thread.sleep(5);
LocalDateTime to = LocalDateTime.now();
Thread.sleep(5);
sensorRepository.save(sensor3);
//when
List<Sensor> result = sensorRepository.search(client.getId(), new SensorSearch(from, to));
//then
assertThat(result).extracting("eco2").containsExactly(1, 2);
}
}
더 다양한 테스트케이스들을 생성해서 테스트하면 좋겠지만,
지금은 핵심 기능 개발할 시간도 부족하기에 아쉽지만 이대로 넘어가도록 하겠다.
다음엔 Service, Controller를 개발할 차례이다.