[JPA] Spring Data JPA
Spring Data JPA는 데이터 접근 계층에서 지루하고 반복적인 CRUD 문제를 해결하기 위해 기본적인 CRUD를 처리하는 인터페이스가 정의되어 있다.
Repository 인터페이스
- Repository 인터페이스를 생성한후 JpaRepository<Entity, 기본키 타입>을 상속받으면 기본적인 CRUD 자동 생성
@Repository
public interface MemberRepository extends JpaRepository<MemberEntity, Integer> {
     int countByName(String name);
     Optional<Members> findByUsername(String username);
     @Transactional
     @Modifying     // DML문을 처리할 경우 사용
     @Query("UPDATE Members m SET m.password=:password WHERE m.username=:username")
     int changePassword(@Param("username") String username, @Param("password") String password);
}
- 기본적인 CRUD가 구현된 인터페이스를 상속받고 있기 때문에 따로 정의 할 필요는 없음.
- 필요에 따라 메서드를 추가하거나 오버라이딩해서 사용함.
JpaRepository
- JPA를 사용 할 수 있도록 정의된 인터페이스
- JpaRepository → PagingAndSortingRepository → CrudRepository 순으로 extends 되어있다.
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID> {
	List<T> findAll();
	List<T> findAll(Sort sort);
	List<T> findAllById(Iterable<ID> ids);
	<S extends T> List<S> saveAll(Iterable<S> entities);
    ...
}
CrudRepository
- JPA의 기본적인 CRUD가 구현되어 있는 인터페이스
public interface CrudRepository<T, ID> extends Repository<T, ID> {
  <S extends T> S save(S entity);               // 해당 entity 저장
  Iterable saveAll(Iterable entities)           // 해당 entity 복수 저장
  Optional<T> findById(ID primaryKey);          // ID로 entity 찾기(1개)
  Iterable<T> findAll();                        // 모든 entity 찾기
  boolean existsById(ID primaryKey);            // ID로 entity 존재여부 확인
  long count();                                 // 사용가능한 entity 수 반환
  void delete(T entity);                        // 해당 entity 삭제
  void deleteById(ID id)                        // ID로 entity 삭제
  void deleteAll();                             // 모든 entity 삭제
  void deleteAll(Iterable<? extends T> entities);  // 해당 entity 복수 삭제
  ...
}
Controller
@Controller
@RequestMapping("/member")
public class MemberController {
    @Autowired
    private MemberRepository memberRepository;
    @GetMapping("")
    public String memberList(Model model) {
        List<MemberEntity> memberList = memberRepository.findAll(Sort.by(Sort.Direction.ASC, "name"));
        model.addAttribute("memberList", memberList);
        return "/member/memberList";
    }
    @GetMapping("/{id}")
    public String memberDetail(@PathVariable("id") Long id, Model model) {
        Optional<MemberEntity> memberWrapper = memberRepository.findById(id);
        MemberEntity member = memberWrapper.get();
        model.addAttribute("member", member);
        return "/member/memberDetail";
    }
    @PostMapping("/create")
    public String memberCreate(MemberEntity memberEntity) {
        memberRepository.save(memberEntity);
        return "redirect:/member";
    }
    @PostMapping("/{id}/update")
    public String memberUpdate(@PathVariable("id") Long id, MemberEntity memberEntity, 
                                RedirectAttributes ra) {
        memberRepository.save(memberEntity);
        ra.addAttribute("updateId", id);
        ra.addFlashAttribute("msg", "수정에 성공하였습니다.");
        return "redirect:/member/{updateId}/update";
    }
    @PostMapping("/{id}/delete")
    @ResponseBody
    public void memberDelete(@@PathVariable("id") Long id) {
        memberRepository.deleteById(id);
    }
}
기본 사용법
| 기능 | 메서드 | 설명 | 
|---|---|---|
| 목록조회 | findAll() | 엔티티 전체 목록 출력 | 
| 상세조회 | findById(id) | 엔티티 1개 출력 | 
| 생성(Create) | save(entity) | 해당 엔티티 저장 | 
| 수정(Update) | save(entity) | 해당 엔티티 수정 | 
| 삭제(delete) | delete(entity) or deleteById(id) | 해당 엔티티 삭제 | 
| 전체삭제 | deleteAll() | 모든 엔티티 삭제 | 
* 생성과 수정에서 save()의 차이점
- 생성과 수정에서 모두 save()를 사용하지만 생성은 persist() 수정은 merge() 사용한다. SimpleJpaRepository의 save()에서 확인할 수 있다.
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    ...
    @Transactional
	public <S extends T> S save(S entity) {
		if (entityInformation.isNew(entity)) {
			em.persist(entity);
			return entity;
		} else {
			return em.merge(entity);
		}
	}
    ...
}
- 생성[persist] : entity의 id 값이 없을 때는 create를 실행한다
- 수정[merge] : entity의 id 값이 존재할 때는 select -> update를 실행한다.
    - HTTP PUT처럼 update 쿼리실행 : 모든 컬럼의 데이터가 변경된다
