- @Repository : MarkerInterface로 특별한 기능은 없음.
- Repository ~ JpaRepository 까지는
@NotRepositoryBean
이 붙어있는 인터페이스이다.
- JpaRepository<Entity,ID> 붙이면 알맞은 프로그래밍 된
SimpleJpaReository
구현체 빈이 등록된다.
- 어떻게? @SpringBootApplication 을 통해 자동으로 붙여지는 @EnableJpaRepositories 의 JpaRepositoriesRegistrar 를 통해서 등록된다.
- JpaRepositoriesRegistrar 는 ImportBeanDefinitionRegistrar 의 구현체이다
- ImportBeanDefinitionRegistrar 는 프로그래밍을 통해 빈을 주입해준다.
- 기존 Repository
@Repository
을 클래스에 붙인다.- 앞서배운 RawJPA의 Repository 기능만 가진 구현체가 생성된다. (DB별 예외처리 등)
- 새로운 JpaRepository
- JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다.
@NotRepositoryBean
된 상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NotRepositoryBean
** = 빈생성 막음)- SpringDataJpa 에 의해 엔티티의 CRUD, 페이징, 정렬 기능 메소드들을 가진 빈이 등록된다. (상위 인터페이스들의 기능)
- 이해를 도와준 블로그 참조 : https://velog.io/@max9106/wtc-learning11
// 변경 전
@Repository
public class UserRepository {
@PersistenceContext
EntityManager entityManager;
public User insertUser(User user) {
entityManager.persist(user);
return user;
}
public User selectUser(Long id) {
return entityManager.find(User.class, id);
}
}
// 변경 후
public interface UserRepository extends JpaRepository<User, Long> {
}
- JpaRepository 에서 사용할 메소드 제한하기
- JpaRepository 는 기본적으로 모든 기능을 제공하기 때문에 리스크가 있을 수 있다.
- 따라서, 아래와 같은 방법으로 원하는 기능 메소드만 구현하도록 제한할 수 있다.
💼 예시: UserRepository가 있다고 할 때, 기능을 제한해주지 않으면 ,findByPassword()등 민감하거나 불필요한 메소드의 선언과 사용이 가능해진다.
💼 두가지의 방법이 있지만, 둘다 내 머리속에 넣기엔 욕심임을 깨닳고 한가지 방법을 명확하게 포스팅하자!
@RepositoryDefinition(domainClass = Comment.class, idClass = Long.class)
public interface CommentRepository {
Comment save(Comment comment);
List<Comment> findAll();
}
- delete() 호출 시 영속성 상태인지 확인한다.
- 영속성 컨텍스트에 없다면(!em.contains(entity))엔티티를 조회해서 영속성 상태로 바꾼다.
-> 삭제할껀데 왜 굳이?
-> CaseCade,OrphanRemoval 에 의한 자식도 삭제가 누락되지 않도록!
- JpaRepository 의 delete() 는 해당 엔티티를 바로 삭제하지 않는다.
- remove() 메소드를 통해 remove 상태로 바꾼다.
public interface MyRepository {
...여기다가 추가할 메소드 선언...
}
@Repository
@Transactional
public class MyRepositoryImpl implements MyRepository {
@Autowired
EntityManager entityManager;
@Override
public void delete(User user) {
entityManager.remove(user);
}
}
- findAll()할 때 이름만 가져오기 커스텀
@Repository
@Transactional
public class MyRepositoryImpl implements MyRepository {
@Autowired
EntityManager entityManager;
@Override
public List<String> findNameAll() {
return entityManager.createQuery("SELECT u.username FROM User AS u", String.class).getResultList();
}
}
- 여기까지 해서 JpaRepository에 대한 개념 정리 ( 틀릴 가능성 높음..! )
- 우리는 보통 JpaRepository<>를 구현해서 Respository를 사용한다.
-> 근데 어떻게 인터페이스에서 상속 받았다고 해서 모든 작업이 이뤄질 수 있을까? 에 대한 고민
-> 위에서 언급하긴 했지만, @SpringBootApplication 동작함으로써 상속 받은 Repository인터페이스의 구현체가 DI되는 것!
-> 그래서 우리가 커스텀한 Repository도 인터페이스로 메소드를 구현해놓고, 구현체를 만들어서 커스텀 해놓으면, Repository에서 extends해서 사용할 수 있는 것!
, MyRepositoryCreate<User> << 커스텀 한 리파지토리를 쓰고 싶다면..
- JpaRepository의존성
- ListPagingAndSortingRepository
- PagingAndSortingRepository : 여기에서 페이징 & 소팅 기능을 제공한다.
1) PageRequest를 사용하여 Pageable에 페이징 정보를 담아 객체화 한다.
2) Pageable을 JpaRepository가 상속된 메서드 T(Entity)와 함께 파라미터로 전달한다.
3) 2번의 메서드의 return으로 Page<T
>가 응답된다.
4) 응답된 Page<T
>에 담겨진 Page정보를 바탕으로 로직을 처리하면 된다.
org.springframework.data.domain.Pageable
PageRequest.of(int page, int size) : 0부터 시작하는 페이지 번호와 개수. 정렬이 지정되지 않음
PageRequest.of(int page, int size, Sort sort) : 페이지 번호와 개수, 정렬 관련 정보
PageRequest.of(int page int size, Sort sort, Direction direction, String ... props) : 0부터 시작하는 페이지 번호와 개수, 정렬의 방향과 정렬 기준 필드들
pageable.getTotalPages() : 총 페이지 수
pageable.getTotalElements() : 전체 개수
pageable.getNumber() : 현재 페이지 번호
pageable.getSize() : 페이지 당 데이터 개수
pageable.hasnext() : 다음 페이지 존재 여부
pageable.isFirst() : 시작페이지 여부
pageable.getContent(), PageRequest.get() : 실제 컨텐츠를 가지고 오는 메서드. getContext는 List<Entity> 반환, get()은 Stream<Entity> 반환
- 저번 프로젝트에도 Page<>형식으로 반환했던 기억이 있다. ( 많은 요소들이 담겨있었음 )
- 게시판 형태의 페이징에서 사용된다.
- 전체 요소 갯수도 함께 조회한다.(
totalElements
)- 응답은 위와 동일
- 더보기 형태의 페이징에서 사용된다.
- 전체 요소 갯수 대신 offset필드로 조회할 수 있음
-> Count쿼리가 따로 발생하지는 않고 limit +1 조회( Offset은 성능이 안좋아서 현업에서 안쓴다고 한다..)
- 전체 목록보기 형태의 페이징에서 사용된다.
- 기본 타입으로 count조회가 발생하지 않는다.
- 컬럼 값으로 정렬하기
Sort sort1 = Sort.by("name").descending(); // 내림차순
Sort sort2 = Sort.by("password").ascending(); // 오름차순
Sort sortAll = sort1.and(sort2); // 2개이상 다중정렬도 가능하다 -> 요거 유용할듯?
Pageable pageable = PageRequest.of(0, 10, sortAll); // pageable 생성시 추가
컬럼이 아닌 값으로 정렬하기
- @Query 사용시 Alias(쿼리에서 as 로 지정한 문구) 를 기준으로 정렬할 수 있다.
// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u, u.password AS user_password FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
// 이렇게 해당 user_password 를 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", Sort.by("user_password"));
- List<>가 필요하면 응답을 Page<>로 받지말고 , List<>로 받아라
-> 전체 count쿼리가 추가로 발생하는 Page<>보다는 List<>가 대용량 처리할 때 더 안정적이고 빠르다.
- SpringData Common의
CRUDRepository
+PagingAndSortRepository
쿼리기능 제공
- 쿼리 실습 예제
// 기본
List<User> findByNameAndPassword(String name, String password);
// distinct (중복제거)
List<User> findDistinctUserByNameOrPassword(String name, String password);
List<User> findUserDistinctByNameOrPassword(String name, String password);
// ignoring case (대소문자 무시)
List<User> findByNameIgnoreCase(String name);
List<User> findByNameAndPasswordAllIgnoreCase(String name, String password);
// 정렬
List<Person> findByNameOrderByNameAsc(String name);
List<Person> findByNameOrderByNameDesc(String name);
// 페이징
Page<User> findByName(String name, Pageable pageable); // Page 는 카운트쿼리 수행됨
Slice<User> findByName(String name, Pageable pageable); // Slice 는 카운트쿼리 수행안됨
List<User> findByName(String name, Sort sort);
List<User> findByName(String name, Pageable pageable);
// 스트림 (stream 다쓴후 자원 해제 해줘야하므로 try with resource 사용추천)
Stream<User> readAllByNameNotNull();
- 기능
- QueryDSL의 Predicate 인터페이스로 조건문을 여러개 구성하여 따로 관리할 수 있다.
findOne(Predicate)
,findAll(Predicate)
가 주로 사용된다.- findOne() = Optional<>리턴
- findALL() = List<> , Page<> , Iteralbe<> , Slice<> 리턴
- 장점
- 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
- 자동완성 등, IDE의 도움을 받을 수 있다.
- 동적인 쿼리 작성이 편하다.
- 쿼리 작성시 제약 조건등을 메서드 추출을 통해 재사용 할 수 있다.( 아마 이게 좀 편리할듯? )
- 원리
QueryDSL의존성을 추가
하면 SpringData에 의해QueryDslPredicateExecutor
인터페이스가 추가된다.QueryDslPredicateExecutor
는Repository가 QueryDsl 을 실행
할 수 있는 인터페이스를 제공하는 역할을 한다.
- Spring 2.X버전 까지는 QueryDSL빌드 Task를 따로 설정해줘야했다.
plugins {
...
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
...
querydsl.extendsFrom compileClasspath
}
def querydslSrcDir = 'src/querydsl/generated'
querydsl {
library = "com.querydsl:querydsl-apt"
jpa = true
querydslSourcesDir = querydslSrcDir
}
sourceSets {
main {
java {
srcDirs = ['src/main/java', querydslSrcDir]
}
}
}
project.afterEvaluate {
project.tasks.compileQuerydsl.options.compilerArgs = [
"-proc:only",
"-processor", project.querydsl.processors() +
',lombok.launch.AnnotationProcessorHider$AnnotationProcessor'
]
}
dependencies {
implementation("com.querydsl:querydsl-jpa") // querydsl
implementation("com.querydsl:querydsl-apt") // querydsl
...
}
- Spring 3.X버전은 의존성만 추가해줘도 빌드에 자동으로 포함되서 실행된다.
// application.yml or properties
dependencies {
....
// 9. QueryDSL 적용을 위한 의존성 (SpringBoot3.0 부터는 jakarta 사용해야함)
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
QuerydslPredicateExecutor<Channel>
의존성 추가
public interface ChannelRepository extends JpaRepository<Channel, Long>, QuerydslPredicateExecutor<Channel> {
}
- QueryDSL TestCode
@Test
void queryDslTest() {
// given
var newChannel = Channel.builder().name("sanghoon").build();
channelRepository.save(newChannel);
Predicate predicate = QChannel.channel.name.equalsIgnoreCase("SANGHOON");
// when
Optional<Channel> optional = channelRepository.findOne(predicate);
// then
assert optional.get().getName().equals(newChannel.getName());
}
- 간단하게 요약하자면, 조건( 어떤 케이스를 찾아온다거나 등.. ) 의 Predicate를 만들어서,
find(Predicate)를 하면 된다.- join이 필요한 쿼리일경우 불가능!!!
- join쿼리는 없는대신 조건이 많은경우 ( 가능! )
@Service
public class ThreadServiceImpl implements ThreadService {
@Autowired
ThreadRepository threadRepository;
@Override
public List<Thread> selectNotEmptyThreadList(Channel channel) {
var thread = QThread.thread;
// 메세지가 비어있지 않은 해당 채널의 쓰레드 목록
var predicate = thread
.channel.eq(channel)
.and(thread.message.isNotEmpty());
var threads = threadRepository.findAll(predicate);
return IteratorAdapter.asList(threads.iterator());
}
@Override
public Thread insert(Thread thread) {
return threadRepository.save(thread);
}
}
- TestCode
@Test
void getNotEmptyThreadList() {
// given
var newChannel = Channel.builder().name("c1").type(Type.PUBLIC).build();
var savedChannel = channelRepository.save(newChannel);
var newThread = Thread.builder().message("message").build();
newThread.setChannel(savedChannel);
threadService.insert(newThread);
var newThread2 = Thread.builder().message("").build();
newThread2.setChannel(savedChannel);
threadService.insert(newThread2);
// when
var notEmptyThreads = threadService.selectNotEmptyThreadList(savedChannel);
// then 메세지가 비어있는 newThread2 는 조회되지 않는다.
assert !notEmptyThreads.contains(newThread2);
}
- 저번 프로젝트 때 썼던 TimeStamp를 생각해보자!
@CreatedDate
private Date created;
@LastModifiedDate
private Date updated;
@CreatedBy
@ManyToOne
private Account createdBy;
@LastModifiedBy
@ManyToOne
private Account updatedBy;
1) 메인 Application에 @EnableJpaAuditing추가
@EnableJpaAuditing
@SpringBootApplication
public class Application {...}
2) 엔티티 클래스위에 @EntityListeners(AuditingEntityListener.class)추가
@Getter
@MappedSuperclass // 다른 엔티티에서 사용할 것임에 필수!
@EntityListeners(AuditingEntityListener.class)
public class TimeStamp {
@CreatedDate
private LocalDateTime createdAt;
@CreatedBy
@ManyToOne
private User createdBy;
@LastModifiedDate
private LocalDateTime modifiedAt;
@LastModifiedBy
@ManyToOne
private User modifiedBy;
}
- 3)
@CreatedAt
,@ModifiedAt
은 구현체 없이 잘 동작하지만,@CreatedBy
,@ModifiedBy
는 구현체가 있어야 값이 들어감 -> 구현체 없으면 해당 필드 null로 들어가는 것 확인.
3-1) 구현체 만들기( AuditorAware )
-> 보통 누구에 의해서 만들어졌냐? , 누구에 의해서 수정됐냐? 는UserDetails
즉,Authentication
에서 빼온 정보를 넣어준다.
@Service
public class UserAuditorAware implements AuditorAware<User> {
@Override
public Optional<User> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
return Optional.of(((UserDetailsImpl) authentication.getPrincipal()).getUser());
}
}
@EnableJpaAuditing(auditorAwareRef = "userAuditorAware") // auditorAware 의 빈이름을 넣어준다.
@SpringBootApplication
public class Application {
- 생성일시, 생성자, 수정일시, 수정자는 결국 엔티티의 영속성이 변경될 때 저장한다.
- 엔티티의 영속성이 변경되는 생성 > 수정 > 삭제 이 흐름을 엔티티 라이프 사이클 이벤트라 한다.
@PostConstruct
: 객체가 생성되면 자동으로 실행되는 메소드에 붙이는 이것의 원리와 같다.
- 엔티티 저장 이벤트
전 :@PrePersist
: EntitiyManager가 엔티티를 영속성 상태로 만들기 직전에 메소드 수행
후 :@PostPersist
: EntitiyManager가 엔티티를 영속성 상태로 만든 직후에 메소드 수행
- 엔티티 수정 이벤트
전 :@PreUpdate
: EntitiyManager가 엔티티를 갱신상태로 만들기 직전에 메소드 수행
후 :@PostUpdate
: EntitiyManager가 엔티티를 갱신상태로 만든 직후에 메소드 수행
- 엔티티 삭제 이벤트
전 :@PreRemove
: EntitiyManager가 엔티티를 삭제 상태로 만들기 직전에 메소드 수행
후 :@PostRemove
: EntitiyManager가 엔티티를 삭제 상태로 만든 직후에 메소드 수행
- 이것들을 활용해서
@CreatedAt
,@ModifiedAt
를 구현해보자
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamp {
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public void updateCreatedAt() {
this.createdAt = LocalDateTime.now();
}
public void updateModifiedAt() {
this.modifiedAt = LocalDateTime.now();
}
}
// Entity 내 정의
/**
* 라이프 사이클 메소드
*/
@PrePersist
public void prePersist() {
super.updateModifiedAt();
super.updateCreatedAt();
}
@PreUpdate
public void PreUpdate() {
super.updateModifiedAt();
}
- 💁♂️ 여기서 과제!!!
- ContextHolder + 엔티티 라이프 사이클 이벤트(@PrePersist, @PreUpdate) 를
- 사용해서 createdBy, modifiedBy 를 구현해볼까요?
- TestCode로 유저 저장될 때 인증객체 만들었음.( 사실 로그인이 맞겠지만 그것까지 테스트에서 신경쓸 필요는 없으니.. ! )
@Test
void contextHolderLifeCycleTest(){
var newUser = User.builder().username("new").password("pass").build();
//var newUser2 = User.builder().username("modified").password("pass").build();
var savedUser = userRepository.save(newUser);
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(newUser,null);
Authentication authentication = token;
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
var newChannel = Channel.builder().name("sanghoon").build();
var newChannel2 = Channel.builder().name("sanghoon modified").build();
var savedChannel = channelRepository.save(newChannel);
savedChannel.updateChannel(newChannel2);
}
@Getter
@MappedSuperclass // 엔티티에 매핑하기 위해서
@EntityListeners(AuditingEntityListener.class)
public class TimeStamp {
@CreatedBy
@ManyToOne
private User createdBy;
@LastModifiedBy
@ManyToOne
private User modifiedBy;
public void updateCreatedBy(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
this.createdBy = null;
}
this.createdBy = (User) authentication.getPrincipal();
}
public void updateModifiedBy(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
this.modifiedBy = null;
}
this.modifiedBy = (User) authentication.getPrincipal();
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class Channel extends TimeStamp {
...// channel을 만들거나, 수정할 때를 테스트할 것 임므로 Chaanel Entity를 사용했음.
@PrePersist
public void prePersist(){
super.updateCreatedAt();
super.updateModifiedAt();
super.updateCreatedBy();
}
@PreUpdate
public void preUpdate(){
super.updateModifiedAt();
super.updateModifiedBy();
}
}