ORM이란
Object Relational Mapping, 객체-관계 매핑
JPA 및 h2-database를 사용하기 위해서는 아래와 같은 dependency가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
generate-ddl: false
hibernate:
ddl-auto: create
defer-datasource-initialization: true
show-sql
: sql 사용 로그 출력format_sql
: sql 사용을 보다 정리해줌ddl-auto
: spring 실행시 table을 create 한다.defer-datasource-initialization
: Spring 2.5 부터 resouce의 sql을 spring 생성시 초기화 하지 않기 때문에 초기화 하기 위해서 true
로 설정해준다.spring:
h2:
console:
enabled: true
public interface MemberRepository extends JpaRepository<Member, Long> {
}
JpaRepository
를 상속하여 사용
@Test
void crud(){ // create, read, update, delete
System.out.println(">>> name 을 기준으로 내림차순 출력");
List<Member> members1 = memberRepository.findAll(Sort.by(Sort.Direction.DESC, "name"));
members1.forEach(System.out::println);
System.out.println(">>> ID 가 1, 3, 5번인 Member 출력");
List<Member> members2 = memberRepository.findAllById(Lists.newArrayList(1L, 3L, 5L));
members2.forEach(System.out::println);
System.out.println(">>> 멤버 삽입");
Member member1 = new Member("duckbill", "duckbill@gmail.com");
Member member2 = new Member("pypy", "pypy@gmail.com");
memberRepository.saveAll(Lists.newArrayList(member1, member2));
memberRepository.findAll().forEach(System.out::println);
System.out.println("Member 중 ID 1 멤버 조회");
Member member = memberRepository.findById(1L).orElse(null);
System.out.println(member);
}
@Test
void countAndExists(){
long count = memberRepository.count();
System.out.println(count);
boolean exists = memberRepository.existsById(1L); // count 를 참조
System.out.println(exists);
}
@Test
void delete(){ // FEAT: n개의 delete 에 대해 n번의 delete 명령 발생
memberRepository.delete(memberRepository.findById(1L).orElseThrow(RuntimeException::new));
memberRepository.findAll().forEach(System.out::println);
}
delete
에서 null 이 들어오면 에러가 발생하므로 orElseThrow
를 활용delete
에 대해 n번의 delete
명령 발생@Test
void deleteInBatch(){ // FEAT: n개의 delete 에 대해 1번의 delete 명령 발생 (or 명령)
memberRepository.deleteAllByIdInBatch(Lists.newArrayList(1L, 3L));
memberRepository.findAll().forEach(System.out::println);
}
delete
에 대해 n번의 delete
명령 발생데이터가 많을 때 Paging을 이용하여 데이터를 요구할 수 있다.
@Test
void paging(){ // 페이징 기법
Page<Member> members = memberRepository.findAll(PageRequest.of(0, 3)); // 크기가 3인 페이지의 0번쨰 장
System.out.println("page: " + members);
System.out.println("totalElements: " + members.getTotalElements());
System.out.println("totalPages: " + members.getTotalPages());
System.out.println("numberOfElements: " + members.getNumberOfElements());
System.out.println("sort: " + members.getSort());
System.out.println("size: " + members.getSize());
members.stream().forEach(System.out::println);
}
Page
interface 활용Query by example : entity를 example로 만들고 mathcer을 추가하여 쿼리를 생성하고 검색하는 방식
@Test
void qbe(){ // MEMO: query by example : entity 를 example 로 만들고 matcher 를 추가하여 쿼리를 생성
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("name") // name 은 무시
.withMatcher("email", endsWith()); // email 은 뒷 부분 검색
Example<Member> example = Example.of(new Member("ma", "gmail.com"), matcher);
memberRepository.findAll(example).forEach(System.out::println);
}
name
: Table을 생성할 때 이름을 변경할 수 있다. 일반적으로는 잘 변경하지 않는다.매핑 전략GenerationType.AUTO
는 선택한 데이터베이스 방언에 따라 IDENTITY
, SEQUENCE
, TABLE
전략 중 하나를 자동으로 선택한다.
기본키 매핑 전략
SEQUENCE
: 데이터베이스 시퀸스에서 식별자 값을 획득한 수 영속성 컨텍스트에 저장 (Oracle)IDENTITY
: 데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다. (IDENTITY 전략은 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.) (MySQL)TABLE
: 데이터베이스 시퀸스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장name
: 데이터베이스의 Column과 Object name을 별도로 매핑하기 위해서 사용nullable
: default는 null 이지만 false로 두면 not null로 테이블을 생성하게 된다.unique
: column 하나만 unique 키로 둘때 사용length
: default=255 Varchar 등의 크기를 결insertable
/ updatable
: DDL 뿐만 아니라 DML에도 영향을 끼친다. 값이 false
이면 insert / update 시에 반영되지 않는다.columnDefinition(DDL)
: 데이터베이스 컬럼 정보를 직접 줄 수 있다.해당 어노테이션을 붙인 Column은 영속성 처리에서 제외되기 때문에 DB에 반영되지 않는다. 객체에서만 사용된다.
@Transient
private String testData;
자바의 enum 타입을 매핑할 때 사용한다.
Enum class의 생성
public enum Gender {
MALE,
FEMALE
}
Enum의 경우 일반적으로 DB에 저장될때 순서대로 숫자를 부여하여 저장된다.(위의 경우 MALE=0, FEMALE=1) 하지만 이런 경우 MALE의 앞에 BABY와 같이 새로이 생성된다면 전부 틀어지게 된다. 위와 같은 경우를 대비하여 @Enumerated(value=EnumType.STRING)
으로 String 형태로 저장될 수 있게 해주는 것이 안전하다.
날짜 타입 (java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.
TemporalType.DATE
: 날짜, 데이터베이스 date 타입과 매핑 (예: 2023-10-10)TemporalType.TIME
: 시간, 데이터베이스 time 타입과 매핑 (예: 15:02:11)TemporalType.TIMESTAMP
: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (예: 2023-10-10 15:02:11)이 필드는 매핑하지 않는다. 따라서, 데이터베이스에 저장하지 않고 조회하지도 않는다. 임시로 어떤 값을 보관하고 싶을 때 사용한다.
@Transient
private Integer count;
Entity listener 에는 다음과 같이 7가지가 있다.
@PrePersist
: insert 메소드가 호출되기 전에 실행 @PrePersist
를 이용한 createdAt, updatedAt 초기화@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
: Update Method 호출 전 실행 @PreUpdate
를 이용한 updatedAt 업데이트@PreUpdate
public void preUpdate(){
this.updatedAt = LocalDateTime.now();
}
@PreRemove
: Remove Method 호출 전 실행@PostPersist
: insert Method 호출 후 실행@PostUpdate
: update Method 호출 후 실행@PostRemove
: remove Method 호출 후 실행@PostLoad
: select 조회가 일어난 이후 실EntityListener 클래스 생성
public class MyEntityListener {
@PrePersist
public void prePersist(Object o){
if (o instanceof Auditable){
((Auditable) o).setCreatedAt(LocalDateTime.now());
((Auditable) o).setUpdatedAt(LocalDateTime.now());
}
}
@PreUpdate
public void preUpdate(Object o){
if (o instanceof Auditable){
((Auditable) o).setUpdatedAt(LocalDateTime.now());
}
}
}
Auditable Interface 생성
public interface Auditable {
LocalDateTime getCreatedAt();
LocalDateTime getUpdatedAt();
void setCreatedAt(LocalDateTime createdAt);
void setUpdatedAt(LocalDateTime updatedAt);
}
interface를 이용하여 Entity 클래스의 기능 정의
Entity에 Entity Listener 등록
@Entity
@EntityListeners(value = MyEntityListener.class)
public class Book implements Auditable
Member 클래스 저장을 위하여 Clone 클래스 생성
MemberHistory class
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class MemberHistory implements Auditable{
@Id
@GeneratedValue
private Long id;
private Long memberId;
private String name;
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
MemberHistoryRepository 생성
MemberEntityListener 클래스 생성
PrePersist
, PreUpdate
사용
public class MemberEntityListener {
// FIXME: EntityListener 은 spring bean 을 주입받지 못한다.
// @Autowired
// private MemberHistoryRepository memberHistoryRepository;
@PrePersist
@PreUpdate
public void prePersistAndUpdate(Object o){
MemberHistoryRepository memberHistoryRepository = BeanUtils.getBean(MemberHistoryRepository.class);
Member member = (Member) o;
MemberHistory memberHistory = MemberHistory.builder()
.memberId(member.getId())
.name(member.getName())
.email(member.getEmail())
.createdAt(member.getCreatedAt())
.updatedAt(member.getUpdatedAt())
.build();
memberHistoryRepository.save(memberHistory);
}
}
memberHistoryRepository
를 주입하여야 한다.ApplicationContextAware
를 활용하여 주입할 수 있다.BeanUtils 클래스 생성 (ApplicationContextAware
)‼️
주입하고자하는 class 이름을 입력받으면 해당하는 클래스를 리턴
@Component
public class BeanUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
BeanUtils.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
}
Member의 EntityListener에 MemberEntityListener.class
추가
@EnableJpaAuditing
어노테이션 부착
@SpringBootApplication
@EnableJpaAuditing
public class JpaRepositoryApplication {
public static void main(String[] args) {
SpringApplication.run(JpaRepositoryApplication.class, args);
}
}
Test
파일에서는 JPA가 잘 동작하지 않으므로 JpaConfig
와 같은 클래스를 생성하고 @EnableJpaAuditing
어노테이션을 부착하는 것이 좋다.EntityListener에 AuditingEntityLister.class
추가
@EntityListeners(value = AuditingEntityListener.class)
@CreatedDate
, @LastModifiedDate
를 사용하여 날짜 추가
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
지금까지의 경우 createdAt, updatedAt을 사용하는 클래스 모두에 해당 필드를 생성하여 초기화 해주었다. 지금부터는 BaseEntity 클래스를 생성하여 createdAt, updatedAt 필드를 생성하고 BaseEntity를 상속하는 방식으로 진행해 볼 수 있다.
BaseEntity
클래스 생성
@Data
@MappedSuperclass // 해당 클래스의 필드를 상속받는 클래스의 필드로 추가
@EntityListeners(value = AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
@MappedSuperClass
: 해당 클래스의 필드를 상속받는 클래스의 필드로 추가하위 클래스에서 BaseEntity 클래스의 필드를 제대로 이용하기 위해서 super 클래스 호출에 대해 재정의 해주어야 한다.
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Member extends BaseEntity implements Auditable {
@ToString
으로 toString 사용시에 상위 클래스 필드의 내용도 출력하게 해야한다.@EqualsAndHashCode
: 관련 필드를 기준으로 모든 개체에 의해 상속된 equals 및 hashCode 메서드에 대한 구현을 생성합니다. (callSuper=true)를 통해 하위 클래스에서 상위 클래스의 필드도 생성해준다.추가적으로 BaseEntity
만 Auditable
을 implements
해주면 하위 클래스는 자동적으로 상속 받게 되므로 삭제해도 무관하다.
@Test
void crud(){ // create, read, update, delete
System.out.println(">>> name 을 기준으로 내림차순 출력");
List<Member> members1 = memberRepository.findAll(Sort.by(Sort.Direction.DESC, "name"));
members1.forEach(System.out::println);
System.out.println(">>> ID 가 1, 3, 5번인 Member 출력");
List<Member> members2 = memberRepository.findAllById(Lists.newArrayList(1L, 3L, 5L));
members2.forEach(System.out::println);
System.out.println(">>> 멤버 삽입");
Member member1 = new Member("duckbill", "duckbill@gmail.com");
Member member2 = new Member("pypy", "pypy@gmail.com");
memberRepository.saveAll(Lists.newArrayList(member1, member2));
memberRepository.findAll().forEach(System.out::println);
System.out.println("Member 중 ID 1 멤버 조회");
Member member = memberRepository.findById(1L).orElse(null);
System.out.println(member);
}