반응형

1. 직원등록 기능

직원등록 기능

1. 직원이름 ,매니저인지 아닌지여부 ,입사일자 ,생일을 파라미터로 받는다.

 

예외처리

1. 각 파라미터 요소들은 값을 필수로 넣어주어야한다.

 

DTO

@Getter
public class MemberSaveRequest {
    @NotBlank
    private String name;
    @NotNull
    private Boolean isManager;
    @NotNull
    private LocalDate startDate;
    @NotNull
    private LocalDate birthDay;
}

Controller

@RestController
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService service;

    @PostMapping
    public void saveMember(@Valid @RequestBody MemberSaveRequest request) {
        service.saveMember(request);
    }
}

Service

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository repository;

    @Transactional
    public void saveMember(MemberSaveRequest request) {
        repository.save(new Employee(request));
    }
}

repository

public interface MemberRepository extends JpaRepository<Employee,Long> {
    Employee findByName(String name);
}

Entity객체

@Entity
@Getter
@NoArgsConstructor
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "position_id")
    private Position position; // 직급
    
    private Boolean isManager;//직책

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    @OneToMany(mappedBy = "employee")
    private List<CommuteHistory> commuteHistories;

    @OneToMany(mappedBy = "employee")
    private List<AnnualLeaveRegister> annualLeaveRegisters;

    private LocalDate startDate;
    private LocalDate birthDay;

    public Employee(MemberSaveRequest request) {
        this.name = request.getName();
        this.startDate = request.getStartDate();
        this.birthDay = request.getBirthDay();
        this.isManager = request.getIsManager();
    }
}

 

에러핸들러

@RestControllerAdvice
public class commuteServiceException {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> illegalArgumentExceptionHandle(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body("error : " + e.getMessage());
    }
	
    //Valid에서 발생시키는 에러를 처리.
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getAllErrors()
                .forEach(c -> errors.put(((FieldError) c).getField(), c.getDefaultMessage()));
        return ResponseEntity.badRequest().body(errors);
    }

}

빈값을 보내면 에러가 발생

 

 

 

요청을 보낸대로 잘 등록된걸 확인할수있다.

요청에서 이름 , 직책여부, 입사일, 생일 데이터만을 보내줬기때문에

나머지 필드는 null이 되어있다.

이 값들은 따로 지정해줄수있도록 만들어주었다.

//DTO
@Getter
public class MemberPositionSetDto {
    private String employeeName;
    private Long positionId;
}

@Getter
public class MemberTeamSetDto {
    private String employeeName;
    private Integer teamId;
}

//Service
@Transactional
public void setTeam(MemberTeamSetDto request) {
    Employee target = repository.findByName(request.getEmployeeName());
    Team team = teamRepository.findById(Long.valueOf(request.getTeamId()))
            .orElseThrow(IllegalArgumentException::new);
    target.setTeam(team);
}
@Transactional
public void setPosition(MemberPositionSetDto request) {
    Employee employee = repository.findByName(request.getEmployeeName());
    Team team = teamRepository.findById(request.getPositionId())
            .orElseThrow(IllegalArgumentException::new);
    employee.setTeam(team);
}

//Controller
    @PostMapping("/setTeam")
    public void setTeam(@RequestBody MemberTeamSetDto request) {
        service.setTeam(request);
    }

    @PostMapping("/setPosition")
    public void setPosition(@RequestBody MemberPositionSetDto request){service.setPosition(request);}

이 기능도 나중에 프론트가 생긴다면 조회한 직원의 필드값들을 input태그에 하나씩 넣어주었다가

수정해서 다시 요청하도록 처리하면 2개를 하나로합쳐서 직원정보 수정으로 변경할수있을것같다.

2. 직원조회 기능

DTO

@Getter
@Setter
@NoArgsConstructor
public class EmployeeResponse {
    private String name;
    private String teamName;
    private String role;
    private String birthDay;
    private String WorkStartDate;
    public void setRole(Boolean isManager){
        if(isManager){
            this.role = "Manager";
        }else{
            this.role = "MEMBER";
        }
    }
}

 

