Spring Data JPA (1)

라이라·2023년 6월 28일
0

JPA란?

Java Persistence API는 자바로 영속 영역을 처리하는 API라고 해석할 수 있다.

JPA의 상위 개념은 ORM(object Relational Mapping)이라는 패러다임으로 이어지는데 이는 객체지향으로 구성한 시스템을 관계형 데이터베이스에 매핑하는 패러다임이다.

JPA가 스프링과 연동할 때 Spring Data JPA라는 라이브러리를 사용한다.

JPA를 이용하는 개발의 핵심은 객체지향을 통해서 영속 계층(Persistence Layer - 데이터 저장 부분)을 처리하는데에 있다.

따라서 JPA를 이용할 때는 테이블과 SQL을 다루는 것이 아닌 데이터에 해당되는 객체를 엔티티 객체라는 것으로 다루고 JPA로 이를 DB와 연동해서 관리하게 된다.

엔티티 객체는 쉽게 말해 PK(기본키)를 가지는 자바의 객체이다.
엔티티 객체는 고유의 식별을 위해 @Id를 이용해서 객체를 구분하고 관리한다.

엔티티 객체를 생성하기 위해서는 엔티티 클래스를 정의해야한다.
엔티티 클래스는 반드시 @Entity가 존재하고 구분을 위해 @Id가 필요하다.

	@Entity  
    public class Entity01{
    
		@Id
 		@GeneratedValue(strategy=GenerationType.IDENTITY)
    	private Long bno;
        ...
        
        }

GenerationType.#{}

  • IDENTITY : 데이터베이스에 위임한다. (auto-increment)
  • SEQUENCE : DB 시퀀스 오브젝트 사용 - @SequenceGenerator 필요
  • TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용 - @TableGenerator 필요
  • AUTO : 방언에 따라 자동 지정, 기본값

DB의 거의 모든 테이블에서 데이터가 추가된 시간이나 수정된 시간 등이 칼럼으로 작성된다.
자바에서는 이러한 것들을 쉽게 처리하기 위해서 @MappedSuperClass를 이용해서 공통으로 사용되는 칼럼들을 지정하고 해당 클래스를 상속해서 처리한다.

	@MappedSuperclass
	@EntityListeners(value = {AuditingEntityListener.class})
	@Getter
	abstract class BaseEntity {
    	@CreatedDate
      	@Column(name = "regDate", updatable = false)
      	private LocalDateTime regDate;

      	@LastModifiedDate
      	@Column(name="modDate")
      	private LocalDateTime modDate;
  }

여기서 Spring Data JPA의 AuditingEntityListener를 지정해주었는데 이것은 엔티티가 DB에 추가되거나 변경될 때 자동으로 시간값을 지정할 수 있게 해준다.

AuditingEntityListener를 활성화 하기 위해서는 프로젝트 설정에 @EnableJpaAuditing을 추가해줘야한다.

	@SpringBootApplication
    @EnableJpaAuditing
    public class BootProject01Application {

        public static void main(String[] args) {
            SpringApplication.run(BootProject01Application.class, args);
        }

    }

기존에 있던 클래스에 공통사항을 모아둔 Entity를 상속하게하고 어노테이션 변경

	@Entity
    @Getter
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public class Board extends BaseEntity{
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long bno;
        @Column(length = 500, nullable = false) //칼럼의 길이와 null 허용여부
        private String title;
        @Column(length = 2000, nullable = false)
        private String content;
        @Column(length = 50, nullable = false)
        private String writer;
    }

JpaRepository 인터페이스

spring data JPA를 이용할 때는 JpaRepository 인터페이스를 이용해 인터페이스 선언만으로 DB관련 작업을 처리할 수 있다.

JpaRepository 인터페이스를 상속하는 인터페이스를 선언하는 것으로 CRUD와 페이징 처리가 완료된다.

public interface BoardRepository extends JpaRepository<Board, Long> {}

이 경우 BoardRepository 내부에 아무런 코드가 없기 때문에 확장문제가 일어날 수 있으나 쿼리 메서드(메서드 이름이 쿼리가 되는 기능)나 Querydsl을 이용해 해결가능하다.

Insert()

