Skip to content

Latest commit

 

History

History
1999 lines (1689 loc) · 67.9 KB

README.md

File metadata and controls

1999 lines (1689 loc) · 67.9 KB

JPA 정리

참고 자료
강의 : https://www.inflearn.com/course/ORM-JPA-Basic
서적 : 자바 ORM 표준 JPA 프로그래밍 - 지은이 : 김영한

1. 기본 키(식별자) 할당 전략

1. 직접 할당 전략

    
    // Member.java
    @Id
    @Column(name = "ID")
    private Long id;
    
    // JpaMain.java
    Member member = new Member();
    member.setId(id); // 기본 키(식별자) 직접 할당
    member.setUserName(name);
    member.setAge(age);
    member.setRoleType(RoleType.USER);
    member.setCreatedDate(new Date());
    member.setLastModifiedDate(new Date());

    entityManager.persist(member);
    

2. IDENTITY 전략

기본 키(식별자) 생성을 DB에 위임(EX. MySQL 의 AUTO_INCREMENT 값 사용)

Entity 를 EntityManager 를 통해 관리(Persistence Context 내 관리)하기 위해선 Entity 가 기본 키(식별자)를 가지고 있어야 하기 때문에, DB 에 값이 INSERT 될 때 기본 키(식별자)가 생성되는 이 전략을 사용하면, entityManager.persist() 를 호출하는 순간 쿼리가 DB에 전달된다. (쓰기 지연이 동작하지 않는다는 뜻)

    
    // Member.java
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long id;
        
     // JpaMain.java
     Member member = new Member();
     //member.setId(id); // 기본 키 직접 할당
     member.setUserName(name);
     member.setAge(age);
     member.setRoleType(RoleType.USER);
     member.setCreatedDate(new Date());
     member.setLastModifiedDate(new Date());
     
     entityManager.persist(member);
    

3. SEQUENCE 전략

