JPA 개념 정리와 예시 코드

choko's avatar
Jun 29, 2024
JPA 개념 정리와 예시 코드
 
 

대표적인 JPA 매핑 어노테이션

  • @Entity
    • JPA를 사용해 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다.
      • JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하므로, 이 생성자는 반드시 있어야 한다.
  • @Table(객체와 테이블 매핑)
    • 엔티티와 매핑할 테이블을 지정한다.
  • @Id(기본 키 매핑)
  • @Column(필드와 컬럼 매핑)
  • @ManyToOne, @JoinColumn(연관관계 매핑)
 
 

연관관계

  • 연관간계
    • 객체와 관계형 데이터베이스 테이블을 매핑해 주어야 한다.
    • 연관 관계 정의 규칙
      • 방향
        • 단방향, 양방향
        • 두 객체 모두 참조용 필드가 있으면 양방향, 한 객체만 참조용 필드가 있으면 단방향 연관관계라고 부른다.
        • 무조건 양방향 관계를 맺으면 된다고 생각할 수 있지만, 모든 엔티티를 양방향으로 하면 너무 복잡해진다.
          • 따라서, 기본적으로 단방향 매핑을 하고 꼭 필요한 경우 양방향 매핑을 설정해주자.
      • 연관 관계의 주인
        • 양방향인 경우, 연관 관게에서의 관리 주체
        • 외래키가 있는 곳을 연관관계의 주인으로 정한다.
        • 설정 이유 - 연관 관계의 주인을 명확히 설정해 주지 않으면 JPA 입장에서 혼란을 주기 때문
      • 다중성
        • 다대일(N:1)
        • 일대다(1:N)
        • 일대일(1:1)
        • 다대다(N:M)
         

양방향, 단방향 연관관계

  • 양방향 연관관계
    • 양쪽 엔티티가 서로의 상태를 자주 참조할 때 사용한다.
    • @Entity public class Parent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 양방향 연관관계 설정 @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) private List<Child> children = new ArrayList<>(); } @Entity public class Child { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 양방향 연관관계 설정 @ManyToOne @JoinColumn(name = "parent_id") private Parent parent; }
    • Parent 쪽에 @OneToMany, Child쪽에 @ManyToOne 존재
    • 양방향 연관관계의 경우 연관관계의 주인을 설정해 주어야 한다.
      • 연관관계의 주인 : A→B, B→A 중 제어의 권한을 갖는 실직적인 관계가 어떤 것인지 Jpa에게 알려줌 → mappedBy() 속성을 통해 지정
 
  • 단방향 연관관계
    • 연관 엔티티가 간단한 구조로, 읽기 중심인 경우 단방향 연관관계 사용
    • @Entity public class Parent { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 단방향 연관관계 설정 @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "parent_id") private List<Child> children = new ArrayList<>(); } @Entity public class Child { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; }
    • Parent 쪽에만 @OneToMany 존재
 
 

다중성