//Controller
@GetMapping
public List<EmployeeResponse> getEmployee() {
    return service.getEmployee();
}

//Service
public List<EmployeeResponse> getEmployee() {
                return repository.findAll().stream()
                .map(result -> {
                    EmployeeResponse response = new EmployeeResponse();
                    response.setName(result.getName());
                    response.setBirthDay(String.valueOf(result.getBirthDay()));
                    response.setRole(result.getIsManager());
                    response.setTeamName(Optional.ofNullable(result.getTeam()).map(Team::getTeamName).orElse(""));
                    response.setWorkStartDate(String.valueOf(result.getStartDate()));
                    return response;
                }).collect(Collectors.toList());
        
    }

 

처음에는 이렇게 모든 직원객체를 조회해서 DTO객체인 EmployResponse로 매핑해주는 방법으로 해주었다. 

이렇게 하니 서비스계층의 메서드가 너무 길어지는 문제가 생겼다. 

그래서 중간에 List<Employee> 를 받아서 --> List<EmployResponse> 로 매핑할 클래스를 하나 만들어주었다.

@Getter
public class ListEmployeeResponse {
    private final List<EmployeeResponse> employeeResponseList;

    public ListEmployeeResponse(List<Employee> employeeList) {
        this.employeeResponseList = employeeList.stream().map(result -> {
            EmployeeResponse response = new EmployeeResponse();
            response.setName(result.getName());
            response.setBirthDay(String.valueOf(result.getBirthDay()));
            response.setRole(result.getIsManager());
            response.setTeamName(Optional.ofNullable(result.getTeam()).map(Team::getTeamName).orElse(""));
            response.setWorkStartDate(String.valueOf(result.getStartDate()));
            return response;
        }).collect(Collectors.toList());
    }
}

이 객체에서 직원리스트를 받아서 응답리스트로 매핑을한다음 필드로 가지고있게된다. 

이런걸 일급컬렉션이라고 한다고 하는데. 그냥 사용해본거라 정확한 개념은 나중에 찾아봐야할것같다.

이렇게하면 서비스계층의 뚱뚱한메서드를 한줄로 줄일수있게된다.

public List<EmployeeResponse> getEmployee() {
    return new ListEmployeeResponse(repository.findAll()).getEmployeeResponseList();
}

[

    {
        "name": "김동현",
        "teamName": "개발팀",
        "role": "Manager",
        "birthDay": "1999-01-13",
        "workStartDate": "2024-03-02"
    },
    {
        "name": "박민주",
        "teamName": "디자인",
        "role": "Manager",
        "birthDay": "1999-07-02",
        "workStartDate": "2023-02-28"
    },
    {
        "name": "방세원",
        "teamName": "개발팀",
        "role": "Manager",
        "birthDay": "1999-07-02",
        "workStartDate": "2022-01-18"
    },
    {
        "name": "김민지",
        "teamName": "",
        "role": "MEMBER",
        "birthDay": "2000-01-18",
        "workStartDate": "2023-03-19"
    }

]

 

이렇게 응답을 잘 받아왔다. 

 

1단계의 팀처리기능을 끝내고 직원의 처리기능을 마무리했다.

세세한 로직들에서 어떻게 처리해주어야할지 고민이 많았는데 우선은 큰 에러처리없이 값을 잘 넘겨주도록 해주었다. 

이번에도 하다보니 더 공부해봐야할 내용들이 많이보여서 스프링이 정말 기능이 많다고 느꼈다. 

 

알아볼내용: @Valid , 일급컬렉션

반응형
반응형

1. 팀 등록

 

팀을 등록할 api의 명세이다.

요구사항은 간단하지만 막상 구현하려면 이렇게 간단하게만 작성할수는없다.

비즈니스적으로 예외사항에대한 처리를 해줘야하고

파라미터로 들어오는값도 검증이 필요하다면 해주어야한다.

 

팀 등록기능 