- update 쿼리를 사용하고 싶으면 merge보단 변경감지(Dirty Check) 사용을 추천한다
 
Query Method 방식
- findBy 뒤에 키워드가 붙음
- 
findBy메서드는 항상Optional타입으로 반환된다.
- 쿼리메서드 방식은 메서드명이 길어지기 때문에 일반적으로 선호되지 않는다.
| Keyword | 메서드 | Query | 
|---|---|---|
| And | findByLastnameAndFirstname | where lastname = ? and firstname = ? | 
| Or | findByLastnameOrFirstname | where lastname = ? or firstname = ? | 
| Is, Equals | findByFirstname,findByFirstnameEquals | where firstname = ? | 
| Between | findByStartDateBetween | where startDate between ? and ? | 
| GreaterThan | findByAgeGreaterThan | where age > ? | 
| GreaterThanEqual | findByAgeGreaterThanEaual | where age >= ? | 
| IsNull, Null | findByAge(Is)Null | where age is null | 
| Like | findByFirstnameLike | where firstname like ? | 
| StartingWith | findByFirstnameStartingWith | where firstname like ? | 
| Containing | findByFirstnameContaing | where firstname like ? | 
| OrderBy | findByAgeOrderByLastnameDesc | where age = ? order by lastname desc | 
| Not | findByLastnameNot | where lastname <> ? | 
@Repository
public interface MemberRepository extends JpaRepository<MemberEntity, Integer> {
    List<MemberEntity> findByLastnameOrderByIdDesc(String lastname);
}
@Query 방식
- 이름 길어지는 쿼리메서드 방식 대신 어노테이션을 사용하여 JPQL을 입력하는 방법을 사용할 수 있다.
- 쿼리메서드 방식보다 선호된다.
- 쿼리메서드와 @Query가 같이 있으면 쿼리메서드는 무시되고 어노테이션이 적용 된다
- 다만, 정적 값을 가지는 한계가 있기에 동적 쿼리를 위해서 QueryDSL을 사용할 수 있다.
@Repository
public interface MemberRepository extends JpaRepository<MemberEntity, Integer> {
    @Query("SELECT m FROM MemberEntity m WHERE lastname=:lastname ORDER BY m.id DESC")
    List<MemberEntity> getListDesc(@Param("lastname") String lastname);
}
벌크 연산
- 
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 날린다
    - 데이터 유효성을 위해 벌크 연산 후 영속성 컨텍스트를 초기화 한 후 데이터를 사용해야 한다(clear)
- 
clearAutomatically = true옵션을 사용하면 쿼리 실행 후 clear를 자동으로 한다
 
@Modifying
- 
@Query를 사용해 INSERT, UPDATE, DELETE 쿼리를 사용할 떄 추가하는 어노테이션
- 
    JpaRepository에서 제공하는 메서드나 쿼리 메서드 방식에는 적용되지 않는다
