jpa정리

꿀이·2022년 4월 28일
0

기본 세팅

        /**
         *     persistence.xml 의 unit name 을 넣어준다
         *     <persistence-unit name="hello">
         *
         * 1.emf 는 애플리케이션이 실행될 때 하나만 생성을 하고
         *   db에 접근하는 행위가 일어날 때마다(ex 고객요청이 올떄마다) em 을 생성해서 작업을 수행해야함
         *   em.getTransaction() 을 생성해주고 begin(),코드 작성, commit() 을 해줘야 한다.
         * 2.em은 db커넥션을 계속 물고 있기 때문에 중간에 에러가 발생해서 close() 를 못한다면 문제가 발생
         *   try-catch 로 코드를 작성
         *   -> 근데 이걸 스프링을 사용하면 간단하게 코드를 작성할 수 있다
         * 3.아래 예제에서 em.persist 를 주석처리하고 findMember 값을 수정하면 em.persist()를
         *   하지 않아도 db의 값이 변경된다. jpa가 find 해서 가져온 값을 관리하면서 커밋시점에 변경을 감지하면
         *   업데이트 쿼리를 자동으로 날려준다.
         */

영속성 컨텍스트

아래 코드 실행 결과를 보면 Member 객체를 생성하고 나서 em.persist() 를 할 때 쿼리가 db로 나가지 않은걸 확인할 수 있다. 그런데 em.find() 를 하면 값이 제대로 출력되는걸 볼 수 있는데, 엔티티 매니저 내부에 캐시공간을 두고서 Member 객체를관리하고 있고, em.find() 를 하면 1차캐시를 먼저 조회를 해서 가져오는거다. 이후에 커밋하는 시점에 쿼리가 날아가는걸 확인!!

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {
    public static void main(String[] args) {
      
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();


        try{
            //비영속 상태
            Member member = new Member();
            member.setId(101L);
            member.setName("HelloSDI");

            //영속 상태
            System.out.println("=== BEFORE ===");
            em.persist(member);
            System.out.println("=== AFTER ===");

            Member findMember = em.find(Member.class, 101L);

            //쿼리를 날리지는 않았는데 조회는 되네? -> 엔티티 매니저에 있는 1차캐시에 저장하고 있던 값
            //1차 캐시에서 먼저 조회한다.
            System.out.println("findMember.id = " + findMember.getId());
            System.out.println("findMember.getName() = " + findMember.getName());

            tx.commit();
        }catch(Exception e){
            tx.rollback();
        }finally {
            em.close();
        }

        emf.close();//애플리케이션이 종료되면 얘도 종료되어야 한다.

    }
}

DB 에서 조회할 때

그렇다면 디비에 저장된 값을 조회할때를 키값이 다른 두개의 값을 조회할 때는 쿼리를 두번 나가는걸 확인할 수 있지만 똑같은 키값으로 find() 를 한다면 이때 쿼리가 한번이 나간다. 이거는 첫번째 조회할때 앤티티 매니저에 저장을 해두고 두번째 조회할 때는 캐시를 찾아보기 때문!! 또한 두개를 == 비교하면 true로 나오는걸 알 수 있다.

변경감지

find() 를 한 후에 값을 변경했다고 하자, 그럼 persist() 를 해줘야 할까??? 이떄는 다시 persist() 를 할 필요가 없다.

커밋하는 시점에 내부적으로 flush() 가 호출이 되는데, 1차 캐시에 있는 @ID, 엔티티, 스냅샷(값을 읽어온 그 시점을 저장해둠) 들을 비교해서 변경이 발생했는지 비교를 한다. 변경이 있다면? -> update 쿼리를 쓰기 지연 sql 저장소에 생성한다.

마치 자바 컬렉션을 사용할때 값을 변경한 후에 다시 컬렉션에 넣지 않는것 처럼!! 컬렉션처럼 동작을 하게 된다.

flush()

근데 커밋하기 전에 강제적으로 flush() 를 해서 db를 업데이트 칠 수도 있다. flush() 를 한다고 해서 1차캐시가 지워지거나 하지는 않는다.

jpql 을 실행하기 전에는 디폴트로 자동으로 flush() 를 미리 해준다. db에 값이 없으면 안되니까

em.clear() & em.detatch

em.clear() 를 하면 영속성 컨텍스트를 모두 날려버리고, em.detatch() 를 하면 특정 엔티티에 대해서만 준영속 상태를 해제할 수 있다.


