MappedBy : 누가 fk를 가지고 있나.(mappedBy 옵션을 주지 않으면 mapper 테이블이 생성되고, mappedBy 옵션을 주면 alter 테이블로 처리된다.)
연관 관계의 주인 == 외래키 관리자 == fk를 가지고 있는 녀석.
(추가하기)
SQL을 추상화한 객체 지향 쿼리.(특정 DB에 의존적이지 않다) 테이블을 기준으로 쿼리를 날리는 것이 아닌 엔티티 객체를 기준으로 쿼리를 날림
ex. select m from Member as m
영속성 컨텍스트가 메모리 상에 캐싱하기 때문. 조회 시 영속성 컨텍스트의 1차 캐시를 찾아보고 없으면 DB에 접근해서 찾는다. DB에 접근하게 되면, 1차 캐시에도 저장해준다.
1차 캐시는 키 값을 id로 가진 객체들이 저장된다.
따라서 findById는 1차 캐시에서 찾아볼 수 있다. 하지만 findByName은 name을 키 값으로 가진 객체가 없기 때문에 DB에서 조회한다. findByName은 JPQL을 사용하는 것이다.
JPQL, ID Generator가 사용되면 내부적으로 flush가 일어난다.
@Test
void update() {
// 트랜잭션 락
Station station1 = stationRepository.save(new Station("잠실역"));
station1.changeName("몽촌토성역");
// 트랜잭션 flush(JPQL, ID Generator이 동작할 때 ‘플러시(flush)’ 일어남)
Station station2 = stationRepository.findByName("몽촌토성역"); // JPQL
assertThat(station2).isNotNull();
// 트랜잭션 커밋
// 트랜잭션 언락(언락 되기 전까지는 DB반영이 아니라, 특정 공간에 로그가 쌓임. 중간에 오류 발생시 로그 다 날려서 롤백)
}
아래 예시에는 findByName이 사용되기 전 flush가 먼저 일어난다.
Jpa를 사용할 때, JpaRepository<엔티티, ID타입>
를 상속해서 Repository를 만든다.
JpaRepository
내부에 보면 클래스 레벨에 @NoRepositoryBean
가 붙어져 있다. JpaRepository
뿐만 아니라, 그 부모 클래스인 PagingAndSortingRepository
, CrudRepository
에도 @NoRepositoryBean
가 존재한다.
공식 문서에 보면 이렇게 나와있다.
Annotation to exclude repository interfaces from being picked up and thus in consequence getting an instance being created.
This will typically be used when providing an extended base interface for all repositories in combination with a custom repository base class to implement methods declared in that intermediate interface. In this case you typically derive your concrete repository interfaces from the intermediate one but don't want to create a Spring bean for the intermediate interface.
이 어노테이션이 붙어있으면, 실제 프록시 빈으로 등록하지 않게 해주는 어노테이션이라고 한다. 그래서 중간 단계의 Repository 들을 빈으로 등록하지 않으려고 붙여져있다.
JpaRepository
를 상속하면, Spring Data JPA가 인터페이스에 대해서 프록시 구현체를 만든 뒤, 구현체를 주입해준다.(의존성 주입)
spring data JPA는 스프링이 만들어서 제공하는 컴포넌트이므로, @Repository를 생략해도 JPA 관련 예외를 스프링 예외로 변환해서 제공해준다.
또 JpaRepository를 상속하면 @Repository가 없어도 빈으로 등록되는데, 이는 컴포넌트 스캔에 의해 빈으로 동작하는 것이 아니라, spring data에서 해당 인터페이스를 구현한 클래스를 찾아서 사용한다. 실제로는 인터페이스를 구현한 클래스를 바로 사용하는게 아니라, 스프링이 동적으로 임의의 구현 클래스를 생성하고, 내가 구현한 클래스를 연결해준다.
참고
JpaRepository나 CrudRepository 같은 인터페이스에 @Repository 어노테이션이 안붙는 이유에 대해서 궁금합니다.
[JPA] OSIV(Open Session In View)
@Transactional이 붙은 하나의 서비스 메서드가 있다고 하자. 이런 경우 트랜잭션이 시작되면 락이 걸리고, 언락이 되기 전에는 db에 반영되는 것이 아니라 특정 공간에 로그를 쌓는다. 트랜잭션 메서드가 성공적으로 끝나면 언락이 되면서 DB에 반영된다.
하지만 메서드 중간에 JPQL(ex. findByName)이나, 저장시 Identity 타입으로 Id를 생성하는 경우는 로그를 쌓는 것만으로는 부족하지 않을까 라는 의문이 들었다. 왜냐하면 이렇게 중간에 flush가 일어나야하는 경우 (Identity 타입으로 ID를 생성하는 경우를 예로 들면) ID를 생성하기 위해서는 DB에 반영이 되어야하기 때문이다.
제이슨에게 물어봤는데 db마다 처리 방식이 다르고, undo 영역을 공부해야한다고 하셨다.
일단 jpa 공부할 때는 그냥 flush 되면 실제 db에 반영된다고 생각하면 된다고한다.(그래서 실제 DB에 반영되기 때문에 auto increment는 롤백 되지 않는 것이다.)
Flush : db에 반영, 롤백 가능
Commit : 롤백 불가능
(추가하기)
(추가하기)
1:다 && 다:1 양방향 매핑을 해줄때 OneToMany(slave) 쪽에 mappedBy로 주인을 표기해줘야 쓸모없는 테이블이 생기지 않는다. 이때 convenience method 주의 하기. 주인쪽엔 무조건 추가 or 삭제를 해줘야하고(테이블에 반영하기 위해), 객체에서 동기화를 위해 slave 쪽에도 직접 추가, 삭제 해주는 것이 좋음.
(추가하기)
(추가하기)
(추가하기)
(추가하기)
(추가하기)
(추가하기)
세션은 확장하기 불편하다. 서버가 늘어나면, 서버마다 세션 정보를 관리하는 저장소를 가지거나, 공통으로 세션 정보를 관리하는 저장소가 필요한데, 서버마다 관리하면 동기화에 있어 비용이 들고, 공통으로 관리해주면 별도의 저장소를 만들어야하기 때문에 비용이 든다. 따라서 세션은 확장성이 좋지 않다.
(추가하기)
-> https://jojoldu.tistory.com/296
(추가하기)
h2 url을 적어줄때 MODE=MySQL을 추가해주고, dialect 옵션을 준다.
spring.datasource.url=jdbc:h2:~/test;MODE=MySQL;
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
but, 이렇게 해주면 h2 1.4.200 버전에서는 테스트 코드에서 에러가 발생한다. 프로덕션 실행은 문제가 없었다.. 스프링부트가 알아서 1.4.200버전을 넣어준 것인데, 왜 이런 문제가 발생할까.. 1.4.199 버전으로 낮추니까 테스트 코드도 문제없이 돌아간다..
제이슨은 이 경우, 다운그레이드할 필요 없이, @AutoConfigureTestDatabase(replace = Replace.NONE) 설정을 해주면 된다고 하셨다.
junit의 @Nested 클래스는 롤백이 보장되지 않는 것 같다.. (?) 아닌 것 같기도 하다.
(추가하기)
Oauth 로그인을 구현하면서 findByOauthId를 통해 유저를 조회해오고 없으면 새로 유저를 save하는 기능이 있었다.
그 기능을 아래와 같이 처리했다.
Member member = memberRepository.findByOauthId(userProfile.getOauthId())
.orElse(memberRepository.save(userProfile.toMember()));
그러나 DB를 살펴보니, 중복 저장도 되고, 에러도 발생했다...
이유는 orElse에 있었다. orElse는 empty든 아니든 무조건 실행하는 녀석이었고, orElseGet은 empty일 때만 실행되는 녀석이었다.
따라서 아래와 같이 orElseGet으로 바꿔주니 잘 동작했다.
Member member = memberRepository.findByOauthId(userProfile.getOauthId())
.orElseGet(() -> memberRepository.save(userProfile.toMember()));
(추가하기)
JPA 관련해서 공부하고 있는데, 너무 잘 정리되어있네요:)
공유 감사합니다😎