기본 키(식별자) 생성을 DB의 시퀀스를 활용해 생성 entityManager.persist() 호출시 시퀀스를 통해 기본 키(식별자)를 조회해 Entity 에 할당하고 Entity 에 기본 키(식별자)가 할당 되었으므로 Persistence Context 에서 관리 됨

    
    // Member.java
    @Entity
    @SequenceGenerator(name = "MEMBER_SEQUENCE_GENERATOR"
            , sequenceName = "MEMBER_SEQ_IN_DB"
            , initialValue = 1
            , allocationSize = 1)
    public class Member {
        @Id
            @GeneratedValue(strategy = GenerationType.SEQUENCE
                            , generator =  "MEMBER_SEQUENCE_GENERATOR")
            @Column(name = "ID")
            private Long id;
    
    // JpaMain.java
    Member member = new Member();
    //member.setId(id); // 기본 키 직접 할당
    member.setUserName(name);
    member.setAge(age);
    member.setRoleType(RoleType.USER);
    member.setCreatedDate(new Date());
    member.setLastModifiedDate(new Date());
     
    entityManager.persist(member);
    

4. TABLE 전략

기본 키(식별자) 생성 전용 Table을 사용하여 Sequence 처럼 사용하는 전략 entityManager.persist() 호출시 기본 키(식별자) 생성 전용 Table 을 조회해 기본 키(식별자)를 Entity 에 할당하고 Entity 에 기본 키(식별자)가 할당 되었으므로 Persistence Context 에서 관리 됨

    
    // Member.java
    @Entity
    @TableGenerator(name = "MEMBER_SEQUENCE_GENERATOR"
            , table = "MEMBER_SEQUENCES"
            , pkColumnValue = "MEMBER_SEQ"
            , allocationSize = 1)
    public class Member {
        @Id
            @GeneratedValue(strategy = GenerationType.SEQUENCE
                            , generator =  "MEMBER_SEQUENCE_GENERATOR")
            @Column(name = "ID")
            private Long id;
    
    // JpaMain.java
    Member member = new Member();
    //member.setId(id); // 기본 키 직접 할당
    member.setUserName(name);
    member.setAge(age);
    member.setRoleType(RoleType.USER);
    member.setCreatedDate(new Date());
    member.setLastModifiedDate(new Date());
     
    entityManager.persist(member);
    

5. AUTO 전략

설정한 Database 방언(Dialect)에 따라 적합한 전략(IDENTITY, SEQUENCE, TABLE 중 하나)을 자동으로 선택 @GeneratedValue.strategy 기본 값이며, 개발 초기 단계에 적합한 것 같다. (property name="hibernate.hbm2ddl.auto" value="create" 설정이 되어있으면 어느정도 자동으로 만들어주지만 실제 SEQUENCE 나 TABLE 전략을 사용하려면 DB 에 해당 시퀀스나, Table을 만들어줘야하기 때문)

2. 연관관계

1. 방향

회원과 팀이라는 개념이 있고, 회원과 팀이 관계가 있을 때, 회원 -> 팀, 팀 -> 회원으로 참조하는 방향이 있다.

회원 -> 팀으로만 참조할 수 있거나, 팀 -> 회원으로만 참조하는 것을 단방향 관계라고 한다. (양쪽으로 참조 가능한 것을 양방향 관계라고 함.)

2. 다중성

'다대일', '일대다', '일대일', '다대다' 라는 다중성이 있다.

회원과 팀의 경우 팀 하나에 여러명의 회원이 소속이 되고, 관계는 '회원 여러명 - 팀' 으로 정리가 된다.

이를 회원을 기준으로 생각한다면 다대일(회원 여러명 - 팀 1개) 관계가 되고, 반대로 팀을 기준으로 생각한다면 일대다(팀 1개 - 회원 여러명) 관계가 된다.

3. 객체 - 테이블

회원이라는 객체(Member.java)는 MEMBER 라는 이름의 table 로 관리하고 있고, 팀이라는 객체(Team.java)는 TEAM 이라는 이름의 Table 로 관리하고 있다.

회원과 팀의 객체끼리는 변수를 통한 참조로 연관관계를 맺고, 회원과 팀의 Table 끼리는 외래 키를 통해 연관관계를 맺는다.

객체끼리의 참조는 회원 -> 팀, 팀 -> 회원 등 참조하는 방향을 가지며, 이는 단방향 관계이다.(단방향 관계를 회원 -> 팀, 팀 -> 회원 2개를 만든게 양방향 관계, 즉, 단방향 관계를 양쪽에서 만든 것)

Table 끼리의 Join 은 방향이 없는, 굳이 방향을 따지자면 양방향으로 외래 키만 있다면 해당 Table 들의 정보를 가져올 수 있다.

4. 단방향 연관관계

1. 다대일(ManyToOne) 단방향 연관관계

  • 회원과 팀의 경우, 회원의 입장에서 보면 '회원 여러명 - 팀 1개' 로 관계가 생성되므로, 회원의 입장에서 다대일(ManyToOne)관계이다.

  • @ManyToOne
    다대일임을 나타내는 어노테이션
    회원(다) 객체에서 Team team 이라는 필드가 있고, 이 필드가 회원(Member) 과 팀(Team)의 다대일 연관관계를 나타내는 것 임을 나타내는 역할을 함.

  • @JoinColumn(name="Foreign key") @ManyToOne 어노테이션을 통해 어떤 엔티티와 다대일 관계인지 나타냈다면, 해당 엔티티와 매핑해 줄 외래 키 이름을 지정

Member.java

     
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "many_to_one_member")
    @Entity(name = "ManyToOneMember")
    public class Member {
    
        @Column(name = "member_id")
        // IDENTITY 는 DB의 AUTO INCREMENT 를 사용하므로, persist 시 INSERT 쿼리가 바로 나간다.
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        @Column(name = "user_name")
        private String userName;
    
        /**
         * 처음 생각
         * @JoinColumn 을 생략할 수 있다는데, 아마도 변수 타입으로 Team 이라는 Entity 를 들고있고,
         * 해당 Entity 의 @Id 컬럼을 알 수 있기 때문이 아닐까??
         *
         * 자료 내용
         * @JoinColumn 을 생략하면 외래 키를 찾을 때 기본 전략을 사용한다고 한다,
         * 기본 전략은 : 필드명 + _ + 참조하는 테이블의 컬럼명
         * 지금 이 변수에서 기본전략을 적용하면,
         * 변수명은 team 이며, 테이블의 컬럼명은 Team entity 에 @Id 에 해당하는 column 명은, team_id
         * 따라서 team_team_id 외래 키를 참조하게 된다고 한다.
         *
         * 결국 Team 이라는 Entity 를 들고있으니 어떤 Entity 혹은 Table 을 참조해야하는 지 알 수 있기에,
         * 기본 전략으로 team_team_id 라는 외래키를 사용할 수 있는 것 같다.
         */
        @JoinColumn(name = "team_id")
        @ManyToOne
        private Team team;
    
        @Override
        public String toString() {
            return "Member{" +
                    "id=" + id +
                    ", userName='" + userName + '\'' +
                    ", team=" + team +
                    '}';
        }
    }
    
  

Team.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "many_to_one_team")
    @Entity(name = "ManyToOneTeam")
    public class Team {
    
        @Column(name = "team_id")
        // IDENTITY 는 DB의 AUTO INCREMENT 를 사용하므로, persist 시 INSERT 쿼리가 바로 나간다.
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        private String name;
    
        @Override
        public String toString() {
            return "Team{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
  

2. 일대다(OneToMany) 단방향 연관관계

  • 주로 사용할 것 같지 않으므로 필요할 때 공부하도록 하겠다.

3. 일대일(OneToOne) 단방향 연관관계

  • 주 테이블이나 대상 테이블 둘 중 어느 테이블이나 외래 키를 가질 수 있다.
    (ManyToOne, OneToMany 에서 Many 쪽 테이블이 외래 키를 가지는 특성과 대비된다.)

Member.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "one_way_one_to_one_member")
    @Entity(name = "OneWayOneToOneMember")
    public class Member {
     
        @Column(name = "member_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
     
        @Column(name = "user_name")
        private String userName;
     
        @JoinColumn(name = "locker_id")
        @OneToOne
        private Locker locker;
     
        @Override
        public String toString() {
            return "Member{" +
                    "id=" + id +
                    ", userName='" + userName + '\'' +
                    ", locker=" + locker +
                    '}';
        }
    }    
    
  

Locker.java

     
     @NoArgsConstructor
     @Setter
     @Getter
     @Table(name = "one_way_one_to_one_locker")
     @Entity(name = "OneWayOneToOneLocker")
     public class Locker {
     
         @Column(name = "locker_id")
         @GeneratedValue(strategy = GenerationType.IDENTITY)
         @Id
         private Integer id;
     
         private String name;
     
         @Override
         public String toString() {
             return "Locker{" +
                     "id=" + id +
                     ", name='" + name + '\'' +
                     '}';
         }
     }
     
  

5. 양방향 연관관계

양방향 연관관계의 경우 관리 Point 가 늘어나기 때문에 필요할 때 공부해서 사용하도록 하겠다.

6. 다양한 매핑

1. 상속 관계 매핑

Item 이라는 부모 타입의 테이블이 있고, Album, Movie, Book 이라는 자식 타입의 상품
테이블이 있다면 공통 속성을 부모 테이블이 가지고있고 각 상품의 특징에 해당하는 속성은
테이블 별로 가져야 할 것이다.
또한 기존에 DB 테이블이 구현된 상태에서 상속 관계를 매핑해야하는 경우에는
아래에 나오는 전략에 따른 테이블 구조를 보고 참고해서 선택하면 된다.

이런 상속 관계를 매핑하는 방법에 대해 알아보도록 하자.

  • 1. 조인 전략(각각의 테이블로 변환)

Entity 별로 테이블을 만들고 자식 테이블이 부모 테이블의 기본 키(식별자)를
기본 키이자 외래 키로 사용하는 것이다.

DB 테이블 구조

스크린샷 2020-09-08 오전 10 32 05

Item.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    // 어떤 컬럼명으로 자식 상품들을 구별할지, name = "item_type" 생략 시 기본 값 "DTYPE"
    @DiscriminatorColumn(name = "item_type")
    // 부모 클래스에 이 어노테이션을 사용해줘야 함, 매핑 전략을 JOINED 로 설정해 전략을 조인 전략으로 설정
    @Inheritance(strategy = InheritanceType.JOINED)
    @Table(name = "join_strategy_item")
    @Entity(name = "JoinStrategyItem")
    /**
     *  abstract 를 빼고 일반 class 로 해도 작동하지만, Item 그 자체로 상품이 아닌, 상세 정보를 나타낼 자식 상품 Entity 가 있어야 하기에
     *  abstract 를 명시적으로 선언해 Item Entity 단일로는 상품의 가치가 없음을 알려주고있다.
     */
    public abstract class Item {
    
        @Column(name = "item_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        private String name;
    
        private int price;
    
        @Override
        public String toString() {
            return "Item{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", price=" + price +
                    '}';
        }
    }
    
  

Album.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    /**
     * 부모(Item.java) Entity 관련 테이블의 구분 컬럼(@DiscriminatorColumn 으로 설정한 컬럼)에 어떤 값으로 들어갈지
     * 이 어노테이션 생략 시 Entity Name 이 기본 값으로 들어가게 된다 (@Entity(name = "JoinStrategyBook"))
     */
    @DiscriminatorValue(value = "ALBUM")
    @Table(name = "join_strategy_album")
    @Entity(name = "JoinStrategyAlbum")
    public class Album extends Item{
    
        private String artist;
    
        @Override
        public String toString() {
            return "Album{" +
                    "artist='" + artist + '\'' +
                    '}';
        }
    }
    
  

Movie.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    /**
     * 부모(Item.java) Entity 관련 테이블의 구분 컬럼(@DiscriminatorColumn 으로 설정한 컬럼)에 어떤 값으로 들어갈지
     * 이 어노테이션 생략 시 Entity Name 이 기본 값으로 들어가게 된다 (@Entity(name = "JoinStrategyBook"))
     */
    @DiscriminatorValue(value = "MOVIE")
    @Table(name = "join_strategy_movie")
    @Entity(name = "JoinStrategyMovie")
    public class Movie extends Item{
    
        private String director;
        private String actor;
    
        @Override
        public String toString() {
            return "Movie{" +
                    "director='" + director + '\'' +
                    ", actor='" + actor + '\'' +
                    '}';
        }
    }
    
  

Book.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    /**
     * 부모(Item.java) Entity 관련 테이블의 구분 컬럼(@DiscriminatorColumn 으로 설정한 컬럼)에 어떤 값으로 들어갈지
     * 이 어노테이션 생략 시 Entity Name 이 기본 값으로 들어가게 된다 (@Entity(name = "JoinStrategyBook"))
     */
    @DiscriminatorValue(value = "BOOK")
    @Table(name = "join_strategy_book")
    @Entity(name = "JoinStrategyBook")
    public class Book extends Item{
    
        private String author;
        private String isbn;
    
        @Override
        public String toString() {
            return "Book{" +
                    "author='" + author + '\'' +
                    ", isbn='" + isbn + '\'' +
                    '}';
        }
    }
    
  
  • 2. 단일 테이블 전략(통합 테이블로 변환)

    부모 자식 테이블을 모두 합쳐 한 테이블로 관리하는 것 DB 테이블 구조

    스크린샷 2020-09-08 오전 10 32 50

    Item.java

      
      @NoArgsConstructor
      @Setter
      @Getter
      // 어떤 컬럼명으로 자식 상품들을 구별할지, name = "item_type" 생략 시 기본 값 "DTYPE"
      @DiscriminatorColumn(name = "item_type")
      // 부모 클래스에 이 어노테이션을 사용해줘야 함, 매핑 전략을 SINGLE_TABLE 로 설정해 전략을 단일 테이블 전략으로 설정
      @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
      @Table(name = "single_table_strategy_item")
      @Entity(name = "SingleTableStrategyItem")
      /**
       *  abstract 를 빼고 일반 class 로 해도 작동하지만, Item 그 자체로 상품이 아닌, 상세 정보를 나타낼 자식 상품 Entity 가 있어야 하기에
       *  abstract 를 명시적으로 선언해 Item Entity 단일로는 상품의 가치가 없음을 알려주고있다.
       */
      public abstract class Item {
      
          @Column(name = "item_id")
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          @Id
          protected Integer id;
      
          protected String name;
      
          protected int price;
      }
      
    

    Album.java

        
        @NoArgsConstructor
        @Setter
        @Getter
        /**
         * 부모(Item.java) Entity 관련 테이블의 구분 컬럼(@DiscriminatorColumn 으로 설정한 컬럼)에 어떤 값으로 들어갈지
         * 이 어노테이션 생략 시 Entity Name 이 기본 값으로 들어가게 된다 (@Entity(name = "JoinStrategyBook"))
         */
        @DiscriminatorValue(value = "ALBUM")
        @Table(name = "single_table_strategy_album")
        @Entity(name = "SingleTableStrategyAlbum")
        public class Album extends Item {
        
            String artist;
        
            @Override
            public String toString() {
                return "Album{" +
                        "artist='" + artist + '\'' +
                        ", id=" + id +
                        ", name='" + name + '\'' +
                        ", price=" + price +
                        '}';
            }
        }
        
    

    Movie.java

        
        @NoArgsConstructor
        @Setter
        @Getter
        /**
         * 부모(Item.java) Entity 관련 테이블의 구분 컬럼(@DiscriminatorColumn 으로 설정한 컬럼)에 어떤 값으로 들어갈지
         * 이 어노테이션 생략 시 Entity Name 이 기본 값으로 들어가게 된다 (@Entity(name = "JoinStrategyBook"))
         */
        @DiscriminatorValue(value = "MOVIE")
        @Table(name = "single_table_strategy_movie")
        @Entity(name = "SingleTableStrategyMovie")
        public class Movie extends Item {
        
            private String director;
            private String actor;
        
            @Override
            public String toString() {
                return "Movie{" +
                        "director='" + director + '\'' +
                        ", actor='" + actor + '\'' +
                        ", id=" + id +
                        ", name='" + name + '\'' +
                        ", price=" + price +
                        '}';
            }
        }
        
    

    Book.java

        
        @NoArgsConstructor
        @Setter
        @Getter
        /**
         * 부모(Item.java) Entity 관련 테이블의 구분 컬럼(@DiscriminatorColumn 으로 설정한 컬럼)에 어떤 값으로 들어갈지
         * 이 어노테이션 생략 시 Entity Name 이 기본 값으로 들어가게 된다 (@Entity(name = "JoinStrategyBook"))
         */
        @DiscriminatorValue(value = "BOOK")
        @Table(name = "single_table_strategy_book")
        @Entity(name = "SingleTableStrategyBook")
        public class Book extends Item {
        
            private String author;
            private String isbn;
        
            @Override
            public String toString() {
                return "Book{" +
                        "author='" + author + '\'' +
                        ", isbn='" + isbn + '\'' +
                        ", id=" + id +
                        ", name='" + name + '\'' +
                        ", price=" + price +
                        '}';
            }
        }
        
    
  • 3. 구현 클래스마다 테이블 전략(서브타입 테이블로 변환)

    부모 테이블의 속성을 가진 각각의 자식 테이블로 관리하는 것
    DB 테이블 구조

    스크린샷 2020-09-08 오전 10 33 41

    Item.java

        
        @NoArgsConstructor
        @Setter
        @Getter
        // 부모 클래스에 이 어노테이션을 사용해줘야 함, 매핑 전략을 TABLE_PER_CLASS 로 설정해 전략을 구현 클래스마다 테이블 전략으로 설정
        @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
        @Table(name = "table_per_concrete_class_strategy_item")
        @Entity(name = "TablePerConcreteClassStrategyItem")
        /**
         *  abstract 를 빼고 일반 class 로 해도 작동하지만, Item 그 자체로 상품이 아닌, 상세 정보를 나타낼 자식 상품 Entity 가 있어야 하기에
         *  abstract 를 명시적으로 선언해 Item Entity 단일로는 상품의 가치가 없음을 알려주고있다.
         */
        public abstract class Item {
        
            @Column(name = "item_id")
            /**
             * Table Per Concrete Class Strategy 의 경우 각각의 테이블이 생기며, Item table 이 생성되지 않는다.
             * 이 뜻은 @GeneratedValue(strategy = GenerationType.IDENTITY) 을 사용하여 Item table 의 기본 키(식별자)의
             * 자동으로 증가하는 유일한 값을 사용할 수 없음을 의미한다.
             *
             * Album, Movie, Book 3개의 테이블이 생성되고
             * 각각의 테이블의 식별자 값은 테이블 간 동일한 기본 키(식별자)를 생성하지 않기 위해서
             * @GeneratedValue(strategy = GenerationType.TABLE) 혹은  @GeneratedValue(strategy = GenerationType.SEQUENCE)을
             * 사용해야한다는 것을 의미하고,
             *
             * @GeneratedValue 에 strategy 를 설정하지 않으면 기본으로 @GeneratedValue(strategy = GenerationType.AUTO) 가 할당되는데
             * 이렇게 되면 자동적으로 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 를 사용하기 위해
             * 설정된 방언(현재 : H2)에 알맞은 @GeneratedValue(strategy = GenerationType.SEQUENCE) 을 설정한다.
             *
             * 기본적으로 Hibernate 가 제공하는 TABLE 을 통한 기본 키(식별자) 획득 관련 TABLE 혹은 SEQUENCE 이외
             * DB 에 설정된 별도의 기본 키(식별자) 획득 관련 TABLE 혹은 SEQUENCE 를 사용하려면,
             * 기본 키(식별자) 할당 전략을 참고하여 알맞게 설정해야한다.
             */
            @GeneratedValue
            @Id
            protected Integer id;
        
            protected String name;
        
            protected int price;
        }
        
      

    Album.java

          
          @NoArgsConstructor
          @Setter
          @Getter
          @Table(name = "table_per_concrete_class_strategy_album")
          @Entity(name = "TablePerConcreteClassStrategyAlbum")
          public class Album extends Item {
          
              private String artist;
          
              @Override
              public String toString() {
                  return "Album{" +
                          "artist='" + artist + '\'' +
                          ", id=" + id +
                          ", name='" + name + '\'' +
                          ", price=" + price +
                          '}';
              }
          }
          
      

    Movie.java

          
          @NoArgsConstructor
          @Setter
          @Getter
          @Table(name = "table_per_concrete_class_strategy_movie")
          @Entity(name = "TablePerConcreteClassStrategyMovie")
          public class Movie extends Item {
          
              private String director;
              private String actor;
          
              @Override
              public String toString() {
                  return "Movie{" +
                          "director='" + director + '\'' +
                          ", actor='" + actor + '\'' +
                          ", id=" + id +
                          ", name='" + name + '\'' +
                          ", price=" + price +
                          '}';
              }
          }
          
      

    Book.java

          
          @NoArgsConstructor
          @Setter
          @Getter
          @Table(name = "table_per_concrete_class_strategy_book")
          @Entity(name = "TablePerConcreteClassStrategyBook")
          public class Book extends Item {
          
              private String author;
              private String isbn;
          
              @Override
              public String toString() {
                  return "Book{" +
                          "author='" + author + '\'' +
                          ", isbn='" + isbn + '\'' +
                          ", id=" + id +
                          ", name='" + name + '\'' +
                          ", price=" + price +
                          '}';
              }
          }
          
      

2. 매핑 정보만 상속해서 사용하기(@MappedSuperclass)

부모 클래스와 자식 클래스를 DB Table 과 매핑하여 사용하던 상속관계와는 달리
공통 속성(등록일자, 수정일자, 등록자 등)을 정의하고 매핑 정보만 상속할 목적으로 사용

BaseEntity.java

    
    @Setter
    @Getter
    /**
     * 상속 관계 매핑은 아니며, 등록일자, 수정일자, 등록자 같이 여러 Entity 에서 공통으로 사용되는 속성을
     * 정의해두고 상속만 받아 사용하는 방식이다.
     *
     * 즉, 매핑 정보를 상속할 목적으로 사용하지 상속 관계 매핑처럼 부모 Entity 를 만들어 DB 의 Table 화를 하는 등
     * Entity 로서 사용하지 않는다.
     * (@Entity 어노테이션이 없는 것을 확인할 수 있다.)
     *
     */
    @MappedSuperclass
    public abstract class BaseEntity {
    
        @Column(name = "base_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        protected Integer id;
    
        protected String name;
    }
    

Member.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    /**
     * 혹시나 @MappedSuperclass 클래스에 선언된 속성 중
     * 특정 Entity 와 연관된 테이블에서의 명이 다른 경우
     * @AttributeOverrides - @AttributeOverride 를 사용해서 재정의 할 수 있다.
     *
     * 내 생각엔 등록일자, 수정일자, 등록자 같이 여러 Entity 에서 공통으로 사용되는 속성을 @MappedSuperclass 클래스에 정의해두고
     * 사용하려하는데 실제 몇몇 Table 에 등록일자, 수정일자, 등록자 관련 컬럼명이 Table 마다 다를 경우에 사용하면 좋을 것 같다.
     * (ex. 다른 Table 에서는 수정일자를 UPDATE_DATE 로 사용하고 있어서 @MappedSuperclass 의 속성에도
     * 수정일자 관련 컬럼을 UPDATE_DATE 로 설정해두었으나, 어떤 TABLE 에서 수정일자를 MODIFY_DATE 로 사용해서
     * MODIFY_DATE 로 사용하고 있는 테이블과 연관된 Entity 에 아래와 같이 설정하는 것처럼...
     *
     *      @AttributeOverrides({
     *              // name : @MappedSuperclass 에 정의된 프로퍼티 명, column : 현재 Entity 와 관련된 Table 의 어떤 컬럼에 매칭할 지
     *              @AttributeOverride(name = "updateDate", column = @Column(name = "MODIFY_DATE"))
     *      })
     *
     * )
     *
     *
     *
     */
    @AttributeOverrides({
            // name : @MappedSuperclass 에 정의된 프로퍼티 명, column : 현재 Entity 와 관련된 Table 의 어떤 컬럼에 매칭할 지
            @AttributeOverride(name = "id", column = @Column(name = "member_id"))
    })
    @Table(name = "mapped_super_class_member")
    @Entity(name = "MappedSuperClassMember")
    public class Member extends BaseEntity {
    
        private String email;
    
        @Override
        public String toString() {
            return "Member{" +
                    "email='" + email + '\'' +
                    ", id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

Seller.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "mapped_super_class_seller")
    @Entity(name = "MappedSuperClassSeller")
    public class Seller extends BaseEntity {
    
        @Column(name = "shop_name")
        private String shopName;
    
        @Override
        public String toString() {
            return "Seller{" +
                    "shopName='" + shopName + '\'' +
                    ", id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

3. 복합 키와 식별 관계 매핑

  • 복합 키
    기존의 기본 키에 해당하는 컬럼 하나를 사용해서 식별자로 사용했다면,
    두개 이상의 컬럼을 합쳐야 기본 키의 역할을 할 수 있을 때가 있다.
    이렇게 두개 이상의 컬럼을 묶어서 식별자로 사용하는 것을
    복합 키 라고 한다.

1. 식별 관계

부모 테이블의 기본 키(식별자)를 자식 테이블의 기본 키(식별자) + 외래 키로 사용하는 관계

2. 비식별 관계

부모 테이블의 기본 키(식별자)를 자식 테이블의 외래 키로만 사용하는 관계
자식 테이블의 기본 키(식별자)는 자식 테이블에서 직접 관리

  • 필수적 비식별 관계 외래 키 NULL 허용 X(모든 데이터가 부모 테이블과 연관관계가 있음)

  • 선택적 비식별 관계 외래 키 NULL 허용 O(각각의 데이터들은 부모 테이블과 연관관계가 있을수도 없을수도 있음)

3. 복합 키 - 비식별 관계 매핑

JPA 는 Persistence Context 에 Entity 를 보관할 때 Entity 의 식별자를 키로 사용한다.
(Map 에서 Key 를 기반으로 Value 를 찾듯)

식별자(기본 키) 필드가 2개 이상이면 별도의 식별자 클래스를 만들어 해당 클래스의
equals(), hashCode() 메서드를 구현해야한다.
(식별자 필드가 2개 이상이기에 식별자 클래스를 만들어 해당 클래스의
hashCode 를 Persistence Context 의 Entity key 로 사용하고
equals() 연산으로 같은 식별자인지 확인하기 위해서가 아닐까?)

복합 키 - 비식별 관계 매핑을 위해 아래 2가지 방법 중 자신에게 맞는 방법을 사용하면 된다.

참고로 복합 키에는 @GeneratedValue 를 사용할 수 없으며, 복합 키를 구성하는 컬럼 중 어떤 하나에도
사용할 수 없다.

  • Table
    parent

스크린샷 2020-09-09 오후 4 55 22

child

스크린샷 2020-09-09 오후 4 55 30

grand_child

스크린샷 2020-09-09 오후 4 55 38

  • @IdClass

ParentId.java

    
    /**
     * @IdClass 식별자 클래스로 사용할 클래스는
     * 1. 식별자 클래스의 변수명과 Entity 에서 사용하는 변수명이 일치해야한다.
     * 2. Serializable 인터페이스를 implements 해야한다.
     * 3. equals(), hashCode() 를 구현해야 한다.
     * 4. 기본 생성자가 있어야 한다.
     * 5. public class 여야 한다.
     */
    @NoArgsConstructor
    @Setter
    @Getter
    public class ParentId implements Serializable {
    
        private String parentId1;
        private String parentId2;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ParentId parentId = (ParentId) o;
            return Objects.equals(parentId1, parentId.parentId1) &&
                    Objects.equals(parentId2, parentId.parentId2);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(parentId1, parentId2);
        }
    }
    

Parent.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    // 복합 키를 지정한 식별자 클래스를 알려 줌
    @IdClass(ParentId.class)
    @Table(name = "id_class_parent")
    @Entity(name = "IdClassParent")
    public class Parent {
    
        @Column(name = "parent_id_1")
        @Id
        private String parentId1;
    
        @Column(name = "parent_id_2")
        @Id
        private String parentId2;
    
        private String name;
    
        @Override
        public String toString() {
            return "Parent{" +
                    "parentId1='" + parentId1 + '\'' +
                    ", parentId2='" + parentId2 + '\'' +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

Child.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "id_class_child")
    @Entity(name = "IdClassChild")
    public class Child {
    
        @Column(name = "chlid_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        @ManyToOne
        @JoinColumns(
                {
                /**
                 * name : Child Table 에 있는 컬럼 명, referencedColumnName : Parent Table 에 있는 컬럼 명
                 * 부모 테이블에 있는 복합 키로 사용되는 컬럼명들과 자식 테이블에 외래 키로 사용될 키들의 컬럼명이 다를 경우
                 * referencedColumnName 로 따로 부모 테이블의 복합 키 관련 컬럼명들을 지정해준다.
                 */
                @JoinColumn(name = "CHILD_PARENT_ID_1", referencedColumnName = "PARENT_ID_1")
                , @JoinColumn(name = "CHILD_PARENT_ID_2", referencedColumnName = "PARENT_ID_2")
                }
        )
        private Parent parent;
    
        private String name;
    
        @Override
        public String toString() {
            return "Child{" +
                    "id=" + id +
                    ", parent=" + parent +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
  • @EmbeddedId

ParentId.java

    
    /**
     * @IdClass 로 설정해 사용하던 식별자 클래스와는 다르게
     * @Embeddable 어노테이션이 붙어있으며
     *
     * 이 식별자 클래스에 있는 컬럼을 바로 사용한다.
     * (@IdClass 에서는 부모 Entity 에 있는 변수 명과 @IdClass 로 사용 할 식별자 클래스의 변수 명이
     * 일치해야하던 것돠 대비된다.)
     *
     * 1. @Embeddable 어노테이션을 붙여줘야한다.
     * 2. Serializable Interface 를 implements 해야한다.
     * 3. equals(), hashCode() 를 구현해야한다.
     * 4. 기본 생성자가 있어야 한다.
     * 5. public class 여야 한다.
     */
    @Setter
    @Getter
    @Embeddable
    public class ParentId implements Serializable {
    
        @Column(name = "parent_id_1")
        private String parentId1;
    
        @Column(name = "parent_id_2")
        private String parentId2;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ParentId parentId = (ParentId) o;
            return Objects.equals(parentId1, parentId.parentId1) &&
                    Objects.equals(parentId2, parentId.parentId2);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(parentId1, parentId2);
        }
    
        /**
         * @IdClass 를 사용할 때엔 부모 Entity 에 복합 키 관련 변수가 있어서 부모 Entity 에 toString 을 구현하면 되었지만
         * @EmbeddedId 를 사용하면 부모 Entity 에서 복합 키로 설정한 부모 Entity 변수에 @EmbeddedId 로 설정한
         * @Embeddable 로 설정하고 객체를 가져다 복합 키로 사용하기 때문에
         *
         * 복합 키에 해당하는  @Embeddable 클래스에 이와 같이 toString() 을 정의해줬다.
         */
        @Override
        public String toString() {
            return "ParentId{" +
                    "parentId1='" + parentId1 + '\'' +
                    ", parentId2='" + parentId2 + '\'' +
                    '}';
        }
    }
    

Parent.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "embedded_id_parent")
    @Entity(name = "EmbeddedIdParent")
    public class Parent {
    
        // @Embeddable 로 설정된 식별자 클래스임을 나타냄
        @EmbeddedId
        private ParentId id;
    
        private String name;
    
        @Override
        public String toString() {
            return "Parent{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

Child.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "embedded_id_child")
    @Entity(name = "EmbeddedIdChild")
    public class Child {
    
        @Column(name = "child_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        @JoinColumns({
                /**
                 * name : Child Table 에 있는 컬럼 명, referencedColumnName : Parent Table 에 있는 컬럼 명
                 * 부모 테이블에 있는 복합 키로 사용되는 컬럼명들과 자식 테이블에 외래 키로 사용될 키들의 컬럼명이 다를 경우
                 * referencedColumnName 로 따로 부모 테이블의 복합 키 관련 컬럼명들을 지정해준다.
                 */
                @JoinColumn(name = "CHILD_PARENT_ID_1", referencedColumnName = "PARENT_ID_1")
                , @JoinColumn(name = "CHILD_PARENT_ID_2", referencedColumnName = "PARENT_ID_2")
        })
        @ManyToOne
        private Parent parent;
    
        private String name;
    
        @Override
        public String toString() {
            return "Child{" +
                    "id=" + id +
                    ", parent=" + parent +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

4. 복합 키 - 식별 관계 매핑

복합 키 - 식별관계의 경우 부모 - 자식 - 손자 Table 까지 기본 키를 계속해서 전달하는 식별 관계다.
자식 Table 은 부모 Table 의 기본 키를 포함해서 복합 키를 구성해나가며,
부모 Table 의 기본 키를 복합 키 이자 외래 키 로 사용하게 된다.

  • Table
    parent

스크린샷 2020-09-09 오후 4 55 56

child

스크린샷 2020-09-09 오후 4 56 07

grand_child

스크린샷 2020-09-09 오후 4 56 18

  • @ClassId

Parent.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "identifying_relationship_id_class_parent")
    @Entity(name = "IdentifyingRelationshipIdClassParent")
    public class Parent {
    
        @Column(name = "parent_id")
        @Id
        private String parentId;
    
        private String name;
    
        @Override
        public String toString() {
            return "Parent{" +
                    "parentId='" + parentId + '\'' +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

ChildId.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    public class ChildId implements Serializable {
    
        // Parent 의 기본 키(식별자) 타입이 String 이고, ChildId 를 식별자 클래스로 사용할 Child Entity 의 Parent 연관관계 변수명이 parent
        private String parent;
        private String childId;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ChildId childId1 = (ChildId) o;
            return Objects.equals(parent, childId1.parent) &&
                    Objects.equals(childId, childId1.childId);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(parent, childId);
        }
    
        @Override
        public String toString() {
            return "ChildId{" +
                    "parent='" + parent + '\'' +
                    ", childId='" + childId + '\'' +
                    '}';
        }
    }
    

Child.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @IdClass(ChildId.class)
    @Table(name = "identifying_relationship_id_class_child")
    @Entity(name = "IdentifyingRelationshipIdClassChild")
    public class Child {
    
        @Column(name = "child_id")
        @Id
        private String childId;
    
        @JoinColumn(name = "child_parent_id")
        @ManyToOne
        @Id
        private Parent parent;
    
        private String name;
    
        @Override
        public String toString() {
            return "Child{" +
                    "childId='" + childId + '\'' +
                    ", parent=" + parent +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

GrandChildId.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    public class GrandChildId implements Serializable {
    
        private ChildId child;
        private String grandChildId;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            GrandChildId that = (GrandChildId) o;
            return Objects.equals(child, that.child) &&
                    Objects.equals(grandChildId, that.grandChildId);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(child, grandChildId);
        }
    
        @Override
        public String toString() {
            return "GrandChildId{" +
                    "child=" + child +
                    ", grandChildId='" + grandChildId + '\'' +
                    '}';
        }
    }
    

GrandChild.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @IdClass(GrandChildId.class)
    @Table(name = "identifying_relationship_id_class_grand_child")
    @Entity(name = "IdentifyingRelationshipIdClassGrandChild")
    public class GrandChild {
    
        @Column(name = "grand_child_id")
        @Id
        private String grandChildId;
    
        @JoinColumns(
                {
                        @JoinColumn(name = "grand_child_grand_parent_id", referencedColumnName = "child_parent_id")
                        , @JoinColumn(name = "grand_child_parent_id", referencedColumnName = "child_id")
                }
        )
        @ManyToOne
        @Id
        private Child child;
    
        private String name;
    
        @Override
        public String toString() {
            return "GrandChild{" +
                    "grandChildId='" + grandChildId + '\'' +
                    ", child=" + child +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    
  • @EmbeddedId Parent.java
    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "identifying_relationship_embedded_id_parent")
    @Entity(name = "IndentifyingRelationshipEmbeddedIdParent")
    public class Parent {
    
        @Column(name = "parent_id")
        @Id
        private String id;
    
        private String name;
    
        @Override
        public String toString() {
            return "Parent{" +
                    "id='" + id + '\'' +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

ChildId.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Embeddable
    public class ChildId implements Serializable {
    
        // Child Entity @MapsId("parentId") 와 매핑됨
        @Column(name = "child_parent_id")
        private String parentId;
    
        @Column(name = "child_id")
        private String childId;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ChildId childId1 = (ChildId) o;
            return Objects.equals(parentId, childId1.parentId) &&
                    Objects.equals(childId, childId1.childId);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(parentId, childId);
        }
    
        @Override
        public String toString() {
            return "ChildId{" +
                    "parentId='" + parentId + '\'' +
                    ", childId='" + childId + '\'' +
                    '}';
        }
    }
    

Child.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "identifying_relationship_embedded_id_child")
    @Entity(name = "IdentifyingRelationshipEmbeddedIdChild")
    public class Child {
    
        @EmbeddedId
        private ChildId childId;
    
        // 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻
        @MapsId(value = "parentId")
        @JoinColumn(name = "child_parent_id", referencedColumnName = "parent_id")
        @ManyToOne
        private Parent parent;
    
        private String name;
    
        @Override
        public String toString() {
            return "Child{" +
                    "childId=" + childId +
                    ", parent=" + parent +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

GrandChildId.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Embeddable
    public class GrandChildId implements Serializable {
    
        private ChildId grandChildParentId;
    
        @Column(name = "grand_child_id")
        private String grandChildId;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            GrandChildId that = (GrandChildId) o;
            return Objects.equals(grandChildParentId, that.grandChildParentId) &&
                    Objects.equals(grandChildId, that.grandChildId);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(grandChildParentId, grandChildId);
        }
    
        @Override
        public String toString() {
            return "GrandChildId{" +
                    "grandChildParentId=" + grandChildParentId +
                    ", grandChildId='" + grandChildId + '\'' +
                    '}';
        }
    }
    

GrandChild.java

    
    @NoArgsConstructor
    @Setter
    @Getter
    @Table(name = "identifying_relationship_embedded_id_grand_child")
    @Entity(name = "IdentifyingRelationshipEmbeddedIdGrandChild")
    public class GrandChild {
    
        @EmbeddedId
        private GrandChildId grandChildId;
    
        @JoinColumns({
                @JoinColumn(name = "grand_child_grand_parent_id", referencedColumnName = "child_parent_id")
                , @JoinColumn(name = "grand_child_parent_id", referencedColumnName = "child_id")
        })
        // 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻
        @MapsId(value = "grandChildParentId")
        @ManyToOne
        private Child child;
    
        private String name;
    
        @Override
        public String toString() {
            return "GrandChild{" +
                    "grandChildId=" + grandChildId +
                    ", child=" + child +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
    

3. Proxy

연관관계를 맺게되면 Entity 를 불러올 때 해당 Entity 와 연관된 Entity 의 Table 을 같이 조회(Join)한다.
하지만 연관된 Entity 를 항상 사용하는 것이 아니라면 실제로 사용될 때까지 DB 조회를 지연하는 것이
좋은데 이를 지연 로딩이라고 한다.

지연 로딩 기능을 사용하기 위해서는 실제 Entity 객체 대신에 해당 Entity 인 것처럼 행동하다
실제 사용될 때 까지 DB 조회를 지연하기 위한 가짜 객체가 필요한데 이것은 Proxy 객체라고 한다.

1. Entity 조회 시 Proxy 객체로 조회하기

    
    /**
     * Entity 조회(즉시 로딩) : entityManager.find();
     * Entity Proxy 객체 조회(지연 로딩) : entityManager.getReference();
     *
     * Proxy Class 의 경우 실제 Entity Class 를 상속받아 만들어지기 때문에
     * Entity 와 겉모습이 같다.
     *
     * 하지만 entityManager.find() 를 통해 조회할 시 해당 Entity 를 즉시 조회하도록
     * Persistence Context 에 요청하고 Persistence Context 에 없으면 DB 를 조회하는데,
     *
     * entityManager.getReference() 를 통해 조회할 시 해당 Entity 를 상속 받은 Proxy 객체가 리턴되고,
     * Proxy 객체는 마치 해당 Entity 처럼 다뤄지는데 빈 값이 들어있는 껍데기에 불과하다.
     *
     * 실제로 referenceMember1.getUserName() 처럼 사용될 때 Persistence Context 에 실제 엔티티 정보를 가져오도록
     * 요청을 한다.(DB 에 SELECT 쿼리가 나가며 Entity 정보를 가져와서 프록시 객체에 세팅한다. => 조회해와도 Type 이 Proxy Class 이다.)
     *
     * 만약에 entityManager.getReference() 를 사용해서 조회하는 Entity 가 Persistence Context 에 존재하는 Entity 라면
     * 바로 해당 Entity 를 리턴하기 때문에 Type 이 Entity Class 이다.
     * (Proxy Class 를 사용하는 이유는 불필요한 쿼리를 발생시키지 않고 정말 필요할 때 쿼리를 발생시키기 위함인데
     *  이미 Persistence Context 에 있는 Entity 일 경우에는 쿼리를 사용하지 않아도 가져올 수 있기 때문이라고 생각한다.)
     *
     * Persistence Context 해당 Entity 정보를 조회 요청을 하고, Persistence Context 해당 Entity 정보가 없으면 빈 껍데기인 Proxy Class 가
     * 실제로 사용될때에 DB 에 SELECT 쿼리를 보내서 Persistence Context 에 해당 Entity 의 정보를 가져와 세팅하거나
     * Persistence Context 에 해당 Entity 정보를 조회 요청을 하고, Persistence Context 해당 Entity 정보가 있으면,
     * Entity 를 그대로 받아서 Proxy Class 가 아닌 해당 Entity 를 리턴하는 등 Persistence Context 에게 의존하고 있다.
     *
     * 따라서 이 Proxy Class 는 준영속 상태가 되면 사용이 불가능 하다.
     *
     */
    Member referenceMember1 = entityManager.getReference(Member.class, member1.getId());
    

2. Proxy 특징

  • Proxy 는 실제 사용될 떄에 Persistence Context 에 실제 Entity 생성을 요청하는데 이것이 Proxy 초기화.

  • Persistence Context 는 Proxy 로 부터 Entity 생성을 요청받는데 이 때 DB 를 조회한다.

  • Persistence Context 로 부터 Entity 객체를 받으면 이에 대한 참조를 Proxy 객체 내 변수에 저장한다.

  • entityManager.getReference() 메서드를 통해 Proxy 객체를 생성하려할때 해당 Entity 가 Persistence Context 에
    존재한다면 Proxy 객체가 아닌 Entity 를 리턴한다.

  • 처음 사용할 때 한 번만 조회 된다.

  • Proxy 객체가 초기화 되더라도 Proxy 객체가 Entity 로 바뀌는 것은 아니다.

  • Proxy 객체는 원본 Entity 상속받은 객체이다.

  • 따라서 타입 체크시에 주의해야한다.

  • Proxy 객체가 실제 사용될 때 초기화를 한다고 했는데 team.getId() 와 같이 식별자를 조회하는
    행위를 하면 프록시가 초기화 되지 않는다.
    (entityManager.getReference() 메서드 호출시에 이미 식별자를 넘겨서 Proxy 객체가 들고 있기 때문)

3. 즉시 로딩

조회하려는 Entity 와 연관된 Entity 를 즉시 조회한다.
(Hibernate 는 가능한 SQL Join 을 통해 한 번에 조회)

4. 지연 로딩

조회하려는 Entity 와 연관된 Entity 를 Proxy 로 조회한다.
(Proxy 를 실제로 사용할 때 초기화를 통해 DB를 조회한다.)

5. Fetch 전략

개발시에 연관관계에 있는 Entity 들 중 어떤 Entity 들이 자주 함께 사용되고,
어떤 Entity 는 필요할 때 조회해오는 것이 좋을지 판단하기 힘들다.

따라서 개발시에는 모든 연관관계에 지연 로딩을 사용하고,
어느정도 틀이 잡히고 서비스에 대한 도메인 지식이 생기면
즉시 로딩이 필요하다고 판단 되는 경우 즉시 로딩을 적용하면 될 것 같다.

6. 영속성 전이

나중에 필요할 때 공부해보도록 하겠다.

4. 값 타입

JPA 에는 Entity 와 Value 두가지 type 이 있다.
Entity 는 Table 과 매핑되는 Entity 객체이며,
Value 타입은 Entity 에서 값 처럼 사용되는 객체이다.

일반적으로 값 타입은 Entity 의 프로퍼티 중 연관있는 프로퍼티를 묶어
Value Class 로 만들고 이를 @Embeddable 로 선언해 값 객체임을 나타내고
Entity 에서 @Embedded 를 통해 값 객체를 사용하고 있음을 나타낸다.

Value Class 를 사용해서 Entity 를 구성하면 어떤 이점이 있는지 코드를 통해 이해해보자.

MemberWithOutEmbedded.java

    
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    @Table(name = "member_with_out_embedded")
    @Entity(name = "MemberWithOutEmbedded")
    public class MemberWithOutEmbedded {
    
        @Column(name = "member_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        private String name;
    
        @Column(name = "member_city")
        private String city;
    
        private String street;
    
        @Column(name = "zip_code")
        private String zipcode;
    
        private LocalDate startDate;
    
        private LocalDate endDate;
    
        @Override
        public String toString() {
            return "MemberWithOutEmbedded{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", city='" + city + '\'' +
                    ", street='" + street + '\'' +
                    ", zipcode='" + zipcode + '\'' +
                    ", startDate=" + startDate +
                    ", endDate=" + endDate +
                    '}';
        }
    }
    

Table 스크린샷 2020-09-11 오후 10 39 32

위와 같은 Entity 가 있다고 하자, 해당 Entity 에는 여러 프로퍼티가 있다.
총 7개의 프로퍼티가 있는데 이름, 나이, 사는 도시, 사는 곳의 도로명, 사는 곳의 우편코드,
시작일, 종료일이 있다.

이 Entity 는 어떤 회사의 직원 정보들이라고 할 때, 크게 식별자, 이름, 사는 곳(집 주소), 근무기간
4가지 정보를 담고 있다.

이렇게 4가지 정보를 담고있는데 Entity 의 프로퍼티는 7개로 너무 많다고 느껴진다.
7개의 프로퍼티는 직원 Entity 에 필요한 프로퍼티가 맞지만 객체지향적으로 묶어서 나타낼 수 있다면
더 좋다고 생각된다.

아래의 임베디드 타입을 보고 한번 생각해보자.

1. 임베디드 타입(복합 값 타입)

Member.java

    
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Table(name = "value_type_embedded_type_member")
    @Entity(name = "ValueTypeEmbeddedTypeMember")
    public class Member {
    
        @Column(name = "member_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        private String name;
    
        // 값 타입을 사용
        @Embedded
        private Period period;
    
        // 값 타입을 사용
        @Embedded
        private Address address;
    
        @Override
        public String toString() {
            return "Member{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", period=" + period +
                    ", address=" + address +
                    '}';
        }
    }
    

Period.java

    
    /**
     * 값 으로 사용할 객체이기 때문에
     * Setter 를 없애고 값 변경이 필요할 경우
     * 새로 만들어서 사용하도록 유도
     */
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    // 값 타입을 정의
    @Embeddable
    public class Period {
    
        private LocalDate startDate;
    
        private LocalDate endDate;
    
        @Override
        public String toString() {
            return "Period{" +
                    "startDate=" + startDate +
                    ", endDate=" + endDate +
                    '}';
        }
    }
    

Address.java

    
    /**
     * 값 으로 사용할 객체이기 때문에
     * Setter 를 없애고 값 변경이 필요할 경우
     * 새로 만들어서 사용하도록 유도
     */
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    // 값 타입을 정의
    @Embeddable
    public class Address {
    
        // 이 값 객체를 사용하는 Table 에 어떤 컬럼 명으로 사용할지 설정 가능
        @Column(name = "member_city")
        private String city;
    
        private String street;
    
        // 이 값 객체를 사용하는 Table 에 어떤 컬럼 명으로 사용할지 설정 가능
        @Column(name = "zip_code")
        private String zipcode;
    
        @Override
        public String toString() {
            return "Address{" +
                    "city='" + city + '\'' +
                    ", street='" + street + '\'' +
                    ", zipcode='" + zipcode + '\'' +
                    '}';
        }
    }
    

Table 스크린샷 2020-09-11 오후 10 39 32

만들어지는 Table 의 컬럼 수는 똑같지만 Entity 의 프로퍼티를 보게되면 위와 같이 Embedded type 을 사용해서
구현하는 것이 관련 Class 수는 많아지지만 프로퍼티 수가 줄어들고 더욱 명확히 의미를 나타낼 수 있다.

나는 의미를 더 명확하게 나타낼 수 있다면 프로퍼티 1개에 대해서라도 의미를 더욱 명확히 나타낼 수 있다면
해당 프로퍼티를 위한 값 객체를 생성하는 것이 개인적으로 맞다고 생각한다.

2. 임베디드 타입 응용(복합 값 타입)

아래 Period 라는 Value Class 를 보자,

    
    /**
     * 값 으로 사용할 객체이기 때문에
     * Setter 를 없애고 값 변경이 필요할 경우
     * 새로 만들어서 사용하도록 유도
     */
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    // 값 타입을 정의
    @Embeddable
    public class Period {
    
        private LocalDate startDate;
    
        private LocalDate endDate;
    
        @Override
        public String toString() {
            return "Period{" +
                    "startDate=" + startDate +
                    ", endDate=" + endDate +
                    '}';
        }
    }
    

이 Value Class 는 시작과 끝을 나타내는 기간을 표현하므로 여러 군데서 다양한 의미로 사용될 수 있다.
예를 들어 입사일 - 퇴직일과 같은 근무 기간일 수 도 있고, 입학일 - 졸업일과 같은 재학 기간일 수도 있다.

따라서 사용하는 곳에 따라 시작일 - 종료일에 대한 컬럼명이 다를 수도 있고 해당 Value Class 에 정의한
매핑 정보를 재정의해야할 때가 있다.

아래의 코드를 보며 재정의를 어떻게 하는지, 어떨 때 하는지 깨달아보도록 하겠다.

Table
employee
스크린샷 2020-09-13 오후 2 22 16

student
스크린샷 2020-09-13 오후 2 22 28

Period.java

    
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Embeddable
    public class Period {
    
        // 해당 프로퍼티에 대한 컬럼 명 지정(사용하는 곳에서 @AttributeOverride 속성을 사용해 재정의 가능)
        @Column(name = "start_date")
        private LocalDate startDate;
    
        // 해당 프로퍼티에 대한 컬럼 명 지정(사용하는 곳에서 @AttributeOverride 속성을 사용해 재정의 가능)
        @Column(name = "end_date")
        private LocalDate endDate;
    
        @Override
        public String toString() {
            return "Period{" +
                    "startDate=" + startDate +
                    ", endDate=" + endDate +
                    '}';
        }
    }
    

Employee.java

    
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    @Table(name = "value_type_embedded_type_advanced_employee")
    @Entity(name = "ValueTypeEmbeddedTypeAdvancedEmployee")
    public class Employee {
    
        @Column(name = "employee_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        private String name;
    
        /**
         * @Embedded 로 사용한 @Embeddable 로 선언된 Value Class 의 속성을 재정의 함
         * @AttributeOverrides @Embeddable 로 선언된 Value Class 의 여러 속성을 재정의 할 때 사용
         * @AttributeOverride(name = "재정의 할 Value Class 의 프로퍼티", column = @Column(name = "어떤 컬럼명으로 사용하고싶은지")
         */
        @AttributeOverrides({
                @AttributeOverride(name = "startDate", column = @Column(name = "employment_start_date"))
                , @AttributeOverride(name = "endDate", column = @Column(name = "employment_end_date"))
        })
        @Embedded
        private Period employmentPeriod;
    
        @Override
        public String toString() {
            return "Employee{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", employmentPeriod=" + employmentPeriod +
                    '}';
        }
    }
    

Student.java

    
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Table(name = "value_type_embedded_type_advanced_student")
    @Entity(name = "ValueTypeEmbeddedTypeAdvancedStudent")
    public class Student {
    
        @Column(name = "student_id")
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Id
        private Integer id;
    
        private String name;
    
        /**
         * @Embedded 로 사용한 @Embeddable 로 선언된 Value Class 의 속성을 재정의 함
         * @AttributeOverrides @Embeddable 로 선언된 Value Class 의 여러 속성을 재정의 할 때 사용
         * @AttributeOverride(name = "재정의 할 Value Class 의 프로퍼티", column = @Column(name = "어떤 컬럼명으로 사용하고싶은지")
         */
        @AttributeOverrides({
                @AttributeOverride(name = "startDate", column = @Column(name = "attendance_start_date"))
                , @AttributeOverride(name = "endDate", column = @Column(name = "attendance_end_date"))
        })
        @Embedded
        private Period attendancePeriod;
    
        @Override
        public String toString() {
            return "Student{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", attendancePeriod=" + attendancePeriod +
                    '}';
        }
    }
    

3. QueryDSL

앞에서는 테이블에 대한 연관관계를 설정하는 법을 익혔다면,
이젠 객체를 활용하여 쿼리를 어떻게 작성하는지를 익혀보자.
JPQL, Criteria 를 사용할 수 있지만 나는 조금 범위를 스킵해서 바로 QueryDSL 를 살펴보려한다.

일단 사용법부터 익히자.. 그러기위해서는 일단 코드가 짱이다.

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("query");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction entityTransaction = entityManager.getTransaction();

        try {

            entityTransaction.begin();

            Member member1 = new Member(null, "member1", 28);
            entityManager.persist(member1);

            Posts posts1 = new Posts(null, "title1", "content1", member1.getId());
            entityManager.persist(posts1);

            Member member2 = new Member(null, "member2", 28);
            entityManager.persist(member2);

            Posts posts2 = new Posts(null, "title2", "content2", member2.getId());
            entityManager.persist(posts2);

            entityManager.clear();
            JPAQuery<Member> query = new JPAQuery(entityManager);
            QMember member = new QMember("myQMember");

            List<Member> findMembers = query.from(member).where(member.age.eq(28)).fetch();

            System.out.println("findMembers info are " + findMembers);

            JPAQuery<Posts> queryForPosts = new JPAQuery(entityManager);
            QPosts posts = new QPosts("myQPosts");
            List<Posts> findPosts = queryForPosts.from(posts).where(posts.memberId.eq(findMembers.get(0).getId())).fetch();

            System.out.println("findPosts info are " + findPosts);

            entityTransaction.commit();

        } catch(Exception e) {
            entityTransaction.rollback();
            System.out.println(e.toString());
        } finally {
            entityManager.close();
        }
        entityManagerFactory.close();
    }
}

일단 테이블에 매핑되는 객체는 Entity 가 아니라 그것을 한번감싼 QEntity 가 된다.
그리고 JPAQuery 를 EntityManager 로부터 생성하는것도 보인다.

그럼 생각해야할거는 아래와 같다.

  1. JPAQuery 인스턴스를 만들어서 Query 를 작성할 준비를 한다.
  2. QEntity 기준으로 테이블 매핑을 한다.
  3. DSL 을 활용해 쿼리를 작성해 나간다.

내가 이해한 바로는 QClass 들의 경우 컴파일 타임에 빌드되어 생성되고
그것을 런타임때 사용하는 것 같다.