컬럼 매핑

enum 타입에 ordinary 를 쓰면 안된다. 새로운 항목들이 추가가 될때마다 index 값이 변경이 일어나서 찾기 힘든 버그 발생!! String 타입을 쓰는게 좋다. (조금 데이터는 조금 더 발생해도 장애 대응에 에너지 쏟는거 보다는...)


기본키

  • 기본 키 제약 조건 : null 아님, 유일, 변하면 안됨(이게 어려움)

  • 기본키 자동생성 IDENTITY 전략 : 근데 이렇게 되면 영속성 관리를 할때 pk 값이 필요한데. 이렇게 하면 DB에 쿼리를 날리기 전에는 pk 값을 알 수가 없다. -> IDENTITY 전략일때만 예외적으로 em.persist() 할 때 DB에 넣어준다.


연관관계

단방향 연관관계

db에 쿼리로 조회를 할때 조인을 이용한 쿼리를 날리면 Member와 외래키로 연결되어져 있는 Team 정보를 조회할 수 있다.

근데 객체의 경우는 Member 객체를 찾아온 후에 거기에 맞는 TeamId 를 꺼내서 다시 em.find() 를 해야한다. 뭔가 객체지향 스럽지 않다..!

            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setUsername("member1");
            member.setTeamId(team.getId());
            em.persist(member);

            //테이블은 외래 키로 조인을 사용하면 되지만, 객체는 참조를 사용해야 해서 계속 타고타고 들어가야 한다.
            Member findMember = em.find(Member.class, member.getId());
            Long findTeamId = findMember.getTeamId();
            Team findTeam = em.find(Team.class, findTeamId);

Member.class 에 @ManyToOne, @JoinColumn(name = "TEAM_ID") 를 해서 연관관계를 매핑한 후에 아래 코드와 같이 Member 만 찾은 후에 Team 객체를 바로 가져올 수 있다. 또한 Team 객체에 접근해서 setter 로 값을 변경하면 db에도 자동적으로 update 쿼리가 나가는걸 확인 할 수 있다..!!

//    @Column(name = "TEAM_ID")
//    private Long teamId;

    //jpa에게 관계를 알려줘야 한다.
    //Member 입장에서는 many / team 입장에서는 one
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
            //저장
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setUsername("member1");
            member.setTeam(team);
            em.persist(member);
            em.flush();
            em.clear();

            //조회 : 연관관계를 매핑하면 바로 team 을 조회할 수 있다.
            Member findMember= em.find(Member.class, member.getId());
            Team findTeam = findMember.getTeam();

            //수정
            findTeam.setName("Updated TeamName");

            //영속성 컨텍스트 의 1차 캐시를 뒤져서 가져온다.
            //왜냐? commit 하는 시점에 db에 insert 쿼리를 날리니까
            //db에서 날리는걸 보고싶으면 em.flush() 를 위에서 해주면 된다.
            //em.clear() 를 하면 영속성 컨텍스트를 clear 하게 해준다.
            System.out.println("findTeam.getName() = " + findTeam.getName());


            tx.commit();

양방향 연관관계

db 테이블에서는 외래키로 조인을 하면 아무테이블에서나 조인된 테이블을 볼 수가 있지만, 객체에서는 그렇지 않다. 이걸 해결하기 위해 나온게 양방향 연관관계 설정인듯?

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;

    /**
     * mappedBy = JPA의 멘붕 난이도. 마치 c 포인터 처럼...
     * 객체의 두 관계중 하나를 연관관계의 주인으로 지정
     * 주인이 아닌쪽은 읽기만 가능
     * 여기서는 mapped 되었다 team 에 의해서 관리가 돼! 한마디로 Member 의 team 이 주인이다.
     * members 에 아무리 값을 넣어도 db에 업데이트가 일어나지 않는다..!!
     * 외래키가 있는 곳을 주인으로 정하는게 기본적인 Rule 이다!! (xxToOne 인 곳에 무조건 주인이 된다고 보면 된다.)
     * -> 이유는 team 의 값을 바꿨는데 업데이트 쿼리는 Member로 나간다. (혼돈..혼란..멘붕..유발한다.)
     */
    @OneToMany(mappedBy = "team")//맴버에 있는 변수명을 걸어줘야 한다.
    private List<Member> members = new ArrayList<>();