1. 팀을 등록할때에는 팀 이름을 필수로 가져야한다.

 

예외처리

1. 동일한 이름의 팀등록은 제외한다.

 

Controller

@RestController
@RequestMapping("/team")
public class TeamController {

    private final TeamService service;

    public TeamController(TeamService service) {
        this.service = service;
    }

    @PostMapping
    public void registerTeam(@RequestBody Map<String, String> requestJson){
        service.registerTeam(requestJson.get("teamName"));
    }
}

Repository

public interface TeamRepository extends JpaRepository<Team,Long> {
    Team findByTeamName(String teamName);
    
    //팀 이름을 조회하여 있으면 true 없으면 false를 리턴
    Boolean existsByTeamName(String teamName);
}

Service

@Service
public class TeamService {

    private final TeamRepository repository;

    public TeamService(TeamRepository repository) {
        this.repository = repository;
    }

    public void registerTeam(String teamName) {
    	//이미 있는 팀이름을 등록하는 경우 에러처리
   	//팀 이름을 키로 지정해도 같은 문제를 해결할수있을것같다.
        if(isExistTeam(teamName)){
            throw new IllegalArgumentException("이미 존재하는 팀 이름입니다: " + teamName);
        }
        repository.save(new Team(teamName));
    }
    
    private boolean isExistTeam(String name){
        return repository.existsByTeamName(name);
    }
}

 


 

 

디자인팀이 데이터베이스에 잘 저장되었다. 

같은 이름의 팀을 저장해보자.

 

 


 

동일한 이름의 팀을 저장하려고 시도하면 

에러가 발생한다.

 

 

 


 

이미 존재하는 팀의 이름을 저장하려고 하면 에러가 발생하는것을 볼수있다. 

api응답으로도 에러메시지를 받을수있는데 이경우에는 controller에서 ResponseEntity를 리턴해줘야하는것 같다.

간단한 저장이라서 응답없이 처리하도록 해주었기때문에

api응답은 에러의 발생원인이 서버에게있다고하는 500에러를 보내게된다. 

 

여기서 문제가 발생한다. 에러를 발생시킨부분은 서버가 맞지만 잘못된 요청으로 인해 에러가 발생한것으로 

다시 잘 요청해달라는 의미로 클라이언트 에러인 400번대 에러를 보내줘야한다. (클라이언트 너의 책임이다!)

 

여기서 스프링 Controller의 에러처리 기능인 @RestControllerAdvice를 사용해주었다. (처음 써봤다.)

 


@RestControllerAdvice , @ControllerAdvice

두 어노테이션 모두 스프링에서 Controller의 전역적인 에러처리를 위해서 사용된다. 

@ControllerAdvice

페이지가 있는 컨트롤러에서 이용되며

주로 에러페이지로 이동하는데 사용한다고 한다(아직 안써봄)

 

@RestController

에러메시지를 Json으로 내려줄수있어서 api를 개발할때 사용한다(적용).

RestControllerAdvice는 ControllerAdvice를 포함하고있다

 

@RestControllerAdvice
public class commuteServiceException {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> illegalArgumentExceptionHandle(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body("error : " + e.getMessage());
    }

}

 

이렇게 에러를 처리해줄 클래스를 만들고 @RestControllerAdvice어노테이션을 달아주면 

컨트롤러에서 IllegarArgumentException이 발생했을때  모두 여기서 처리해서 에러응답을 보내주게된다.

에러코드는 400으로 나온다. badRequest

 

이제 팀을 저장하는 기능이끝났다. 


2. 팀장 등록

추가적인 기능을 위해

팀 테이블에 팀장의 컬럼을 추가시켜서

이 팀의 팀장이 누구인지의 데이터를 가질수있게 처리해줬다.

팀을 저장할때는 팀의 이름만 저장하기때문에 팀장값은 null로 가지고있을것이다.  

이제 이 팀장을 지정해주는 기능을 만들어주어야한다.

 

요구사항

1. 직원의 정보를 보내서 팀장을 저장할수있게해준다.

 

