@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
private Long id;
private String username;
private int age;
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
==> 기본 생성자(파라미터 없는 생성자)를 자동으로 만들어주되 접근 제어자를 protected로 설정하라는 의미이다.
즉, 위 코드를 컴파일하면,
protected Member() {
}
와 같은 생성자를 만들어준다. 참고로 엔티티에서, 기본생성자가 아닌 생성자를 별도로 만들게 되면 기본생성자를 무조건 선언해줘야한다.
@ToString(of = {"id", "username", "age"})
==> 객체 자체를 System.out.println으로 출력하면 내부적으로 객체의 toString()이 호출되는데,
일반적인 객체 자체를 System.out.println으로 출력하면 해당 객체의 toString()이 따로 오버라이딩 되어있지 않기 때문에 주소값이 출력된다.
@ToString(of = {"id", "username", "age"})를 해주면, Member객체를 출력할때 id, username, age 필드만 포함해서 출력하겠다는 의미이다.
즉, 내부적으로
@Override
public String toString() {
return "Member(id=" + this.id + ", username=" + this.username + ", age=" + this.age + ")";
}
와 같이 오버라이딩 해준다.
참고로 of를 사용하지않고 @ToString만 기본으로 쓰면 모든 필드가 출력된다.
<Member> member = Optional.of(member1); 에서 Member를 꺼내는 방법public Optional<Member> findByLoginId(String loginId) {
List<Member> all = findAll();
return all.stream()
.filter(m -> m.getLoginId().equals(loginId))
.findFirst();
}=> stream의 filter()는 조건을 만족하면 다음 단계인 findFirst()를 실행하고 만족하지 않으면 버려지고 다음 루프를 돈다.public Member login(String loginId, String password){
return memberRepository.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password)
.orElse(null);
}=> Optional의 filter()는 Optional 객체 안에 저장된 값을 조건에 따라 필터링 한다. 조건을 만족하는 값이 있으면 해당 값(Optional<Member> 객체)을 그대로 반환하며, 그렇지 않으면 비어있는 Optional객체를 반환한다. 이때, 비어있는 Optional객체를 반환하지않고 다른값을 반환하고 싶으면 orElse()를 사용하면 된다.public long count() {
return em.createQuery("select count(m) from Member m", Long.class).getSingleResult();
}
=> count는 반환타입이 Long타입으로 나오기 때문에 반환타입을 Long.class로 하면되고, 한개만 리턴되기 때문에 getSingleResult()를 사용한다.
인터페이스는 인터페이스만 가지고 new로 객체를 생성할 수 없다. 인터페이스를 구현한 구현 클래스가 있어야한다. 그리고 객체는 구현 클래스를 가지고 생성한다.
즉,
public interface TestInterface {
}
가 있으면,
public class TestClass implements TestInterface {
}
와 같이 구현 클래스가 있어야하고,
이를 바탕으로
TestInterface t = new TestClass(); 와 같이 구현클래스로 객체를 만든다.
@SpringBootApplication
@EnableJpaRepositories(basePackages = "study.datajpa.repository")
public class DataJpaApplication {
...
}
// Spring Data Jpa를 사용하기 위해서는 @EnableJpaRepositories를 사용해서 사용하고자하는 위치를 적어줘야한다.
// 하지만 스프링 부트에서는 이를 생략해도, @SpringBootApplication이 선언된 클래스의 package 와 그 하위 패키지 모두에서, Spring Data JPA는 내부적으로 "JpaRepository를 상속한 인터페이스"를 구현한 클래스(프록시클래스)를 만들고, 구현객체(프록시 객체)를 생성해서 스프링빈으로 생성하고 필요한 곳에 주입해준다.
@Entity
@NamedQuery(name = "Member.findByUsername", query = "select m from Member m where m.username = :username")
public class Member {
}
@Query(name = "Member.findByUsername") // 선언된 NamedQuery를 사용하는방법
List<Member> findByUsername(@Param("username") String username); // NamedQuery의 JPQL에 파라미터로 값을 넘겨주기 위해서는 @Param을 무조건 적어줘야한다.
@Query("select m from Member m where m.username = :username and m.age = :age")
List<Member> findUser(@Param("username") String username, @Param("age") int age);
Query에 선언된 JPQL에 파라미터로 값을 넘겨주기 위해서는 @Param을 무조건 적어줘야한다.
참고로, @Query를 사용하면 메서드 이름으로 쿼리를 만들지 않고 @Query 내용을 그대로 사용하여 JPQL을 실행한다.
스프링 데이터 JPA는 유연한 반환 타입을 지원한다.
반환 타입은 단건(Optional, 객체)이 될 수 도 있고, 다건(List)이 될 수 도 있다. 메서드를 선언할 때 실제 나가는 쿼리 결과를 예상해서 개발자가 적절한 반환타입을 선언해주면 된다.
<Member> findByUsername(String name); // 다건 => 결과가 있으면 List<Member> 반환. 결과가 없으면 빈 컬렉션 반환.<Member> findByUsername(String name); // 단건 => 결과가 있으면 Optional<Member> 반환. 결과가 없으면 비어있는 Optional객체 반환, 결과가 2개 이상이면 예외 발생. SELECT * FROM table LIMIT 3 OFFSET 3;참고로 위 쿼리에서,
OFFSET 3 // 조회된 데이터에서 앞에서 3개 건너뛰고,
LIMIT 3 // 그 다음 3개를 가져와라.
즉, 4번째(OFFSET 3에서 1더한것)부터 3개를 가져오는것.
아래 쿼리와 동일한 결과이다.SELECT * FROM table LIMIT 3,3;
PageRequest객체는, Pageable인터페이스의 구현체이다.
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username");
// 첫번째 파라미터에는 현재페이지, 두번째 파라미터에는 조회할 데이터 수를 입력한다. 세번째 파라미터에 정렬조건도 넣어줄 수 있다.
// 이렇게 사용하면, 첫번째 페이지를 조회하고, 해당 페이지에 3개의 데이터를 보여준다는것이다.
// 즉, 1페이지(0번 페이지)를 조회하고, 그 페이지에는 3개의 데이터(limit 3)를 포함하며 username기준으로 DESC정렬한것이다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
@Test
public void paging() {
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 10));
memberRepository.save(new Member("member3", 10));
memberRepository.save(new Member("member4", 10));
memberRepository.save(new Member("member5", 10));
int age = 10;
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
}
3-1. MemberRepository에서 메서드를 선언할때, 메서드의 반환타입에 따라서 count쿼리를 날릴지 안날릴지 결정된다.
3-2.
<Member> findByUsername(String name, Pageable pageable); //반환타입을 Page로 하면 count쿼리(페이지당 개수가 아니라 전체 데이터의 개수를 구하는 쿼리)가 같이 나간다.<Member> findByUsername(String name, Pageable pageable); //반환타입을 Slice로 하면 count쿼리(페이지당 개수가 아니라 전체 데이터의 개수를 구하는 쿼리)는 나가지않는다. limit + 1로 조회된다. 데이터를 한개 더 가져온다.<Member> findByUsername(String name, Pageable pageable); //반환타입을 List로 하면 count쿼리는 나가지않는다.[Page]
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.age=?
order by
member0_.username desc limit 3
select
count(member0_.member_id) as col_0_0_
from
member member0_
where
member0_.age=10
[Slice]
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.age=?
order by
member0_.username desc limit 4
[List]
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.age=?
order by
member0_.username desc limit 3
참고로,
@Query(value = "select m from Member m")
Page<Member> findByAge(int age, Pageable pageable);
-> 이렇게 하면 페이징 쿼리는 아래와 같이 된다.
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
order by
member0_.username desc
limit 3
즉, 개발자는 jpql을 직접 짜더라도, 반환타입과 Pageable이 있기 때문에 페이징에 대한 쿼리는 직접 안짜도 된다. 페이징에 대한 쿼리는 자동으로 붙여준다.
참고로, 전체 count 쿼리는 매우 무겁다. 그래서 만약에 count쿼리의 성능이 좋지않는다면 count쿼리만 따로 지정해줄 수 있다.
예를들어
SELECT *
FROM A
LEFT JOIN B ON A.id = B.a_id
LIMIT ?, ?
와 같이
페이징 쿼리에서 WHERE조건이 없고, LEFT JOIN만 한다고 했을때, 전체 count쿼리에서는 left join을 하나 안하나 개수가 동일하기 때문에 left join을 안해도 된다.
예를들어
SELECT COUNT(*)
FROM A
LEFT JOIN B ON A.id = B.a_id
와
SELECT COUNT(*)
FROM A
는 동일하기 때문이다.
그래서 count쿼리를 따로 지정해줄 수 있다.
@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);
-> 이렇게 하면, 페이징쿼리는 아래와 같이되고,
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
order by
member0_.username desc
limit 3
전체 카운트 쿼리는 아래와 같이 된다.
select
count(member0_.member_id) as col_0_0_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
즉, count쿼리에서도 쓸데없이 left join을 한다.
@Query(value = "select m from Member m left join m.team t", countQuery = "select count(m) from Member m")
Page<Member> findByAge(int age, Pageable pageable);
-> 하지만 이렇게 countQuery를 지정해주면,
전체 count쿼리는 아래와 같이 된다.
select
count(member0_.member_id) as col_0_0_
from
member member0_
참고로 @Query 없이 메서드이름만으로 쿼리를 생성할때, 어떤 엔티티에 대해 쿼리를 생성할지는 리포지토리의 제네릭 타입(extends JpaRepository<Member, Long>)을 보고 나서 결정된다.
그래서, List<Member> findTop3By(); 라고 해도 Member에 대해 조회를하고, List<Member> findMemberTop3By()라고 해도 Member테이블에 대해 조회를 한다.
Member findMemberByUsername(String name); 이것도 Member findByUsername(String name); 이렇게 해도된다.
즉, 메서드 이름에 엔티티명을 꼭 넣을 필요는 없고, 제네릭이 기준이다.
참고로, stream의 map처럼, page.map()은 Page내부의 content(= List<Member> content)에 있는 각각의 데이터(Member)를 MemberDto로 변환해서 새로운 Page<MemberDto>로 반환한다.
즉, Page내부의 content만 List<Member>에서 List<MemberDto>로 변환하고 기존 Page의 페이징 정보(totalElements, totalPages 등)는 그대로 유지하면서 새로운 Page 객체를 만들어 주는것이다.
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bultAgePlus(@Param("age) int age);
참고로 JPA는 JPQL쿼리가 실행되면 flush()가 자동으로 호출되고 나서 JPQL이 실행된다.
엔티티그래프는 연관된 엔티티를 SQL 한 번에 가져오기 위해 사용하며, 내부적으로 페치조인과 (거의) 동일한 SQL이 실행된다.
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll(); //JpaRepository에 있는 findAll()메서드를 오버라이드 @EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
이렇게도 가능하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(String username);
이렇게도 가능하다. 실행하게 되면, 아래와 같은 SQL이 실행된다.
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
where
member0_.username=?
JPA 쿼리힌트는 JPA 구현체인 Hibernate에게 “이 쿼리를 이렇게 처리하라”라고 지시하는 옵션이다.
@QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true"))
List<Member> findReadOnlyByUsername(String username);
@Test
public void queryHint(){
Member member1 = new Member("member1", 10);
memberRepository.save(member1);
em.flush();
em.clear();
Member findMember = memberRepository.findReadOnlyByUsername("member1");
findMember.setUsername("member2");
}
@QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true", forCounting = false))
Page<Member> findByAge(int age, Pageable pageable);
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
@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 {
}
Spring Data JPA는 인터페이스(JpaRepository)를 기반으로 하는데, 특정 메소드는 직접만든 JPQL이나 복잡한 쿼리를 사용해야될 때가 있다. 그럴때 별도로 Custom Repository 인터페이스를 만들어서 사용한다.
인터페이스 이름은 아무거나 해도 되지만, 구현 클래스 이름은 Repository 인터페이스 이름 + Impl로 해야한다.
이렇게 하지않으면, 커스텀 레포지토리에서 만든 메서드를 사용할때 오류가 발생한다.
구현 클래스에 대한 객체 생성 및 스프링빈으로 등록되지않아서 메서드를 사용할 수 없다. 이 규칙대로 해야만 @Repository 또는 @Component 어노테이션이 없어도 Spring Data JPA가 자동으로 인식해서 객체를 생성하고 스프링빈으로 등록한다.
MemberRepository에서 MemberRepositoryCustom을 상속받았기 때문에 MemberRepositoryCustom의 구현 클래스 이름은 해당 Custom 인터페이스를 상속받은 Repository 인터페이스 이름 + Impl로 해야한다.
이렇게 하면 MemberRepository를 통해 MemberRepositoryCustom에 있는 메서드를 사용할 수 있다.
즉, MemberRepository를 통해서 MemberRepositoryCustom인터페이스에 있는 메서드를 사용하려면 MemberRepositoryCustom의 구현 클래스에 대한 객체 생성 및 스프링빈으로 등록되어 있어야한다.
- Spring Data JPA는
public interface MemberRepository extends JpaRepository<Member, Long> { Page<Member> findByAge(int age, Pageable pageable); }개발자는 메서드 시그니처만 정의하고 구현은 따로 하지않는다.
Spring Data JPA는 Repository 인터페이스에 메서드 시그니처만 정의하면, 메서드가 호출될 때 내부적으로 필요한 작업을 수행하고, 선언된 반환 타입에 맞게 값을 리턴해준다.
- 참고로, Spring Data JPA는
Page<T>처럼 반환 타입이 인터페이스인 경우에는, 내부적으로 return new PageImpl<>(content, pageable, total); 처럼 Page인터페이스의 구현체인 PageImpl을 리턴한다.
- content : 현재 페이지 데이터
- pageable : 페이지 정보
- total : 전체 데이터 개수
@column(updatable=false)
private String username;
User user = userRepository.findById(1L).get();
user.setUsername("NewName");
- @PrePersist 메서드는 save()나 persist()를 호출해서, 영속성 컨텍스트에 엔티티를 저장하기 직전에 호출된다.
- @PreUpdate 메서드는 엔티티를 수정해서, 엔티티의 변경 감지가 일어나고 트랜잭션 커밋 또는 em.flush()시점에 UPDATE쿼리가 실행되는데, 이 UPDATE쿼리가 실행되기 직전에 호출된다.
- @PostPersist 메서드는 INSERT쿼리가 실행되고 난 후에 호출된다.
- @PostUpdate 메서드는 UPDATE쿼리가 실행되고 난 후에 호출된다.
Auditing(=스프링데이터 Auditing) : 스프링데이터JPA가 등록날짜/수정날짜/등록자/수정자 값을 자동으로 처리해주는것.
스프링데이터JPA가 등록날짜/수정날짜 값을 자동으로 처리해주는 방법
1. 엔티티에 @EntityListeners(AuditingEntityListener.class)가 있어야한다.
2. 스프링 부트 설정 클래스(@SpringBootApplication)에 @EnableJpaAuditing가 있어야한다.
엔티티의 필드에 @CreatedDate가 있으면, save()나 persist()를 호출해서, 영속성 컨텍스트에 엔티티를 저장하기 직전에 현재시간(LocalDateTime.now())을 @CreatedDate필드에 넣어준다.
엔티티의 필드에 @LastModifiedDate가 있으면, 엔티티를 수정해서, 엔티티의 변경 감지가 일어나고 트랜잭션 커밋 또는 em.flush()시점에 UPDATE쿼리가 실행되는데, 이 UPDATE쿼리가 실행되기 직전에 현재시간(LocalDateTime.now())을 @LastModifiedDate필드에 넣어준다.
스프링데이터JPA가 작성자/수정자 값을 자동으로 처리해주는 방법
1. 엔티티에 @EntityListeners(AuditingEntityListener.class)가 있어야한다.
2. 스프링 부트 설정 클래스(@SpringBootApplication)에 @EnableJpaAuditing가 있어야한다.
3. AuditorAware<String>구현체가 스프링 빈으로 등록되어 있어야 한다.
엔티티의 필드에 @CreatedBy이 있으면, save()나 persist()를 호출해서, 영속성 컨텍스트에 엔티티를 저장하기 직전에 스프링이 내부적으로 AuditorAware.getCurrentAuditor() 메서드를 실행하고, 반환한 Optional안의 실제 값을 꺼내서 @CreatedBy필드에 넣어준다.
엔티티의 필드에 @LastModifiedBy이 있으면, 엔티티를 수정해서, 엔티티의 변경 감지가 일어나고 트랜잭션 커밋 또는 em.flush()시점에 UPDATE쿼리가 실행되는데,
이 UPDATE쿼리가 실행되기 직전에 스프링이 내부적으로 AuditorAware.getCurrentAuditor() 메서드를 실행하고, 반환한 Optional안의 실제 값을 꺼내서 @LastModifiedBy필드에 넣어준다.
@Getmapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findALl(pageable);
return page;
}
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
@Entity
@Getter
public class Item {
@Id @GeneratedValue
private Long id;
}
@Test
public void save() {
Item item = new Item();
itemRepository.save(item);
}
Spring data JPA는 save()를 호출 할때 새로운 엔티티라고 판단하면 em.persist()를 호출하고, 그게 아니면 em.merge()를 호출한다.
새로운 엔티티를 판단하는 기본 전략
하지만 만약에 @GeneratedValue와 같이, 저장할 때 id가 할당되는것이 아니라, @GeneratedValue를 사용하지않고 개발자가 직접 id값을 지정해준 다음에 save()를 하게되면, Spring data JPA는 save()를 할때 새로운 엔티티라고 판단하지않고 em.merge()를 하게된다.
참고로 em.merge()는 해당 엔티티가 이미 DB에 있으면 UPDATE하고, 없으면 em.persist()한다. 그래서 있으면 SELECT 쿼리 호출 후 UPDATE 하고, 없으면 SELECT 쿼리 호출 후 INSERT 한다.
==> 이게 문제이다. 새로운 엔티티라고 판단하지도 않고, SELECT 쿼리를 호출했다가 INSERT 하는것이 문제.
이러한 경우 Persistable 인터페이스를 구현해서 판단 로직을 변경할 수 있다. 새로운 엔티티 판단 기준을 ID가 아닌 isNew()로 제어한다.
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
@Id @GeneratedValue
private String id;
@CreatedDate
private LocalDateTime createdDate;
public Item(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null; // createdDate가 null이면 새로운 엔티티
}
}
Persistable인터페이스를 구현하고, getId()와 isNew()를 재정의한다. getId()의 리턴은 엔티티의 식별자 필드를 반환하면 되고, isNew()의 리턴은 새 엔티티인지 판단기준을 개발자가 직접 주면된다.
이렇게 하게 되면 save()를 호출 할떄 is New()가 먼저 호출되고, isNew()가 true이면 em.persist()를 호출하고, false이면 em.merge()를 하게된다.