참고로, 다대다(N:M)는 유지보수, 복잡한 쿼리의 이유로 실무에서 사용하지 않는다고 한다.
  • 다대일(N:1)
    • 하나의 게시판(Board)과 게시글(Post) 예시
      • 단방향
        • @Entity public class Post { @Id @GeneratedValue @Column(name = "POST_ID") private Long id; @Column(name = "TITLE") private String title; @ManyToOne @JoinColumn(name = "BOARD_ID") private Board board; //... getter, setter } @Entity public class Board { @Id @GeneratedValue private Long id; private String title; //... getter, setter } 출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그:티스토리]
        • post 쪽에 @ManyToOne만 할당하였다.
      • 양방향
        • @Entity public class Post { @Id @GeneratedValue @Column(name = "POST_ID") private Long id; @Column(name = "TITLE") private String title; @ManyToOne @JoinColumn(name = "BOARD_ID") private Board board; //... getter, setter } @Entity public class Board { @Id @GeneratedValue private Long id; private String title; @OneToMany(mappedBy = "board") List<Post> posts = new ArrayList<>(); //... getter, setter } 출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그:티스토리]
        • post 쪽에 @ManyToOne만 할당하였다.
        • board쪽에는 @OneToMany 가 할당되었고, Post가 ArrayList로 담겨진다.
         
 
  • 일대일(1:1)
    • 하나의 게시글(Post)와 첨부파일(Attach)로 예시
      • 단방향
        • @Entity public class Post { @Id @GeneratedValue @Column(name = "POST_ID") private Long id; @Column(name = "TITLE") private String title; @OneToOne @JoinColumn(name = "ATTACH_ID") private Attach attach; //... getter,setter } @Entity public class Attach { @Id @GeneratedValue @Column(name = "ATTACH_ID") private Long id; private String name; //... getter, setter } 출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그:티스토리]
           
      • 양방향
        • @Entity public class Post { @Id @GeneratedValue @Column(name = "POST_ID") private Long id; @Column(name = "TITLE") private String title; @OneToOne @JoinColumn(name = "ATTACH_ID") private Attach attach; //... getter,setter } @Entity public class Attach { @Id @GeneratedValue @Column(name = "ATTACH_ID") private Long id; private String name; @OneToOne(mappedBy = "attach") private Post post; //... getter, setter } 출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그:티스토리]
 

상속 관계 매핑

조인 전략과 단일 테이블 전략 중 선택한다.
  • 조인 전략 - @Inheritance(strategy = InheritanceType.JOINED)
  • 단일 테이블 전략 - @Inheritance(strategy = InheritanceType.SINGLE_TABLE
  • 상속 관계 예시
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn @Getter @Setter public class Item { @Id @GeneratedValue @Column private Long id; private String name; private int price; } @Entity @DiscriminatorValue("A") @Getter @Setter public class Album extends Item{ private String artist; } @Entity @DiscriminatorValue("B") @PrimaryKeyJoinColumn(name = "BOOK_ID") @Getter @Setter public class Book extends Item{ private String author; private String isbn; }
  • @Inheritance : 상속 관계 매핑은 부모 클래스에 @Inheritance를 사용하여야 한다. 조인 전략에 따라 Type을 명시해 준다.
  • @DiscriminatorColumn(name = "DTYPE") : 부모 클래스에 구분 칼럼을 명시, 기본값은 DTYPE 이다.
  • @DiscriminatorValue("A") : 자식 타입에 엔터티 구분 칼럼 값을 명시한다. 부모 클래스의 @DiscriminatorColumn 칼럼에 “A”가 들어가게 된다.
 
조인 전략은 Item과 Album/Book 테이블이 따로 생성된다. 단일 테이블 전략은 item 테이블에 Album/Book 칼럼들이 모두 포함되고, 빈 값은 null이 들어가게 된다.
 
 

@MappedSuperclass

  • 부모 클래스는 테이블과 매핑하지 않고, 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶다면 @MappedSuperclass를 사용한다.
  • 하단 예시에서, Member와 Seller 테이블에 id, name 칼럼이 추가된다.
@MappedSuperclass public abstract class BaseEntity { @Id @GeneratedValue private Long id; private String name; } @Entity public class Member extends BaseEntity { private String email; } @Entity public class Seller extends BaseEntity { private String shopName; }
  • @AttributeOverrid 로 매핑 정보를 재정의 할 수 있다.
  • @AssocitaionOverride 로 연관관계를 재정의 할 수 있다.
  • BaseEntity 클래스는 테이블에 매핑되지 않고, 자식 클래스 엔티티의 매핑 정보 상속을 위해 사용한다.
 
 

복합 키와 식별 관계 매핑

  • 복합키: 두 가지 이상의 칼럼을 기본키로 지정하는 것
  • 식별 관계 : 부모 테이블의 기본 키를 내려받아 자식 테이블의 기본키 + 외래키로 사용한다.
  • 비식별 관계 : 부모 테이블의 기본 키를 내려받아 자식 테이블의 외래키로만 사용한다.
  • 다음 이유로, 식별 관계보다는 비식별 관계를 주로 사용한다.
    • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서, 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 또 복합키별로 복합키 클래스를 만들어야 한다. → 복잡도가 늘어남
    • 비식별 관계의 기본 키는 주로 대리키를 사용하고 JPA는 @GeneratedValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.
    • → 될 수 있으면 비실별 관계를 사용하고, 기본 키는 Long 타입의 대리키를 사용하자.
 
  • 복합키를 사용할 때, 그냥 @Id를 두 가지 이상 칼럼에 지정하면 에러가 발생한다.
  • JPA는 복합키를 지원하기 위해 @IdClass와 @EmbeddedId 두 가지 방법을 제공한다.
 

@IdClass : 복합키 식별자 클래스를 하나 만든 후 @IdClass를 붙인다.

 
@Entity @IdClass(ParentId.class) @Setter @Getter public class Parent { @Id @Column private String id1; @Id @Column private String id2; @Column private String name; }
  • @IdClass(ParentId.class) 를 통해 식별자 클래스인 ParentId 명시를 해 주어야 한다.
  • 복합키로 세팅하고 싶은 key들에게 @Id를 붙인다.
 
@EqualsAndHashCode @RequiredArgsConstructor public class ParentId implements Serializable { @EqualsAndHashCode.Include public String id1; @EqualsAndHashCode.Include public String id2; }
  • 식별가 클래스 ParentId는 Serializable를 상속 받아야 한다.
  • equal, hashCode method를 구현해야 한다. 위에선 lombok의 @EqualsAndHashCode로 구현하였다.(@EqualsAndHashCode.Include 사용)
 
public interface ParentRepository extends JpaRepository<Parent, ParentId> { }
  • Repo는 다음과 같이 구현한다. JpaRepository<Parent, ParentId>
 
 

@EmbeddedId

  • @IdClass보다 더 객체지향적인 방법이다.
@Entity @Setter @Getter public class Parent { @EmbeddedId private ParentId parentId; @Column private String name; }
 
@EqualsAndHashCode @Embeddable @RequiredArgsConstructor public class ParentId implements Serializable { @EqualsAndHashCode.Include public String id1; @EqualsAndHashCode.Include public String id2; }
 
  • @IdClass 대신 @EmbeddedId, @Embeddable을 붙여 구현한다.
  • @Embedded의 경우, 쿼리조회 같은 경우 약간 더 쿼리문이 길어질 수 있다.
    • @IdClass의 경우에는 member.getId()와 같이 사용
    • @EmbeddedId의 경우에는 member.getParentId().getId()와 같이 사용해야 한다.
  • Embedded 타입의 JPA Query
    • 다음과 같이 EmbeddedId 클래스 내의 필드로 JPA query를 조회할 수 있다.
    • findByParentIdId1 // ParentId -> EmbeddedId // ID1 -> query하고싶은 embedded 클래스 내의 필드
       

조인 테이블

notion image
테이블 간 연관관계는, 조인 컬럼과 조인 테이블 두 가지 설계 방법이 있다.
  • 조인 칼럼
    • 조인 컬럼이라고 부르는 외래 키 컬럼을 사용해 관리한다.
    • 연관 테이블에 연관관계가 맺어지지 않으면, 외래키에 null이 저장되어 있게 된다. (단일 테이블 전략)
      • → 아주 가끔씩 데이터가 연간관계를 맺을 경우에는 비효율적이다.
  • 조인 테이블
    • 조인 테이블이라는 별도의 테이블을 사용하여 연관관계를 관리한다.
    • 조인 테이블에 두 테이블간의 외래 키를 가지고 연관관계를 관리한다.
    • 관리해야 할 테이블이 하나 늘어나고, 조인 시 조인 테이블까지 추가로 조인해야 한다.
 
→ 기본적으로 조인 칼럼을 사용하고, 필요하다고 생각되면 조인 테이블을 사용하자.
 
 
 

프록시

  • 지연 로딩 : Entity를 조회할 때, 연관된 Entity들은 조회 될 수도, 안 될 수도 있다. 불필요한 낭비를 줄이기 위해, JPA는 실제 Entity의 조회가 일어날 때까지 조회를 지연한다. 이를 지연 로딩이라고 한다.
    • 하단의 Fetch 참고
  • 프록시 객체 : 지연 로딩을 사용하려면 실제 Entity 대신 DB 조회를 지연할 수 있는 가짜 객체가 필요한데, 이를 프록시 객체라 한다.
→ JPA의 경우, 지연 로딩 구현 방법을 JPA 구현체에 위임했기 때문에, 직접 설정할 필요는 없다.
 

Fetch

  • 애플리케이션이 DB로부터 데이터를 가지고 오는것
  • DB에서 데이터를 가져오는 방법은, EAGER(즉시로딩), LAZY(지연로딩)이 있다.
    • FetchType.EAGER(즉시로딩)
      • 연관관계에 있는 Entity를 전부 로딩해서 영속성 컨텍스트에 가져온다.
      • @ManyToOne, @OneToOne 에서 Default로 세팅된다.
    • FetchType.LAZY(지연로딩)
      • 연견관계에 있는 Entity를 모두 가져오는게 아닌, 호출 시 그때 가져온다.
      • 위의 프록시 객체 가 이때 사용된다.
      • @OneToMany, @ManyToMany 에서 Default로 세팅된다.
  • N+1 문제
    • 연관관계가 설정된 엔티티를 조회하는 경우, 조회된 데이터의 개수만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 것
      • ex) 모든 팀들을 쿼리하는데(1), 해당 팀에 속한 멤버들의 쿼리(N)가 계속 반복되는 경우
      • 이 경우 성능 저하의 원인이 된다.
    • 해결 방법?
      • FetchType.LAZY
        • 연관관계의 Entity에 접근하는 경우가 아니라면, N+1 문제를 해결할 수 있다.
        • 하지만 연관관계 객체가 호출이 된다면, 해결이 되지 않는다.
      • Fetch Join 사용
        • JPQL 쿼리를 사용하여 Fetch 조인
        • List<Team> teams = entityManager.createQuery( "SELECT t FROM Team t JOIN FETCH t.members", Team.class).getResultList();
           
 

CasCade(영속성 전이)

  • 특정 Entity를 영속 상태로 만들 때 연관된 Entity도 함께 영속 상태로 만드는 기능
  • 부모 Entity를 저장할 때 자식 Entity도 저장할 수 있다. → 부모 Entity를 삭제할 때 자식 Entity도 삭제할 수 있다.
    • ALL : 모두 적용
    • PERSIST : 영속
    • REMOVE : 삭제
    • 주로 @OnetoMany에서 다음과 같이 사용한다.
      • @OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)
      •  
  • User와 Authority가 있다고 할 때
public class USERTEST{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) private List<AUTHORITYTEST> authorities = new ArrayList<>(); } public class AUTHORITYTEST { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ManyToOne @JoinColumn(name= "user_id") private USERTest user; }
  • User을 생성하고, 해당 유저의 Authority들을 부여하는 상황일 때
void jpaTest4() { USERTest user = new USERTest(); user.setUsername("test"); AUTHORITYTEST authority1 = new AUTHORITYTEST(); ... AUTHORITYTEST authority2 = new AUTHORITYTEST(); ... List<AUTHORITYTEST> authorities = new ArrayList<>(); list.add(authority1); list.add(authority2); user.setAuthorities(authorities); userTestRepository.save(user); }
  • cascade 옵션이 없는 경우
    • User 테이블의 test 데이터는 db에 저장되고, authority 테이블에는 데이터자 저장되지 않는다.
  • cascade = CascadeType.PERSIST 인 경우
    • User 테이블의 test 데이터는 db에 저장되고, authority 테이블에도 authority1, authority2가 저장된다.
    • test 데이터가 삭제되도, authority1, authority2는 삭제되지 않는다.
  • cascade = CascadeType.ALL 인 경우
    • PERSIST의 경우 뿐만 아니라, 모든 경우에 영속성이 적용된다.
    • test 데이터를 삭제하면, authority1, authority2도 삭제된다.
 
  • CasCade는 자식 엔티티가 하나의 부모 엔티티에 종속적일때만 사용해야 한다.
    • CasCade = ALL로 되어있을 경우 자식 엔티티의 DELETE 쿼리 등이 작동하지 않을 수 있다.
 
 

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

@Entity public class Member { @Id @GeneratedValue private Long id; private String name; @Embedded Period workPeriod; @Embedded Address homeAddress; } @Embeddable public class Period { // 필드 } @Embeddable public class Address { // 필드 }
  • @Embedded : 어노테이션으로 필드를 개별 클래스로 가질 수있다.
  • @Embeddable : 값 타입을 정의하는 곳에 표시
    • @Embeddable 클래스에도 연관관계 정의가 가능하다.
    •  

JPQL

  • Entity객체를 조회하는 객체지향 쿼리이다. SQL과 유사하지만 더 간결하다.
  • 특정 데이터베이스에 의존하지 않는다.

Criteria 쿼리

  • JPQL 빌더 클래스이다.
  • 컴파일 시점에 오류를 발견할 수 있고, IDE 자동완성이 되기 때문에 더 간편하다.
  • 동적 쿼리를 작성하기 편하다.
select m from Member as m where m.username = 'kim'
위 JPQL를 Criteria 쿼리로 바꾸면
/ ... // CriteriaBuilder, CriteriaQuery 객체 생성 코드 필요 / ... CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim")) List<Member> resultList = em.createQuery(cq).getResultList();
 

QueryDSL

  • Criteria처럼 JPQL 빌더 역할, Criteria보다 훨신 단순하고 사용하기 쉽다.
  • JPA 표준이 아닌 오픈소스 프로젝트이다.
  • 위 동일한 JPQL문 예시
JPAQuery query = new JPAQuery(em); List<Member> members = query.from(member).where(member.username.eq("kim")).list(member);
 
 

스프링 데이터 JPA

  • 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트
  • 지루하게 반복되는 CRUD 문제를 세련된 방법으로 처리할 수 있다.
  • 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
    • 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해주기 때문
    •  

공통 인터페이스 기능

  • JPARepsotiroy → 제네릭에 Entity 클래스와 식별자의 타입을 지정한다.
publid interface ExamRepository extends JPARepository<Example, Long> {}
  • JpaRepository→ PagingAndSortingRepository → CrudRepository → Repository
notion image
  • JpaRepository 인터페이스의 주요 메서드들
    • save(S)
    • delete(T)
    • findOne(ID)
    • getOne(ID) : Entity를 프록시로 조회한다.
    • findAll()
 
 

쿼리 메서드 기능

  • 컬럼&메서드 이름을 조합하여, 인터페이스에 등록만으로 사용할 수 있다.
  • 컬럼이 변하면, 메서드의 이름도 수정해 주어야 한다.
Keyword
Sample
JPQL snippet
Distinct
findDistinctByLastnameAndFirstname
select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And
findByLastnameAndFirstname
… where x.lastname = ?1 and x.firstname = ?2
Or
findByLastnameOrFirstname
… where x.lastname = ?1 or x.firstname = ?2
Is, Equals
findByFirstname,findByFirstnameIs,findByFirstnameEquals
… where x.firstname = ?1
Between
findByStartDateBetween
… where x.startDate between ?1 and ?2
LessThan
findByAgeLessThan
… where x.age < ?1
LessThanEqual
findByAgeLessThanEqual
… where x.age <= ?1
GreaterThan
findByAgeGreaterThan
… where x.age > ?1
GreaterThanEqual
findByAgeGreaterThanEqual
… where x.age >= ?1
After
findByStartDateAfter
… where x.startDate > ?1
Before
findByStartDateBefore
… where x.startDate < ?1
IsNull, Null
findByAge(Is)Null
… where x.age is null
IsNotNull, NotNull
findByAge(Is)NotNull
… where x.age not null
Like
findByFirstnameLike
… where x.firstname like ?1
NotLike
findByFirstnameNotLike
… where x.firstname not like ?1
StartingWith
findByFirstnameStartingWith
… where x.firstname like ?1 (parameter bound with appended %)
EndingWith
findByFirstnameEndingWith
… where x.firstname like ?1 (parameter bound with prepended %)
Containing
findByFirstnameContaining
… where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy
findByAgeOrderByLastnameDesc
… where x.age = ?1 order by x.lastname desc
Not
findByLastnameNot
… where x.lastname <> ?1
In
findByAgeIn(Collection<Age> ages)
… where x.age in ?1
 

JPA NamedQuery

  • 쿼리에 이름을 보여하여 사용하는 방법
  • @NamedQuery를 사용하여 name과 query를 지정
@Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @NamedQuery( name = "User.findByUseranme", query = "select u from User u where u.username = :username" ) public class User { @Id @GeneratedValue private Long id; private String username; }
 
  • 생성한 쿼리는 EntityManager의 createNamedQuery로 사용할 수 있다.
public List<User> findByUsername(String username) { return entityManager.createNamedQuery("User.findByUsername", User.class) .setParameter("username", "이름") .getResultList(); }
 
 

@Query

  • @Query를 사용하면 레퍼지토리 메서드에 직접 쿼리를 정의할 수 있다.
public interface AuthorRepository extends JpaRepository<User, Long> { List<User> findByFirstNameAndLastName(String firstName, String lastName); @Query("select u from user u where u.firstName = :firstName and u.lastName = :lastName") List<User> findByName(String firstName, String lastName); }
  • findByFirstNameAndLastName와 findByName는 동일한 역할을 수행한다.
 
 
 

페이징과 정렬

  • 페이징 : org.springframework.data.domain.Pageable (내부에 sort 포함)
    • PageRequest로 정렬 및 페이징 조건을 설정할 수 있다.
  • 정렬 : org.springframework.data.domain.Sort
 
  • name이 김씨로 시작하는 회원을 내림차순으로 10개씩 페이징하는 예제
    • // findByNameStartingWith는 인터페이스에 등록되어 있어야 한다. PageRequest pageRequest = new PageRequest(0,10,new Sort(Sort.Direction.DESC, "name")); Page<Member> result = memberRepository.findByNameStartingWith("김", pageRequest); List<Member> members = result.getContent(); // 조회된 데이터 int totalPages = result.getTotalPages(); // 전체 페이지 수 boolean hasNextPage = result.hasNext(); // 다음 페이지 존재 여부
    • Page 인터페이스는 다양한 메서드를 제공한다.
    •  
       
       

      TroubleShooting

      JPA 양방향 순환 참조
      save the transient instance before flushing
       
Share article

Tom의 TIL 정리방