DB에 insert를 실행하는 기능은 JpaRepository.save()를 통해 이루어진다.
save()는 현재 영속 컨텍스트 내에 데이터가 존재하는지 찾아보고 있다면 update를, 없다면 insert를 자동실행한다.

	@Autowired
    private BoardRepository repository;

    @Test
    public void testInsert(){
        IntStream.rangeClosed(1, 100).forEach(v->{
            Board board = Board.builder()
                    .title("title..." + v)
                    .content("content..."+ v)
                    .writer("user"+ (v % 10))
                    .build();

            Board result = repository.save(board);
            log.info("BNO : " + result.getBno());
        });
    }

Select()

특정 번호의 게시물을 조회하는 기능은 findById()를 이용해 처리하고 findById()의 리턴타입은 Optional< T >이다.

	@Test
    public void testSelect(){
        Long bno = 100L;

        Optional<Board> result = repository.findById(bno);
        Board board = result.orElseThrow();

        log.info(board);
    }

update()

update기능은 insert와 동일하게 save()를 통해서 처리된다.
또한 update는 등록 시간이 필요하기 때문에 가능하면 findById()로 가져온 객체를 이용해서 약간의 수정을 통해 처리하도록 한다.

	public class Board extends BaseEntity{
    ...
    public void change(String title, String content){
		this.title = title;
        this.content = content;
        }
    }
    
    @Test
    public void testUpdate(){
        Long tno = 100L;
        Optional<Board> result = repository.findById(tno);
        Board board = result.orElseThrow();
        board.change("update... title " + tno, "update content " + tno);
        repository.save(board);
        log.info(board);
    }
    

테스트 결과

(1)
	Hibernate: 
    select
        board0_.bno as bno1_0_0_,
        board0_.mod_date as mod_date2_0_0_,
        board0_.reg_date as reg_date3_0_0_,
        board0_.content as content4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer as writer6_0_0_ 
    from
        board board0_ 
    where
        board0_.bno=?
        
(2)
Hibernate: 
    select
        board0_.bno as bno1_0_0_,
        board0_.mod_date as mod_date2_0_0_,
        board0_.reg_date as reg_date3_0_0_,
        board0_.content as content4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer as writer6_0_0_ 
    from
        board board0_ 
    where
        board0_.bno=?
        
(3)
Hibernate: 
    update
        board 
    set
        mod_date=?,
        content=?,
        title=?,
        writer=? 
    where
        bno=?

우선 findById()를 실행할 때 (1)을 실행하고 save()를 처리할 때 같은 번호가 있는지 다시 검사 (2)한 후에 update() (3)를 실행한다.

mod_date'2023-06-28 15:02:47.705192', reg_date '2023-06-28 14:02:46.737135'
update가 실행된 후에 시간이 자동으로 변경된다.

delete()

delete()는 @Id에 해당하는 값으로 deleteById()를 통해서 실행할 수 있다.

@Test
public void testDelete(){
    Long tno = 1L;
    repository.deleteById(tno);
}

테스트 결과

	Hibernate: 
   select
       board0_.bno as bno1_0_0_,
       board0_.mod_date as mod_date2_0_0_,
       board0_.reg_date as reg_date3_0_0_,
       board0_.content as content4_0_0_,
       board0_.title as title5_0_0_,
       board0_.writer as writer6_0_0_ 
   from
       board board0_ 
   where
       board0_.bno=?
       
       
Hibernate: 
   delete 
   from
       board 
   where
       bno=?

수정이나 삭제 시에 굳이 select 문이 먼저 실행되는 이유는?

JPA를 이용하는 것은 영속 컨텍스트와 DB를 동기화하여 관리한다는 의미이다.

그렇기 때문에 특정 엔티티 객체가 추가되면 영속 컨텍스트에 추가하고 DB와 동기화가 이루어져야한다.

수정이나 삭제가 이루어질 때에도 영속컨텍스트에 해당 엔티티 객체가 존재해야만 하므로 먼저 select로 엔티티 객체를 영속 컨텍스트에 저장해서 이를 삭제한 후에 delete가 이루어져야 한다.

profile
혼자 보려고 올리는 용도

0개의 댓글