스프링 데이터 JPA 소개
소개
소리가 좀 큼
프로젝트 환경설정
프로젝트 생성
스프링 데이터 JPA와 DB 설정, 동작확인
application.yml 생성
show_sql
org.hibernate.SQL
org.hibernate.type
entity
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
}
repository
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em;
public Member save(Member member){
em.persist(member);
return member;
}
public Member find(Long id){
return em.find(Member.class, id);
}
}
test
단축키 : 테스트
junit5
@SpringBootTest
이제는 RunWith 안씀
import org.junit.jupiter.api.Test;
생성자에서 파라미터 넘기는게 더 나은 방법
protected Member(){ }
JPA가 사용할수 있게 기본생성자를 사용.
그러나 사람이 사용 못하게 protected
에러가 나네요.
No EntityManager with actual transaction available
디버그 : yml
format_sql: true영속성 복습
스프링 Data jpa 리포지토리
test 생성
Optional 제공 = 있을수도 있고 없을수도 있다.
원래 이렇게 쓰면 안되요.
NosuchElementException 에러 뜸
Member findMember = memberRepository.findById(savedMember.getId()).get();
내부 확인
외부 라이브러리 : p6spy
예제 도메인 모델
예제 도메인 모델과 동작확인
Member 수정
실무에서 좋아하는 방법
@Column(name="member_id")
맴버랑 팀은 다 : 1
private List<Member> members = new ArrayList<>();
프로젝트 세팅
상용 버전
도움이 됩니다.
@OneToMany(mappedBy = "team")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
team 넣으시면 큰일남
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of={"id" , "name"})
public class Team {
연관관계 메소드
public void changTeam(Team team){
this.team = team;
team.getMembers().add(this);
}
Team 생성자 추가.
setter 쓰지마
터트리거나 무시하거나
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 10, teamA);
Member member3 = new Member("member3", 10, teamB);
Member member4 = new Member("member4", 10, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
em.flush();
em.clear();
//확인
List<Member> members = em.createQuery(
"select m from Member m", Member.class).getResultList();
for (Member member : members) {
System.out.println("member = " + member);
System.out.println("-> member.team = " + member.getTeam());
}
@XxToOne 관계는 fetch = LAZY 필수
정리
공통 인터페이스 기능
순수 JPA 기반 리포지토리 만들기
공통 인터페이스
jpa 모르면 한계가 옴
em.remove
private void delete(Member member){
em.remove(member);
}
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
JAVA8 Optional 중요해
public Optional findById(Long id){
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
public long count(){
return em.createQuery("select count(m) from Member m", Long.class)
.getSingleResult();
}
단축키 : 같은 변수명 바꾸기
public long count(){
return em.createQuery("select count(t) from Team t", Long.class)
.getSingleResult();
}
거의 같다.
JPA 데이터 변경감지 = 수정 함수가 필요 없음.
단축키 : 테스트 파일로 이동
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberJpaRepository.save(member1);
memberJpaRepository.save(member2);
옵셔널로 쓰세요
Member findMember1 = memberJpaRepository.findById(member1.getId()).get();
Member findMember2 = memberJpaRepository.findById(member2.getId()).get();
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
//리스트 조회 검증
List<Member> all = memberJpaRepository.findAll();
assertThat(all.size()).isEqualTo(2);
//카운트 검증
long count = memberJpaRepository.count();
assertThat(count).isEqualTo(2);
//삭제 검증
memberJpaRepository.delete(member1);
memberJpaRepository.delete(member2);
long deletedCount = memberJpaRepository.count();
assertThat(count).isEqualTo(2);
개발자의 유일한 여유
변경 되는거 맞나요.
더티 체킹
변경 감지 검증
팀 테스트는 생략
공통 인터페이스 설정
쿼리 메소드 기능
메소드 이름으로 쿼리 생성
쿼리 메소드 기능
쿼리 메소드
기가막힙니다.
public List<Member> findByUsernameAndAgeGreaterThen(String username, int age){
return em.createQuery(
"select m from Member m" +
" where m.username = :username" +
" and m.age > : age")
.setParameter("username", username)
.setParameter("age", age)
.getResultList();
}
단축키 : 이전파일
@Test
public void findByUsernameAndAgeGreaterThen(){
Member m1 = new Member("AAA", 10);
Member m2 = new Member("AAA", 20);
memberJpaRepository.save(m1);
memberJpaRepository.save(m2);
memberJpaRepository.findByUsernameAndAgeGreaterThen("AAA", 15);
}
public Member(String username, int age) {
this.username = username;
this.age = age;
}
assertThat(result.get(0).getUsername()).isEqualTo("AAA");
assertThat(result.get(0).getAge()).isEqualTo(20);
assertThat(result.size()).isEqualTo(1);
잘 되는거에요. = 내가 귀찮은거에요.
상용버전
public void findByUsernameAndAgeGreaterThan(){
Member m1 = new Member("AAA", 10);
Member m2 = new Member("AAA", 20);
memberRepository.save(m1);
memberRepository.save(m2);
List<Member> result = memberRepository.findByUsernameAndAgeGreaterThan("AAA", 15);
assertThat(result.get(0).getUsername()).isEqualTo("AAA");
assertThat(result.get(0).getAge()).isEqualTo(20);
assertThat(result.size()).isEqualTo(1);
}
Did you mean username?
스프링 데이터 메뉴얼
개인적으로 2개정도 까지는 쓴다.
컨디션 안넣으면 전체 조회
find별명ByuserName.. 이렇게 식별하수 있다.
find별명2ByuserName..
Count, Exists, Delete, Distinct, limit
짧은 쿼리들
컴파일에 또 오류를 검출할수 있다.
사람 처럼 말해줌
JPA NamedQuery
@Query, 리포지토리 메소드에 쿼리 정의하기
@Query, 값, DTO 조회하기
@Query 값, DTO
실무 테스트 케이스에서는 assert로 해야됨.
@Query("select m.username from Member m")
List<String> findUsernameList();
Dto에 @Data 쓰지마 getter, setter 다들어가 있음.
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
롬복 toString
Dto 를 쓸때 주로 많이 씀.
파라미터 바인딩
반환 타입
유연한 반환타입 지원
List<Member> findListByUsername(String username); //컬렉션
Member findMemberByUsername(String username); //단건
Optional<Member> findOptionalByUsername(String username); // 단건 optional
Optional<Member> aaa = memberRepository.findOptionalByUsername("AAA");
System.out.println("aaa = " + aaa.get());
컬렉션 조회 = 데이터가 없을수 있음
없으면 빈 컬렉션을 반환함
안좋은 코드 = 큰일납니다.
if(result != null){ }
단건일때 결과가 없음. = NULL
SpringDataJPA 는 개발자가 불편할까봐 try catch로 감싼다음 NULL 반환
JPA는 NoResultException
더 좋은건 Optional
result = Optional.empty
Optional<Member> result = memberRepository.findOptionalByUsername("AAAB");
System.out.println("result = " + result.orElse());
1개 나와야 되는데 2개가 나오면 = 예외가 터집니다.
IncorrectResultSizeDataAccessException: query did not return a unique result: 2;
NonUniqueResultException 을 바꿔 준거임
page<T>
순수 JPA 페이징과 정렬
public List<Member> findByPage(int age, int offset, int limit){
return em.createQuery(" select m from Member m where m.age = :age order by m.username desc")
.setParameter("age", age)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
public long totalCount(int age){
return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
.setParameter("age", age)
.getSingleResult();
}
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 10));
memberJpaRepository.save(new Member("member3", 10));
memberJpaRepository.save(new Member("member4", 10));
memberJpaRepository.save(new Member("member5", 10));
int age = 10;
int offset = 0;
int limit = 3;
//when
List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
long totalCount = memberJpaRepository.totalCount(age);
좋은게 있습니다.
스프링 데이터 JPA 페이징과 정렬
아주 기가 막힙니다.
요즘 더보기 페이징 = Slice
단축키 :
Pageable = 페이지의 상태
Page = 반환 타입
PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> findByAge(int age, Pageable pageable);
total count 가져올 필요가 없음.
조회 쿼리
디버깅 : Repository 쪽 pageable 참조가 문제가 되서 호출 부분 에서도 캐스팅이 안되는 문제가 발생.
양쪽을다 맞춰주자.
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
count 쿼리
assertThat(content.size()).isEqualTo(3);
assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0);
assertThat(page.getTotalPages()).isEqualTo(2);
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();
정리
인터페이스
Slice 무한 페이징
내 주문 0번째 에서 3개
왜 total count를 부를까요.
주의할점
여기는
Page<Member> findByAge(int age, Pageable pageable);
얘는 슬라이스
//when
Slice<Member> page = memberRepository.findByAge(age, pageRequest);
count 쿼리가 없음.
내 주문은 3개
리미티는 4개
모바일 리스팅 기법
보통의 경우 " 아 안되요"
여기는 반환 타입 바꾸거나
같은 코드인데 반환 타입만 다른 api를 추가하거나.
페이징 쿼리를 안쓰는 이유
totalCount가 데이터 많아질수록 최적화가 힘듬
잘짜야 될때가 있음.
LEFT OUTER를 할때 카운트는 조인을 할 필요가 없음.
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m.username) from Member m")
List<Member> findByAge(int age, Pageable pageable);
실무썰
이제는 집중해야되는 쿼리만 짜면됨
Sorting 도 복잡해지면 저걸로 안됨.
소팅 정보도 넣을수 있음.
실무 꿀팁 - 활용편 2탄
그대로 반환하면 큰일남.
왜냐면 엔티티라는건 절대 노출하면 안됨
쉽게 DTO로 변환하는 방법
Page<MemberDto> map = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
뒤에 강의에 있습니다.
퇴근 쌉가능
너무 편해져서 감회가 있다.
정리
주의 페이지가 0부터 시작한다!
벌크성 수정 쿼리
벌크성 수정 쿼리
"모든 직원에 연봉을 10% 인상해"
코딩
회원의 나이를 한번에 변경하는 쿼리 JPA
응답 값의 개수가 나옴
executeUpdate
단축키 : 인라인
public void bulkUpdate(){
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 19));
memberJpaRepository.save(new Member("member3", 20));
memberJpaRepository.save(new Member("member4", 21));
memberJpaRepository.save(new Member("member5", 40));
//when
int resultCount = memberJpaRepository.bulkAgePlus(20);
//then
assertThat(resultCount).isEqualTo(3);
}
단축키 : 지금 코드블럭 돌리기
update member set age=age+1 where age>=20;
반환타입 맞춰야됨.
@Query("update Member m set m.age = m.age + 1 where m.age >= : age")
int bulkAgePlus(@Param("age") int age);
@Modifying = 변경하는구나 = 함수,결과값 다름을 표시
디버그 : ": age" -> ":age" 한칸
@Query 빼면 어노테이션 하라고 에러뜸.
Not supported for DML operations [update study.datajpa.
이게 되게 쉬운데..
문제가 있어요.
조심해야할 부분이 있다.
영속성
em.flush();
em.clear();
save 같은 로직 때도 영속성 초기화를 한다.
영속성 옵션이 있다.
@Modifying(clearAutomatically = true)
@EntityGraph
@EntityGraph
//given
//member1->teamA
//member2->teamB
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
teamRepository.save(teamA);
teamRepository.save(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 10, teamB);
memberRepository.save(member1);
memberRepository.save(member2);
em.flush();
em.clear();
//when
List<Member> members = memberRepository.findAll();
for (Member member : members) {
System.out.println("member = " + member.getUsername());
}
@엔티티그래프로 패치 조인을 이용할수 있다.
for (Member member : members) {
System.out.println("member = " + member.getUsername());
System.out.println("member.team = " + member.getTeam().getName());
}
프록시 복습
System.out.println("member.teamClass = " + member.getClass());
fetch join 설명
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
한번에 다긁어옴
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.team_id as team_id4_0_0_,
member0_.username as username3_0_0_,
team1_.name as name2_1_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
member.teamClass = class study.datajpa.entity.Member
LAZY 때문에 프록시로 만들고 나중에 가져오다가
한번에 긁어옴
쿼리쓰기 귀찮은데
그래서 나온게 @엔티티그래프
findAll은 상위 함수.
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
member = member1
member.teamClass = class study.datajpa.entity.Member
member.team = teamA
member = member2
member.teamClass = class study.datajpa.entity.Member
member.team = teamB
패치 조인만 추가 해도됨.
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findAll();
회원만 있으면 가져올 필요가 없다.
그러나 얽혀 있는것이 너무 많다.
같은 이름 변경
@EntityGraph 는 패치조인을 편하게 사용할수 있다.
JPA 표준 스팩
Named = 거의 안씀
@NamedEntityGraph(name="Member.all", attributeNodes = @NamedAttributeNode("team"))
public class Member {
@EntityGraph("Member.all")
List<Member> findEntityGraphByUsername(@Param("username") String username);
간단하게 쓸때는 이렇게
아니면 JPQL 에서 패치 조인을 쓴다.
JPA Hint & Lock
하이버네이트 에게 알려주는 힌트.
우리가 아는 그 힌트가 아니다.
ReadyOnly 쿼리 기능
@Test
public void queryHint(){
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
}
결과 동기화
em.flush();
영속성 컨텐스트 날리기
em.clear();
변경 감지의 치명적인 단점
= 원본이 있어야된다.
변경 안할꺼야 = 메모리 안쓸꺼야
@QueryHints(value=@QueryHint(name="org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
변경 감지 체크를 안함.
삼천포
다 ReadOnly 로 해야지 = 성능 향상 거의 없음
그냥 레디스 쓰세요.
ROCK
//select for updateJPA 기능 : 비관적인 락
import javax.persistence.LockModeType;
@Lock(LockModeType.PESSIMISTIC_READ)
List<Member> findLockByUsername(String username);
List<Member> result = memberRepository.findLockByUsername("member1");
member0_.username=? for update
짤막 광고
확장 기능
사용자 정의 리포지토리 구현
사용자 정의 리포지토리
우리가 봣던 모든 인터페이스 구현 = 현실적이지 않음.
마이바티스 기능을 활용한다거나.
실무에서 굉장히 많이 씀
특히 QueryDsl
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery(
"select m from Member m")
.getResultList();
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom
스프링 Data JPA가 엮어서 해주는것
@Test
public void callCustom(){
List<Member> memberCustom = memberRepository.findMemberCustom();
}
쿼리 나옴
이름을 맞춰야됨
MemberRepository + Impl
바꿀수 있는데 하지마
핵심 비즈니스 로직 쿼리랑 화면 쿼리랑 분리할 필요가 있다.
멀리간 이야기긴한데 망하는 지름길이 될수도 잇다.
핵심 비즈니스 리파지토리 클래스가 다름
//분리된 쿼리
@Autowired MemberQueryRepository memberQueryRepository;
Auditing
JPA-Auditing
기본적으로 테이블을 만들때 남기는것들
등록일, 수정일, 등록자, 수정자
실무에서 많이 사용합니다.
실수로 바꿔도 디비에 적용 안됨.
@Column(updatable = false)
persist 하기전에 이벤트 발생
@PrePersistthis는 중요해서 강조할때.
쿼리 할때 null이 있으면 지저분해짐
public class Member extends JpaBaseEntity
create table member에 없음.
JPA 에는
진짜 상속(기본편 상속 강의)과
속성만 상속이 있다.
@MappedSuperclassMember member = new Member("member1");
memberRepository.save(member); //@PrePersist
테스트에 슬립 안좋은 코드
Thread.sleep(100);
member.setUsername("member2");
em.flush(); //@PreUpdate
em.clear();
//when
Member findMember = memberRepository.findById((member.getId())).get();
//then
System.out.println("findMember = " + findMember.getCreatedDate());
System.out.println("findMember = " + findMember.getUpdatedDate());
디비 확인
모든 등록일 수정일의 자동화
스프링 JPA DATA
@EnableJpaAuditing설정 필요
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
등록자 수정자
@CreatedBy
@Column(updatable = false)
private String createBy;
@LastModifiedBy
private String lastModifiedBy;
@Bean
public AuditorAware<String> auditorProvider(){
// 스프링 시큐리티 자리
return ()-> Optional.of(UUID.randomUUID().toString());
}
UUID 사용
Optional.of(UUID.randomUUID().toString());
실제는 스프링 시큐리티 쓰시면 홀드 세션 정보와서 꺼내 와서
전체적용 XML도 있다.
저 같은 경우에는
테이블마다 특성이 달라서 필요할때도 있고 아닐때도 있다.
(부)베이스타임 - (모)베이스엔티티
Web 확장 - 도메인 클래스 컨버터
Web 확장 도메인 클래스 컨버터
잘보세요
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id){
Member member = memberRepository.findById(id).get();
return member.getUsername();
}
@GetMapping("/members2/{id}")
public String findMember(@PathVariable("id") Member member){
return member.getUsername();
}
개인적으로 권장하지 않음.
왜냐면 Long 으로 막 접근하는것 찾는것도 쉽지 않다.
!주의 조회용
Web 확장 - 페이징과 정렬
(필수)Web 확장 - 페이징과 정렬
글로벌 설정
data:
web:
pageable:
default-page-size: 10
max-page-size: 2000
컨트롤러에만 설정하고 싶어.
count(member0_.member_id) as col_0_0_
엔티티 노출하지마세요.
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size=5, sort="username")Pageable pegeable){
Page<Member> page = memberRepository.findAll(pegeable);
Page<MemberDto> map = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
return map;
}
단축키 : 인라인
꿀팁
생성자로 들어가야된다.
단축키 : 부가기능(람다-메소드레퍼런스)
반환 데이터
페이지를 1부터 시작하고 싶어
궁극적인 해결 방법:
pageable 말고 임의의 객체를 사용.
귀찬아
2번 좀 한계가 있다.
one-indexed-parameters: true
디버깅 = this 잘쓰자.
뭔가 되는거 같은데 여기서 막혀요.
밑에 데이터가 안맞음
스프링 데이터 JPA 분석
스프링 데이터 JPA 구현체 분석
새로운 엔티티를 구별하는 방법
새로운 엔티티를 구별하는 방법
전략
기능:
디버그
단축키 : 이전 테스트
shift+F9
primitiv
단축키 : 디버깅 넘김 F8
@SpringBootTest
class ItemRepositoryTest {
@Autowired ItemRepository itemRepository;
@Test
public void save(){
Item item = new Item("UUID");
itemRepository.save(item);
}
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {
@Id
private String id;
public Item(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
PK에 값이 있다. = persist 호출 안됨.
DB에 없는거 확인
select item0_.id as id1_0_0_ from item item0_ where item0_.id='UUID';
그리고 넣는다.
insert into item (id) values ('UUID');
이래서 Merge를 수정으로 쓰면 안된다.
강사님은 업무에 깔고 들어간다.
ID를 생성해야 할때가 있다. = 데이터 많을때
Persistable
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable {
@Id
private String id;
@CreatedDate
private LocalDateTime createDate;
public Item(String id) {
this.id = id;
}
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createDate == null;
}
}
실무 사용법 - 많이 써요
(중요)persist 되기전에 createdDate가 호출
@Override
public boolean isNew() {
return createDate == null;
}
ㅋㅋ
디버깅:
@EntityListeners(AuditingEntityListener.class)
정리
나머지 기능들
네이티브 쿼리
왠만하면 쓰지 마세요.
@Query(value="select * from member where username?", nativeQuery = true)
Member findByNativeQuery(String username);
디버그 : username? -> username = ?
한계가 너무 많아요.
엔티티 가져오기도 난감함
Jdbc 탬플릿이나 마이바티스
Sort가 이상함
@Query(value="select m.member_id as id, m.userName, t.name as teamName" +
" from member m left join team t",
countQuery = "select count(*) from member",
nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);
Page<MemberProjection> result = memberRepository.findByNativeProjection(PageRequest.of(1, 10));
List<MemberProjection> content = result.getContent();
for (MemberProjection memberProjection : content) {
System.out.println("memberProjection.getUsername() = " + memberProjection.getUsername());
System.out.println("memberProjection.getTeamname() = " + memberProjection.getTeamname());
}
디버그 : 페이지는 0부터 시작
값 나옴
정적 쿼리 좀 쓸때는 쓸수도 있을듯
정리
진짜 복잡한건 하둡에서 가져옴