@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

//    @Column(name = "TEAM_ID")
//    private Long teamId;

    //jpa에게 관계를 알려줘야 한다.
    //Member 입장에서는 many / team 입장에서는 one
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

✱양방향 연관관계 주의점1

연관관계의 주인인 곳에 set 을 해줘야지 db에 제대로 반영이 된다. mapped by 된곳은 읽기전용으로 생각하면 된다.

            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setUsername("member1");

            // (2) 연관관계의 주인이 member 의 team 이기 때문에 여기서 먼저 team 을 적용해줘야 한다.
            member.setTeam(team);

            // (1) team 의 members 에다가 member를 추가해보자
            // 근데 이렇게 하면 값이 안들어간다.
            // (2) 를 해보면 들어간다. 아래 코드는 넣어도 되고 안넣어도 된다. 어차피 읽기 전용이니까--> 라고 생각했는데 근데 이건 넣어주는게 좋다.
            // team.getMembers().add(member);
            em.persist(member);

당황하지 말것..!


추가

근데 이게 양쪽 모두에 값을 넣어주는게 맞다고 한다. em.flush() 를 안한상태에서 만약에 List<> members 를 체크해야 할일이 있으면 db로 쿼리가 안나간 상태이기 때문에 로직을 태울수가 없게 된다. 그래서 영속성으로 관리를 해주는게 좋다. flush() 를 깜빡하고 em.find() 를 할때 1차캐시에서 가져올 수가 있으니까 또한 testcase 작성을 위해서도 양쪽으로 값을 세팅해주는게 옳다..!!!

근데 이게 이렇게 하면 사람이다 보니 양쪽 모두 넣는 코드를 깜빡할 수도 있다. 그래서 아예 주인쪽에서 가값을 변경할때 해줄때 양쪽 모두에 넣어주는 방법을 사용한다고 한다..!! (java 기본 set 메서드의 기능과 혼돈을 줄 수 있기 때문에 changeTeam() 등의 이름을 많이 쓴다고 한다. )

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }

✱양방향 연관관계 주의점2

toString() 을 Team 과 Member 에 모두 걸어주게 되면 양쪽을 계속 참조하면서 무한루프에 빠질 수 있다..!! 주의!!!!! (lombok,json 생성 라이브러리(근데 엔티티는 컨트롤러에서 반환하지 않는게 추천방법?! 이건 한번 찾아보자) 도 주의)

정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료!! (괜히 처음부터 머리아프게 할 필요가 없다..!!)
  • 양방향 매핑은 조회기능이 추가된 것!!
  • JPQL 에서 역방향으로 탐색할 일이 많은데 이럴때 양방향 매핑 적용을 고려하면 된다..!! (테이블에 영향을 주지 않음)

다양한 연관관계 매핑

다대일[N:1]

Member 와 Team 을 보면 Member 는 하나의 Team 을 가지지만 Team 입장에서는 여러명의 Member를 가질 수 있다. 이럴때 Member 쪽이 Many, Team 쪽이 One 이 된다. 외래키는 Many 쪽에 생성되는게 일반적이다. (Team 이 모든 멤버를 가지고 있는게 효율적이지 않으니까?) 이때 JPA 에서 연관관계의 주인은 항상 외래키가 있는 Many 쪽이라고 생각하면 된다.

만약 양방향을 걸어주고 싶다고 한다면? 이때는 Many 의 반대쪽 객체에 List 를 추가해주면 된다. mapped by Member 되는거라 읽지 전용 사용된다.


일대다[1:N] (권장하지는 않는다고 한다!!)

똑같은 Team 과 Member 를 생각해보자. 근데 이게 설계가 어떤 상황이냐면

  • Team 은 Member 들을 알고 싶다.
  • Member 는 Team 을 알고 싶지 않다.

근데 db 관점에서 보면 외래키는 항상 Many 쪽에 있어야 한다. 이렇다 보니 Team 에다가 Member를 추가했는데 update 쿼리가 Member 에도 나가야 하는 상황이 생긴다. (헤깔릴 수가 있음) -> 근데 이게 왜 나가는지는 좀 더 생각을 해보자

이거도 양방향을 걸 수도 있는데... 이건 그냥 다대일 양방향을 사용하자...


다대다[N:M] (얘도 권장 x)

