<스프링 데이터 JPA 구현체>
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
}
엔티티에서 @GenerateValue로 식별자 생성할 경우 save() 호출 시점에 식별자가 없으므로 null로 판단해 (원시 타입은 0) 새로운 엔티티로 인식해서 persist로 정상적으로 저장된다.
그러나 @Id 만 사용해서 직접 할당을 하게 될 경우 이미 식별자 값이 있는 상태로 save() 를 호출하므로 merge() 가 호출된다. merge() 는 우선 DB에서 조회 후, 값이 없으면 새로운 엔티티로 인지해서 저장하므로 쿼리가 늘어나 비효율적이다.
따라서 id를 직접 할당할 일이 있을 경우 Persistable 를 사용해서 새로운 엔티티 확인 여부를 직접 구현하는게 효과적이다.
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
@Id
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 )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다. (@CreatedDate에 값이 없으면 새로운 엔티티로 판단)
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
수정 쿼리를 직접 날리면 영속성 컨텍스트를 무시하고 실행하기 때문에, 한 트랜잭션 내의 영속성 컨텍스트에 엔티티가 있는 상태에서 벌크 연산 후 추가 조회를 하면 디비의 수정 값이 아닌 이전의 영속성 컨텍스트 값이 조회된다.
따라서 추가 조회를 하려면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
//패치 조인
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
readonly -> 디비에서 값 조회 후 영속성 컨텍스트에 값 저장 x
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",value = "true")},
forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
동시성 문제 해결
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적 (등록일 수정일 등록자 수정자)
-설정
@EnableJpaAuditing - 스프링 부트 설정 클래스에 적용
@EntityListeners(AuditingEntityListener.class) - 엔티티에 적용
-사용 어노테이션
@CreatedDate
@LastModifiedDate
@CreatedBy
@LastModifiedBy
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
public class Member extends BaseEntity {}
-> 등록시간, 수정시간은 필요하지만 등록자,수정자는 필요없을 수도 있다.
그래서 다음과 같이 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.
등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
//세션 정보나, 스프링 시큐리티 로그인 정보에서 ID 받기
}
}