예외처리. 

x.

 

따로 예외처리를 해주지는 않았지만 나중에 비즈니스 요구사항에 따라 다양한 예외처리가 추가될수있을것같다.

특정 직급이상만 팀장으로 등록이 가능하다거나.

특정 직원은 등록이 불가능하게 처리한다거나 할수있을듯하다. 

(현재는 딱히 필요없어보여서 생략)

//DTO
public class TeamSetDto {
    private String teamName;
    private Integer leaderCode; // 팀장의 이름이나 id 둘중의 하나만 가지면 될것같음
    //private String leaderName;
}

//jpa를 통해서 id 나 이름이 있다면 직원정보를 조회할수있기때문에 두개중에 하나만있다면 
//조회해서 다른정보를 불러서 사용이 가능하다.

//Controller
@PostMapping("/setLeader")
public void setTeamLeader(@RequestBody TeamSetDto request){
    service.registerTeamLeader(request);
}

//Service
@Transactional
    public void registerTeamLeader(TeamSetDto request) {
        Team team = repository.findByTeamName(request.getTeamName());
        team.setTeamLeader(request.getLeaderCode());
    }
    
//팀원의 이름이나 정보로조회해서 저장하는경우.
@Transactional
    public void registerTeamLeader(TeamSetDto request) {
        Team team = repository.findByTeamName(request.getTeamName());
        Employee employee = memberRepository.findByName(request.getName());
        team.setTeamLeader(employee.getId());
    }

 

각 계층에서 추가한부분만 옮겨봤다.

팀의 정보와 팀장으로저장할 직원의 정보를받아서 팀에 넣어주면

영속성컨텍스트에의해 조회한 team 인스턴스가 변경된걸 캐치해서

자동으로 업데이트 해주게된다. 

 

개인적인 생각

지금은 그냥 postman을 통해서 요청값을 세팅하기때문에 문자열정보인 String이 더 편하지만

추후에 개발시에 프론트를 구성한다면 직원정보를 조회하고

보이는쪽은이름 실제 보내지는 value쪽은 직원의 id값이 들어가서

api에 id로 요청하는게 더 수월해지기때문에 id가 필요한 api로 개발하는게 더 좋은것같다.

조회는 id로 해야 db의 index로 인해서 더 빠르다는 얘기도 들어본것같은...

 

 

 


3. 팀 조회기능

//DTO
@Getter
@AllArgsConstructor // Jpa매핑용 , 모든파라미터생성자
public class TeamResponse {
    private String name;
    private String manager;
    private Long count;
}

//Controller
@GetMapping
public List<TeamResponse> getTeam(){
    return service.getTeam();
}

//Service
public List<TeamResponse> getTeam(){
        return repository.getAllTeam();
    }
    
//Repository
// Jpql사용
@Query("SELECT new Company.demo.commute.dto.response.TeamResponse(a.teamName, b.name, COUNT(c.id)) " +
                  "FROM Team a " +
                  "LEFT JOIN Employee b ON a.teamLeader = b " +
                  "LEFT JOIN Employee c ON a.teamId = c.team.teamId " +
                  "GROUP BY a.teamName, b.name")
    List<TeamResponse> getAllTeam();

조회결과.

 

이번에는 Jpql로 데이터를 조회했다.

jpa로 응답객체를 내주는데에는 여러가지방법이있는데 간단한조회라면 

그냥 객체를 findby로 조회해서 서비스레이어에서 매핑해서 응답객체를 내주는것도 괜찮은 방법인것같다. 

여기서는 팀장의 이름을 알아내기위해 직원 테이블도 조인해줘야하고 팀의 총인원을 집계해줘야해서

단순조회로는 코드가 너무 길어질것같아서 쿼리로 처리해 주었다.

Jpql에서 join을 사용하려면 Entity객체에서도 관계설정이 되어있어야한다.

이로인해서 기존 Entity코드에도 변경이 생겼다.

 

Team (Entity)