객체의 경우는 다대다 관계가 가능하지만 db테이블의 경우는 다대다 관계는 중간테이블을 만들어서 일대다 & 다대일 관계로 풀어서 설계를 해야한다.


상속관계 매핑

객체의 상속을 db에서는 어떤식으로 표현 하냐면 Item / Album,Movie,Book 사이를 모두 Item_id 로 pk및 fk 를 걸어서 join을 해서 할 수도 있고, 하나의 Item 테이블에 모든 컬럼값을 넣어서 관리할 수도 있다. 디폴트는 모든 컬럼을 Item 에 넣어서 관리하는것, @Inheritance(strategy = InheritanceType.JOINED) 를 통해서 테이블을 join 해서 만드는 형식으로 만들 수도 있다.

근데 싱글테이블로 운영을 한다고 하면 DTYPE 을 통해서 구분을 해줘야 한다. book 인지 movie 인지...
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) 를 사용하면 된다. (싱글 테이블이 성능상 장점이 있다. insert 도 한번에 들어가고 select 도 join 할 필요도 없고!!)

개발단계에서 조인전략으로 개발을 한후에 테스트 하다가 성능이 너무 안나오면 싱글테이블로 설계를 바꿀 수 있는데, 테이블이 바뀌는건데도 코드가 변하는게 없다!!!


MappedSuperclass

뭔가 중복적인 속성같은게 모든 entitiy 에 들어가야 할때 해당 어노테이션을 사용해서 클래스를 만들어서 쓸수 있다. entity 들에서는 해당클래스를 extends 해서 사용하면 됨.

이런 속성들?

  • created by
  • created date
  • last modified
    ...

프록시

프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는게 아니라, 프록시 객체를 통해서 엔티티에 접근을 하는거다. 따라서 프록시 객체와 원본 엔티티를 비교할 때는 == 비교 대신에 instance of 를 사용해야 한다.

근데 이미 영속성 컨텍스트에 있는 엔티티를 getReference() 로 가져오면 이때는 프록시 객체가 아니라 실제 엔티티 객체가 나오게 된다.


즉시 로딩과 지연 로딩

지연로딩 lazy 를 사용하면 예를들어서 Member 를 em.find 를 할때 Team 에 대한거는 프록시 객체를 생성을하고 쿼리를 날리지는 않는다. 이후에 실제로 Team 객체의 어떤 컬럼값이 필요하다면 이때 db로 쿼리를 날린다.

eager 즉시로딩을 하면 Member를 find 할때 부터 Team 을 join 해서 모두 가져온다. Team 의 객체를 출력해보면 프록시 객체가 아니라 실제 객체가 튀어 나오게 된다.

가급적 지연로딩만 사용하는걸 추천

  • 테이블이 수십개가 엮여있다고 하면 em.find()만 해도 성능상 문제가 된다.
  • 기본적으로 lazy로 다 설정해두고 통으로 불러와야 할 때는 fetch join 을 사용하면 된다.
  • one to one , many to one 은 default 가 즉시로딩이므로 lazy 설정 필요!!

영속성 전이 : CASCADE

cascade = CascadeType.ALL 설정을 해주면 부모를 persist 할 때 연관된 엔티티들까지 모두 영속화 해준다.

하나의 부모가 자식들을 관리할 때 많은 도움이 된다고 한다. 만약 다른 엔티티와 연관관계가 있을때는 쓰지 않는게 좋다고 한다. (다른애들이 child 를 알 고 있을 때)
추가적으로 parent와 child 의 라이프 사이클이 유사할 때 써도 좋다


고아 객체

orphanRemoval = true 설정을 해주면 부모쪽에서 list로 child를 가지는 연관관계를 가진다고 할때. 부모 list 에서 child 를 제거하면 delete 쿼리가 db로 나가게 된다.

  • 참조하는 곳이 하나일 때 사용해야함!!
  • 특정 엔티티가 개인 소유할 때 사용한다!!

값 타입

임베디드 타입

뭔가 클래스에 묶어주고 싶은 멤버변수들을 따로 클래스로 만들어서 관리를 하고 싶을때 사용하는거 같다.

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시
  • 기본 생성자는 필수

만약 하나의 Member 클래스 안에서 같은 임베디드 타입의 멤버변수가 두개가 있다면 이때는 AttributeOverrides 를 사용하면 된다.

