관계 데이터베이스에서 가장 복잡하고도 중요한 개념은 연관관계라고 할 수 있습니다. 두 객체가 1:1관계인지, 1:N관계인지, 아니면 M:N관계인지 파악하고 테이블을 먼저 설계해야 합니다.
예제 프로젝트를 생성하겠습니다.
그리고 이전 프로젝트에서 사용하던 MariaDB와 타임리프 설정을 가져옵니다.
build.gradle의 dependencies에 추가
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '3.1.0'
application.properties에 추가
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3308/bootex
spring.datasource.username=bootuser
spring.datasource.password=bootuser
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
spring.thymeleaf.cache=false
직전 예제에 사용했던 BaseEntity도 그대로 생성해줍니다.
BaseEntity
package org.zerock.board.entity;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@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;
}
엔티티리스너 기능을 추가했으므로 main에 해당하는 클래스에 어노테이션도 추가합니다.
BoardApplication.java
package org.zerock.guestbook;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class GuestbookApplication {
public static void main(String[] args) {
SpringApplication.run(GuestbookApplication.class, args);
}
}
다음으로는 연관관계 설정 이전에 각 테이블과 매핑되는 엔티티 클래스를 먼저 생성하겠습니다.
Member 엔티티
package org.zerock.board.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Member extends BaseEntity {
@Id
private String email;
private String password;
private String name;
}
Board 엔티티
package org.zerock.board.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
@Entity
@Builder
@AllArgsConstructor
@Getter
@ToString
@NoArgsConstructor
public class Board extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long bno;
private String title;
private String content;
}
Reply 엔티티
package org.zerock.board.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Reply extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rno;
private String text;
private String replier;
}
이제는 연관관계에 대해 생각해보겠습니다. Member와 Board 사이의 관계를 생각했을 때, Member는 여러 board를 작성할 수 있지만, 각 board는 하나의 member와 매칭됩니다.
새발표기법으로 연관관계를 그려보면 위의 다이어그램으로 표현할 수 있습니다. 즉, Member와 Board는 1:N 관계를 가지고 있습니다.
1:N 관계를 표현할 때는 @ManyToOne
어노테이션을 N에 적용합니다. N에 해당하는 엔티티는 1에 해당하는 엔티티의 기본키를 외래키로 참조하기 때문에 Board를 아래와같이 수정합니다.
Board 엔티티
package org.zerock.board.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Builder
@AllArgsConstructor
@Getter
@ToString(exclude = "writer") //외래키는 ToString에서 제외
@NoArgsConstructor
public class Board extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long bno;
private String title;
private String content;
//외래키를 참조함을 명시
@ManyToOne
private Member writer;
}
다음으로 Reply와 Board의 관계 역시 다이어그램으로 표현해보면 아래와 같습니다.
Reply에도 @ManyToOne
어노테이션을 추가하여 아래와 같이 코드를 수정합니다.
Reply 엔티티
package org.zerock.board.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "board")
public class Reply extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rno;
private String text;
private String replier;
@ManyToOne
private Board board;
}
먼저 Hibernate가 3개의 테이블을 생성시키고 각각의 기본키를 지정했습니다.
다음으로 Board와 Reply에 대해 외래키를 지정해주었습니다.
MariaDB를 확인하면 새로운 테이블 3개가 생성된 것을 확인할 수 있습니다.
ToString에서 연관관계를 가지는 애트리뷰트를 exclude한 이유는 바로 다음의 Lazy Loading에서 알 수 있는데, 참조되는 엔티티의 toString까지 접근해야하기 때문에 DB접근이 한번 더 일어나게 되고, 접근량이 많아지면 성능저하를 유발하기 때문에 제외해야 합니다.
ex: Board를 toString할 때 Member객체인 writer를 같이 출력한다면, writer에 해당하는 Member의 toString을 출력해야하고, 이를 위해 Member테이블에 접근해야 함