@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long teamId;
    
    private String teamName;
    
    @OneToOne
    @JoinColumn(name = "team_leader_id")
    private Employee teamLeader;
    //private Integer teamLeaderId;
    
    private Integer annualLeaveBefore;

    @OneToMany(mappedBy = "team")
    private List<Employee> employeeList;

    public Team(String positionName) {
        this.teamName = positionName;
    }

    public void setTeamLeader(Employee leader) {
        this.teamLeader = leader;
    }
    // public void setTeamLeader(Integer leaderCode){
    //  this.teamLeaderId = leaderCode;
    // }
}

Team 엔티티 객체에서 기존에 단순히 팀장코드를 가지고있던 필드에 관계설정을 해줘서 직원객체랑 매핑해주었다.

이렇게 해주어야 Jpql에서 join을 사용해서 테이블끼리 조인해주는게 가능했다. 

Jpql문법에 대해서는 따로 공부해본적은없는데 관계설정해주는것만생각하면 

기본 sql문과 크게 차이없이 사용이가능한것같다.

 

이렇게 미니프로젝트 팀관련 api는 모두 처리해주었다. 

다음은 직원에 관련된 api기능을 처리해주자.

 

반응형
반응형

7일동안 과제를 마치고 이제 미니프로젝트 과제를 하게되었다. api 명세를 받아서 

api를 개발하면 되는데 지금 다 끝난시점에서 쓰는거지만 하나씩 할때마다 테이블에대해서도 고민하고 쿼리에대해서도 여러가지 생각을 해보게되는 좋은 경험이었다. 

 

[ 프로젝트 1단계 ]

SpringInitilizer로 스프링프로젝트를 생성한후에 실행해주었다.

 

웹으로 접근이 가능하게 하기위해서 spring web

Jpa를 쓰기위해서 spring Jpa

MySql 데이터베이스를 사용하기위해서 MySQL Driver

코드를 줄이고 편하게작성하기위해서 lombok을 의존성에 추가해서 프로젝트를 만들어주었다.

Dependencies추가

 

 

이제 필요한 기능명세에 따라서 기능을 만들어주겠다.

 

위의 기능을 보고 컨트롤러를 작성하기전에 DB를 먼저 생성해주었다. 

필요한 정보를보면 직원의정보 , 직원이 속한 부서 ,직원의 직급이 필요하다.

 

직원의 정보에 부서와,직급정보가 모두 포함될수도있지만 중복이 발생하기때문에 서로 분리해주었다.(정규화)

 

[직원]

직원코드, 직원 이름 , 직급(코드) , 부서(코드) , 생일 , 입사일

 

[직급]

직급코드, 직급이름

 

[부서]

부서코드, 부서이름 , 부서장(직원코드)

 

이렇게 간단하게 개념을 정리하고 테이블을 설계해주었다 코드는 기본키로설정하고

auto Increase로 자동증가하도록 해주었다. 

 

그리고 이런개념을 정리하기위해서 erd를 작성해보았다.

 

아래로가는 화살표는 이후 과제를 하면서 추가되는 테이블이다.

직급과 부서에는 해당하는 정보만 저장되도록하고 직원테이블에는 직급과 부서를 외래키로 참조하도록해서

직급의 이름과 부서의 이름을 받아올수있도록 해주었다. 

(사실 이게 제대로 정규화가 된건지는 잘모르겠다.)

이어진 선을보면 알수있는데 직급,부서 하나에 직원이 여러명 연결된 1:N관계이다.

 

이제 이 구조대로 자바코드에 DAO객체를 만들어주고 JpaRepository를 만들어주면 api를 만들 준비가 끝난다.

 

Employee(DAO)

@Entity
@Getter
@NoArgsConstructor
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "position_id")
    private Position position;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    private String startDate;
    private String birthDay;
}

 

@NoArgsConstructor어노테이션을 사용하면 파라미터가없는 기본생성자가 생성된다.( lombok )

EmployeeRepository(Repository)

public interface EmployeeRepository extends JpaRepository<Employee,Long> {
    Employee findByName(String name);
}

 

