@Service
public class FruitServiceImpl implements FruitService{
private final FruitRepository repository;
public FruitServiceImpl(@Qualifier("first")FruitRepository repository) {
this.repository = repository;
}
@Override
public void saveFruit(String name, String date, Integer price) {
repository.saveFruit(name,date,price);
}
@Override
public void updateFruit(long id) {
if(repository.isFruitExist(id)){
throw new IllegalArgumentException("수정할 자료가 없습니다.");
}
repository.updateFruit(id);
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return repository.getFruitStat(name);
}
}
FruitRepository
(기존코드)
@Repository
public class FruitSQLRepository implements FruitRepository {
private final JdbcTemplate template;
public FruitSQLRepository(JdbcTemplate jdbcTemplate) {
this.template = jdbcTemplate;
}
@Override
public void saveFruit(String name, String date, Integer price) {
String sql = "insert into fruitshop (name,warehousedate,price) values (?,?,?)";
template.update(sql, name, date, price);
}
@Override
public void updateFruit(long id) {
String sql = "update fruitshop set sell = true where id = ?";
template.update(sql, id);
}
@Override
public boolean isFruitExist(long id) {
String readSql = "select id from fruitshop where id = ?";
return template.query(readSql, (rs, rowNum) -> 0, id).isEmpty();
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
String sql = "SELECT sell, SUM(price) as totalAmount FROM fruitshop WHERE name = ? GROUP BY sell";
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
template.query(sql, new Object[]{name}, (rs, rowNum) -> {
boolean status = rs.getBoolean("sell");
long totalAmount = rs.getLong("totalAmount");
if (status) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
return null;
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
}
이전에 만들었던 SQL쿼리를 이용한 Repository와 Service를
Jpa를 이용한 케이스로 수정.
Fruit (DAO) - 테이블과 매칭되는 DAO객체에 @Entity와 @ID @Column으로 매칭
@Entity
public class Fruit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
@Column(nullable = false, length = 20)
private String name;
private Integer price; //타입 필드이름 이 테이블과 일치하면 @Column은 생략가능.
private String wareHouseDate; // 테이블의 필드이름 ware_house_date
private boolean sell = false;
private static long seq = 1;
public Fruit(String name, Integer price, String wareHouseDate) {
this.name = name;
this.price = price;
this.wareHouseDate = wareHouseDate;
}
public Fruit() {
}
public String getName() {
return name;
}
public long getPrice() {
return price;
}
public String getWareHouseDate() {
return wareHouseDate;
}
public void setSell(){
this.sell = true;
}
}
객체의 이름은 테이블과 매칭되어야하고
객체의 필드이름을 카멜케이스로 쓰게되면 테이블의 필드는 스네이크케이스로작성되어있어야한다.
만약 객체의 이름이 테이블의 이름과 다르다면@Entity(name = "테이블이름")으로 사용하면된다.
Repository (Interface)
public interface FruitRepositoryJpa extends JpaRepository<Fruit, Long> {
//아래는 추상메서드 기본제공되는 메서드이외에 필요시 추가해서 사용이 가능하다.
//메서드의 이름의로 쿼리가 자동으로 작성되어 구현되고
//쿼리가 복잡하여 메서드의이름으로 구현이안되는경우 Query어노테이션을 통해서 쿼리를 사용할수있다.
@Query(value = "SELECT sell, SUM(price) as totalAmount FROM fruit WHERE name = :name GROUP BY sell", nativeQuery = true)
List<Object> findSellAndTotalAmountByName(@Param("name") String name);
Long countByName(String name);
List<Fruit> findByPriceGreaterThan(Integer price);
List<Fruit> findByPriceLessThan(Integer price);
}
@Service
public class FruitServiceJpa implements FruitService {
private final FruitRepositoryJpa repository;
//Jpa를 상속받은 repository를 주입받는다.
public FruitServiceJpa(FruitRepositoryJpa repository) {
this.repository = repository;
}
@Override
public void saveFruit(String name, String date, Integer price) {
repository.save(new Fruit(name, price, date));
}
@Override
public void updateFruit(long id) {
Fruit fruit = repository.findById(id).orElseThrow(IllegalArgumentException::new);
fruit.setSell();
repository.save(fruit);
}
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return mapToAmount(repository.findSellAndTotalAmountByName(name));
}
public Map<String, Long> getFruitCount(String name) {
long count = repository.countByName(name);
Map<String, Long> resultMap = new HashMap<>();
resultMap.put("count", count);
return resultMap;
}
private FruitShopAmoutResponse mapToAmount(List<Object> result) {
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
result.stream()
.map(obj -> (Object[]) obj)
.forEach(rs -> {
boolean sell = (boolean) rs[0];
long totalAmount = ((BigDecimal) rs[1]).longValue();
if (sell) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
public List<FruitResponse> getListCondition(String condition, Integer price) {
if (condition.equals("GTE")) {
// select * from fruit where price > 16000
return repository.findByPriceGreaterThan(price).stream()
.map(fruit -> new FruitResponse(fruit.getName(), Integer.valueOf((int) fruit.getPrice()),
fruit.getWareHouseDate()))
.collect(Collectors.toList());
} else{
return repository.findByPriceLessThan(price).stream()
.map(fruit -> new FruitResponse(fruit.getName(), Integer.valueOf((int) fruit.getPrice()), fruit.getWareHouseDate()))
.collect(Collectors.toList());
}
}
}
Service에서 사용하는 repository를 Jpa를 상속받는 repositoryJpa로 주입받아서
repository를 통해 Jpa의 기능을 사용할수있다.
해당 인터페이스의 구현체를 따로구현하지않아도 구현체가 자동으로 생성되어주입되고
JpaRepository를 상속받으면서 빈으로도 자동등록되기때문에 따로 처리를 해줄필요도 없다. (간편)
기본 save나 update문 들은 Jpa에 구현되어있는 기능으로 만들수있었는데
getFruitStat은 조건과 그룹바이가 달려있어서 기본 쿼리만으로는 사용할수없어서
인터페이스에 따로 추상메서드를 만들어주었다.
@Query(value = "SELECT sell, SUM(price) as totalAmount FROM fruit WHERE name = :name GROUP BY sell", nativeQuery = true)
List<Object> findSellAndTotalAmountByName(@Param("name") String name);
@Query어노테이션 을 통해서 기존에 사용하던 쿼리문을 사용할수있다.
name으로 조회해서 sell필드로 그룹을 만들어서 그룹의 가격을 List<Object>로 리턴한다.
@Override
public FruitShopAmoutResponse getFruitStat(String name) {
return mapToAmount(repository.findSellAndTotalAmountByName(name));
}
private FruitShopAmoutResponse mapToAmount(List<Object> result) {
AtomicLong salesAmount = new AtomicLong(0);
AtomicLong notSalesAmount = new AtomicLong(0);
result.stream()
.map(obj -> (Object[]) obj)
.forEach(rs -> {
boolean sell = (boolean) rs[0];
long totalAmount = ((BigDecimal) rs[1]).longValue();
if (sell) {
salesAmount.addAndGet(totalAmount);
} else {
notSalesAmount.addAndGet(totalAmount);
}
});
return new FruitShopAmoutResponse(salesAmount.get(), notSalesAmount.get());
}
해당 쿼리를 통해서 List<Object>로 리턴되는값을 매핑해서
FruitShowAmountResponse객체로 만들어서 리턴해준다.
새로운 API를 추가하고 Jpa를 통해서 기능을 구현.
API 스펙
해당 API의 진입지점을 먼저 Controller에 만들어주자.
Controller
@GetMapping("/count") // api/v1/fruit 은 RequestMappint에서 명시되어있다.
public Map<String,Long> getFruitCount(@RequestParam String name){
return service.getFruitCount(name);
}
"count": long인 Json을 리턴해주기위해서 Map타입을 리턴해주도록 했다.
Repository
public Map<String, Long> getFruitCount(String name) {
long count = repository.countByName(name);
Map<String, Long> resultMap = new HashMap<>();
resultMap.put("count", count);
return resultMap;
}
파라미터로 들어오는 이름으로 조회해서 갯수를 세는 쿼리를 만들어야하는데
Jpa는 interface에 있는 메서드의 이름으로 해당 쿼리를 구현해서 자동으로 만들어준다,
Long countByName(String name); //select count(*) as count from fruit where name = ?
이렇게 인터페이스에 countByName이라는 함수를 만드는것만으로 기능을 구현할수있다.
메서드의 이름을통해서 쿼리를 만드는거라고 생각하면 될것같다.
여기도 규칙이있는데 이 규칙은 필요할때 찾아보면서 익숙해지면 편하게 사용할수있을것같다.
http://localhost:8080/api/v1/fruit/count?name=사과
이 문제도 Jpa를 통해서 쿼리를 만들지않고 추상메서드를 만드는것으로 해결할수있었다.
여기서는 조건에 따라 다른 쿼리문을 실행시켜줘야한다.
SQL문이었다면 쿼리문을 두개만들어놓고 조건에 따라 다른 쿼리를 실행시켜주거나
쿼리문 중간의 조건을 바꿔치기 하는방법으로 사용할수있을것같다.
Jpa에서는 쿼리문을 만들지않아도되기때문에 간단하게 추상 메서드를 두개 만들어서 사용해주었다.