값 타입은 공유 참조를 조심해야 한다. member1를 set 할때 address 값 타입을 복사해서 그대로 member2에서 사용해버리면 의도치 않게 원하지 않는 값들이 바뀔수 있고 이런 버그는 찾기가 매우매우 힘듬!


//이런식으로 새로 만들어서 set 을 하는식으로 해야 한다. 복사를 해서 값타입을 넣는거를 조심해야 한다.
            Address address = new Address(xxxx);
            Member1.set;
            em.persist();

            Address address2 = new Address(yyyy);
            Member2.set;
            em.persist();

근데 이렇게 한다고 해도 여러사람이 개발을 하다보면 이런게 깨질 위험이 있다. 그래서 불변 객체로 설계를 하는게 안전하고 좋은 방법이다. Integer / String 이 대표적인 불변객체 예시이다.

  • set을 다 지우자...

    불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다...! 그냥 객체를 새로 생성해서 넣어주는 방법을 사용하자

값 타입의 비교

비교를 할때 == 을 사용하면 당연히 false 가 나온다. equals() 를 구현을 해줘야지 비교가 가능하다.

값 타입 컬렉션

  • 컬렉션들은 기본적으로 지연로딩이 적용되어있음
  • 얘도 마찬가지로 객체를 새로 생성을 한 후에 수정을 해줘야 한다. 그렇지 않으면 원치않는 쿼리가 나가게 되고, 전체의 값들이 바뀔 수 있다.
  • list객체 이런거 같은경우는 remove 를 한후에 새로 add를 해줘야 한다.

실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는게 좋다고 한다. 엔티티를 그냥 생성을 한다.

값 타입 컬렉션을 언제 쓰냐하면, 진짜 너무 간단한 구조일 때 사용한다고 한다. 셀렉트 박스 같은거에서 선택같은거 할때, 값이 바껴도 업데이트 칠 필요가 없을때 주로 사용한다고 한다.


쿼리

테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리

가잔 단순한 조회 방법

  • EntityManager.find()
  • 객체 그래프 탐색(a.getB().getC())

만약 나이가 18살 이상인 회원을 모두 검색하고 싶다면? -> 애플리케이션에 필요한 최소한의 데이터만 불러 와야 한다.

  • CriteriaBuilder의 createQuery를 통해서 자바쿼리를 사용해서 동적 쿼리를 사용할 수 있다.
  • 근데 이게 다른사람들이 함께 개발할 때 뭐가뭔지 알기가 힘들다. 유지보수 측면에서 좋지가 않다고 한다. 이런게 있구나 정도만 알고 넘기자. -> 너무 복잡하고 실용성이 없다.

JPQL

  • 엔티티와 속성은 대소문자 구분 필요
  • JPQL 키워드는 대소문자 구분 하지 않아도 된다
  • 테이블 이름이 아니라 엔티티 이름을 사용해야 한다. default 엔티티 이름을 쓰던가 Entity(name = "불라불라") 이 이름을 사용해야 한다.
  • 별칭은 필수 (as는 생략 가능하다)
    →select m from Member as m

프로젝션 (select)

em.createQuery() 로 select 해서 온 객체가 있다고 하면 얘도 영속성 관리가 된다.

아래와 같이 여러 값들을 가져오고 싶을 때 dto 객체를 하나 만들어서 가져올 수도 있는데, 이때 new jpql 을 사용해서 가져올 수 있다. dto 내부의 생성자가 호출되는듯

List<MemberDTO> result = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
                    .getResultList();

페이징 API

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수

조인

  • 내부조인 : select m from Member m join m.team t
  • 외부조인 : select m from Member m left join m.team t
  • 세타조인(무근본 아무거나 조인하고 싶을 때??) : select count(m) from Member m, Team t where m.username=t.name

서브쿼리

  • JPA는 where,having 절에서만 서브 쿼리 사용 가능
  • select 절도 가능(하이버네이트에서 지원)
  • from 절의 서브 쿼리는 현재 JPQL에서 불가능

jpql 타입 표현과 기타식

enum의 경우 패키지 경로를 모두 적어줘야지 된다. 아니면 setparameter 로 매핑시켜줄 수도 있다.

조건식 -case식

  • case
  • coalesce
  • nullif

jpql 기본함수

  • concat
  • substring
  • trim
  • lower, upper
  • length
  • locate
  • abs, sqrt, mod
  • size, index(jpa 용도)
profile
내게 맞는 옷을 찾는중🔎

0개의 댓글