직원의 이름으로 값을 받아올수있는 추상메서드를 하나 작성해두었다.

 

Position(DAO)

@Entity
@NoArgsConstructor
@Getter
public class Position {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long positionId;
    private String positionName;
    @OneToMany(mappedBy = "position")
    private List<Employee> employeeList;
}

 

 

직급은 여러개의 직원정보를 가질수가있다.  (1:N)

예를 들어 직급이 대리 인데이터를 직원테이블에서조회한다고하면 김대리,이대리,박대리 가 나올수있는것이다.

그래서 직급하나가 여러개의 직원정보를 가져서 1:N관계가 되는것이다.

Jpa에서는 이런관계를 @OneToMany로풀어낼수있다.

이 기능을 이용하기위해서는 직급코드를 가지고있는

Employee클래스에서도 @ManyToOne으로 표기를 해주어야 서로 매핑이 되어서 이용이 가능하다. 

 

이 관계에는 주인이있는데 상대방을 알고있는쪽이 주인이라고 생각하면 편하다 

직원 클래스는 직급의코드를 가지고있어서 직급에대한 정보를 알고있지만 

직급 클래스는 직원에대해서 어떠한 정보도 가지고있지않다. 

 

그래서 직급클래스에는 mappedBy속성으로 관계의 주인을 명시해주고

직원 클래스에는 @JoinColumn을 사용해서 외래키인 position_id를 명시해주어야한다. - (쿼리에 들어갈 컬럼이름)

PositionRepository(Repository)

public interface PositionRepository extends JpaRepository<Position,Long> {
    Position findByPositionName(String positionName);
}

직급이름으로 조회할수있게 추상메서드를 작성해주었다.

 

Team(DAO)

@Entity
@Getter
@NoArgsConstructor
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long teamId;
    private String teamName;
    private Integer teamLeaderId;
    @OneToMany(mappedBy = "team")
    private List<Employee> employeeList;
}

 

TeamRepository(Repository)

public interface TeamRepository extends JpaRepository<Team,Long> {
    Team findByTeamName(String teamName);
}

부서이름으로 조회할수있게 추상메서드를 작성해주었다.

 

 

 

\main\resources\application.yml

spring:
  datasource:
    url: "jdbc:mysql://localhost:3308/company"
    username: "root"
    password: ""
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

리소스에 applocation으로 설정정보를 작성까지 해주었다.

 

이제 데이터베이스에 접근하기위한 준비가 모두 끝났다. 이제 API명세를 보고 필요한 기능을 하나씩 작성해보자.

반응형
반응형

 

FruitService

(기존코드)

@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);
}

 

Interface로 repository를 만들어주고 JpaRepository를 상속받아준다. 

Jpa의 기본기능은 따로 상속받아서 오버라이딩 하지않아도.

save나 findById findAll같은 기본조회나 업데이트가 사용가능하다.

 

Service

@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에서는 쿼리문을 만들지않아도되기때문에 간단하게 추상 메서드를 두개 만들어서 사용해주었다.

 

repositoryJpa

List<Fruit> findByPriceGreaterThan(Integer price);
List<Fruit> findByPriceLessThan(Integer price);

 

Service

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에서 문자열로들어오는조건에 따라서 다른 메서드를 실행하게 처리해서 적용해주었다. 

 

Controller

@GetMapping("/list")
public List<FruitResponse> getListCondition(String condition, Integer price){
    return service.getListCondition(condition,price);
}

 

 

Jpa를 새로 적용하면서 중간에 에러도 많이 발생했는데 거의 대부분이

객체의 필드와 조회된 테이블이 매칭되지않아서 생기는 문제였다. 

이후에 익숙해지고 컨버터 개념도 배우면 좀 쉽게 해결이가능할것같지만 우선은 지금사용하는정도로도 

코드의 양을 눈에 띄게 줄일수있고 보는걸로 db에서 어떤기능을 하는지 쉽게 파악할수있게되었다.

반응형

+ Recent posts