먼저 테이블 관계를 봐보자
user와 user_history의 관계는 1:N이다.
user에는 회원정보가 저장되어있고, userhistory에는 특정 user의 정보 변경 내용이 담겨져있다. (ex : 이름, 이메일 주소 등)
user와 user_history Entity를 봐보자
[User]
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(value = EnumType.STRING)
private Gender gender;
@NonNull
private String name;
@NonNull
private String email;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
@ToString.Exclude
private List<UserHistory> userHistories = new ArrayList<>();
}
위 코드에서 짚어야 할 포인트는 다음과 같다.
(1) OneToMany : 1:N 관계에서 1이 User임을 뜻하며, 다시말해 연관관계의 주인임을 의미한다. 또한 User에서 외래키를 관리하겠다는 것을 뜻하기도 한다.
* 외래키 (Foreign Key) : 한 테이블 필드 중, 다른 테이블의 행을 식별할 수 있는 키
(2) JoinColumn : 외래키를 맵핑할 때 사용한다. [tableName]_[column]의 관계를 나타내며, 위 코드의 경우, user 엔티티의 id필드를 외래키로 가진다는 뜻이다.
(3) ToString.Exclude : 해당 변수를 ToString과정에 포함하지 않는다는 것을 뜻한다. 보통 StackOverFlow를 방지하기 위해 사용한다.
[UserHistory]
public class UserHistory extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", insertable = false, updatable = false)
private Long userId;
private String name;
private String email;
@ManyToOne
private User user;
}
(1) ManyToOne : N:1관계에서 Many가 UserHistory임을 뜻한다. 다시 말해 연관관계에서 주인이 아님을 뜻한다.
Test 코드를 봐보자
Test코드를 작성하여 실제 N대1의 연관관계가 잘 동작하는지를 확인해보자.
[UserRepositoryTest]
@Test
void userRelationTest(){
User user = new User();
user.setName("david");
user.setEmail("david@fastcampus.com");
user.setGender(Gender.MALE);
userRepository.save(user);
user.setName("daniel");
userRepository.save(user);
user.setEmail("daniel@fastcampus.com");
userRepository.save(user);
System.out.println("-------test1-------");
userRepository.findAll().forEach(System.out::println);
List<UserHistory> result = userRepository.findByEmail("daniel@fastcampus.com").getUserHistories();
System.out.println("-------test2-------");
result.forEach(System.out::println);
System.out.println("-------test3-------");
System.out.println("UserHistory.getUser() : " + userHistoryRepository.findAll().get(0).getUser());
}
위 코드의 흐름은 다음과 같다.
(1) david 유저 생성
(2) david 유저의 이름과 이메일을 변경
(3) userRepository에 있는 모든 user들의 정보를 생성
User(super=BaseEntity(createdAt=2021-09-12T16:11:04.559640, updatedAt=2021-09-12T16:11:04.559640), id=1, gender=null, name=martin, email=martin@fastcampus.com)
User(super=BaseEntity(createdAt=2021-09-12T16:11:04.563068, updatedAt=2021-09-12T16:11:04.563068), id=2, gender=null, name=dennis, email=dennis@fastcampus.com)
User(super=BaseEntity(createdAt=2021-09-12T16:11:04.563556, updatedAt=2021-09-12T16:11:04.563556), id=3, gender=null, name=sophia, email=sophia@slowcampus.com)
User(super=BaseEntity(createdAt=2021-09-12T16:11:04.564035, updatedAt=2021-09-12T16:11:04.564035), id=4, gender=null, name=james, email=james@slowcampus.com)
User(super=BaseEntity(createdAt=2021-09-12T16:11:04.564434, updatedAt=2021-09-12T16:11:04.564434), id=5, gender=null, name=martin, email=martin@another.com)
User(super=BaseEntity(createdAt=2021-09-12T16:11:05.693437, updatedAt=2021-09-12T16:11:05.769698), id=6, gender=MALE, name=daniel, email=daniel@fastcampus.com)
-> 현재 userRepository에 등록된 모든 유저들의 정보를 출력했다.
(4) userHistory에서 daniel@fastcampus.com이라는 이메일을 가진 대상의 히스토리리 내역을 모두 출력
UserHistory(super=BaseEntity(createdAt=2021-09-12T19:54:41.258, updatedAt=2021-09-12T19:54:41.289010), id=1, userId=6, name=david, email=david@fastcampus.com, user=User(super=BaseEntity(createdAt=2021-09-12T19:54:41.244593, updatedAt=2021-09-12T19:54:41.326078), id=6, gender=MALE, name=daniel, email=daniel@fastcampus.com))
UserHistory(super=BaseEntity(createdAt=2021-09-12T19:54:41.320914, updatedAt=2021-09-12T19:54:41.320914), id=2, userId=6, name=daniel, email=david@fastcampus.com, user=User(super=BaseEntity(createdAt=2021-09-12T19:54:41.244593, updatedAt=2021-09-12T19:54:41.326078), id=6, gender=MALE, name=daniel, email=daniel@fastcampus.com))
UserHistory(super=BaseEntity(createdAt=2021-09-12T19:54:41.326362, updatedAt=2021-09-12T19:54:41.326362), id=3, userId=6, name=daniel, email=daniel@fastcampus.com, user=User(super=BaseEntity(createdAt=2021-09-12T19:54:41.244593, updatedAt=2021-09-12T19:54:41.326078), id=6, gender=MALE, name=daniel, email=daniel@fastcampus.com))
-> userHistory에서 daniel 유저가 개인정보를 바꾼 내역들을 출력한다.
(5) userHistory에서 첫 번째로 등록된 객체의 유저 정보를 가져오기
UserHistory.getUser() : User(super=BaseEntity(createdAt=2021-09-12T16:32:58.157394, updatedAt=2021-09-12T16:32:58.243673), id=6, gender=MALE, name=daniel, email=daniel@fastcampus.com)
-> daniel 유저만 현재까지 히스토리 변경 내역을 갖고 있으므로, daniel 유저의 정보를 출력한다.
어떻게 가능한걸까?
외래키의 관계를 잘 살펴보면된다.
예시로 Test코드가 작동될때 생성되는 user 테이블과 user_history 테이블은 다음과 같다.
[user]
create table user (
id bigint generated by default as identity,
created_at timestamp,
updated_at timestamp,
email varchar(255),
gender varchar(255),
name varchar(255),
primary key (id)
)
One-To-Many에서 One(주인)에 해당되는 user는 외래키를 갖고있지 않다.
외래키의 핵심인 id를 본인이 갖고있기 때문이다.
[user history]
create table user_history (
id bigint generated by default as identity,
created_at timestamp,
updated_at timestamp,
email varchar(255),
name varchar(255),
user_id bigint, // 외래키
primary key (id)
)
One-To-Many에서 Many(종속자)에 해당하는 user history는 user_id를 외래키로 갖고있다.
user에서는 id를 Primary Key로 사용하고 있고, 이 PK기반으로 user(주인)에게서 user history내역을 가져오고 있다.
굳이 외래키를 왜쓰지? 그냥 PK(Primary Key)만 써서 조회해도 되지않아?
참조 무결성 (Referential integrity)를 지키기 위해서다.
관계형 DB 모델에서 참조 무결성은 참조 관계에 있는 두 테이블의 데이터가 항상 일관된 값을 유지해야한다.
위 예시를 봐보자. Artist에는 4번 아이디를 가진 대상이 없다. 아마 기존에 있었으나 삭제되었을 것이다.
하지만 아래 Artist Album 테이블은 여전히 4번을 참조하고 있다. 4번 아티스트의 album정보도 출력하고 있다.
만약 primary key로만 데이터의 연관관계를 나타낸다면, 아래 그림과 같이 4번 Artist에 대한 정보를 옳바르게 출력하지 못했을 것이다. 하지만 외래키를 사용해 테이블의 관계를 나타냈기 때문에 참조 무결성이 강제로 적용되어 데이터를 누군가 실수, 혹은 고의로 강제로 삭제하여 데이터의 원본을 찾지 못하는 사태를 미연에 방지할 수 있다.
참고한 사이트
위키백과 - 참조 무결성
'👩🏻💻 Programming > SpringBoot' 카테고리의 다른 글
DB와 Transaction (0) | 2021.09.26 |
---|---|
영속성 컨텍스트 (0) | 2021.09.12 |
Entity, DTO, 그리고 DAO (0) | 2021.09.05 |
Entity의 Listener (0) | 2021.09.05 |
Entity의 기본 속성(annotation) (0) | 2021.09.05 |