- 
clearAutomatically속성 : 쿼리 실행 직후 영속성 컨텍스트를 초기화한다
- 
flushAutomatically속성 : 해당 쿼리를 실행하기 전에 영속성 컨텍스트를 flush한다
@Modifying(clearAutomatically = true)       // clearAutomatically 설정
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
Pageable 인터페이스 (페이징처리)
- 
Pageable인터페이스 : 페이지 처리에 필요한 정보를 전달하는 용도의 타입
- 
PageRequest클래스 :Pageable의 구현체- protected 이기 때문에 new를 사용하지 못하고of()를 통해 사용한다
 
- protected 이기 때문에 
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@Test
public void testpageDefault() {
    // 1페이지 10개
    Pageable pageable = PageRequest.of(0, 10);
    Page<Memo> result = memoReopsitory.findAll(pageable);
    System.out.println(result.getTotalPages());         // 전체 페이지수
    System.out.println(result.getTotalElements());      // 전체 갯수
    System.out.println(result.getNumber());             // 현재 페이지번호 (0부터)
    System.out.println(result.getSize());               // 페이지당 갯수
    System.out.println(result.hasNext());               // 다음페이지 존재여부
    System.out.println(result.isFirst());               // 시작페이지 여부
    // 정렬
    Sort sort = Sort.by("mno").descending();
    Pageable pageable2 = PageRequest.of(0, 10, sort);
    Page<Memo> result2 = memoReopsitory.findAll(pageable2);
    Sort sort1 = Sort.by("mno").descending();
    Sort sort2 = Sort.by("mnoText").ascending();
    Sort sortAll = sort1.and(sort2);
    Pageable pageable3 = PageRequest.of(0, 10, sortAll);
    Page<Memo> result3 = memoReopsitory.findAll(pageable3);
}
Page<Memo> result = memoReopsitory.findAll(pageable);
→ findAll()에 파라미터로 pageable을 넘겨주면 페이징 처리 쿼리가 실행되고 결과를 리턴타입으로 지정된 Page<Entity타입> 객체로 저장
Page 객체
- 추가적인 count 쿼리 결과를 같이 반환하는 타입
- 페이지 번호가 0부터 시작
public interface Page<T> extends Slice<T> {
    int getTotalPages();     //전체 페이지 수
    long getTotalElements(); //전체 데이터 수
    <U> Page<U> map(Function<? super T, ? extends U> converter);    //변환기
}
→ 갯수를 출력할 수 있는 메소드를 갖고 있다
- count 쿼리 분리가능 : countQuery속성 사용- 여러 테이블을 조인하는 쿼리일 경우 count 쿼리에도 조인이 걸리기 때문에 성능에 문제가 생기기에 분리 하는 것이 좋다
 
@Query(value = “select m from Member m”,
         countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);
Slice 객체
- count 결과는 없고 다음페이지 여부(더보기) 확인을 위해 limit+1개를 결과를 반환하는 타입
public interface Slice<T> extends Streamable<T> {
    int getNumber();
    int getSize();
    int getNumberOfElements();
    List<T> getContent();
    boolean hasContent();
    Sort getSort();
    boolean isFirst();
    boolean isLast();
    boolean hasNext();
    boolean hasPrevious();
    Pageable getPageable();
    Pageable nextPageable();
    Pageable previousPageable();    //이전 페이지 객체
    <U> Slice<U> map(Function<? super T, ? extends U> converter);   //변환기
}
Controller에서 페이징 사용
- Spring data JPA는 페이징과 정렬 기능을 Controller에서 사용 할 수 있게 제공해 준다
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}
- 파라미터로 Pageable을 받으면PageRequest객체를 생성해서 요청 파라미터를 담는다
- 요청 파라미터 종류
    - /members?page=0&size=3&sort=id,desc&sort=username,desc
- page : 현재 페이지 (0부터 시작)
- size : 한 페이지당 갯수
- sort : 정렬 조건 ex) sort=속성,정렬방향