User Course Entity 테이블 설정 - LikeLionTeam/BootHouse GitHub Wiki
-
전제 조건
- 유저 한명은 여러개의 코스를 등록할 수 있다.
- 코스 하나는 여러명의 유저를 가질 수 있다.
-
문제점 : 유저와 코스 다대다 매핑으로 성능 문제, 복잡한 쿼리, 데이터 무결성 관리의 어려움, 유연성의 부족 등 여러 단점이 존재
-
해결책 : User Entity와 Course Entity를 연결해 주는 UserCourse Entity를 생성하여, 일대다 다대일 관계로 풀어 줌.
처음 테이블
@Entity
@Table(name = "user_courses")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
public class UserCourseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_course_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private UserEntity userEntity; // 유저
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
private CourseEntity courseEntity; // 코스
}
@Entity
@Table(name = "courses")
@Getter @Setter
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
public class CourseEntity extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "course_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bootcamp_id")
private BootCampEntity bootcampEntity; // 훈련과정을 주관하는 교육기관 매핑
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private CategoryEntity categoryEntity; // 훈련과정 카테고리 -- UX/UI 디자인, 백엔드/자바
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sub_course_id")
private SubCourseEntity subCourseEntity; // 각 코스가 어떤 subCategory에 속할지 추가
@Column(length = 255, nullable = false)
private String name; // 프로그램 이름
@Column(nullable = false, name = "start_date")
private LocalDate startDate; // 수업 시작일
@Column(nullable = false, name = "end_date")
private LocalDate endDate; // 수업 종료일
@Column(nullable = false, name = "closing_date")
private LocalDateTime closingDate; // 모집 마감일
@Column(name = "coding_test_exempt", nullable = false)
private boolean codingTestExempt; // 코딩 테스트 여부
@Column(nullable = false, name = "card_requirement")
private boolean cardRequirement; // 내일배움카드 여부
@Column(nullable = false, name = "online_offline")
private boolean onlineOffline; // 온라인,오프라인 여부 (T : 온라인, F : 오프라인)
@Column(length = 255, name = "location")
private String location; // 오프라인 경우의 장소 (온라인일 경우에, 해당필드 무시)
@Column(length = 255, nullable = false, name = "tuition_type")
private String tuitionType; // "유료" or "무료" (교육비용)
@Column(columnDefinition = "TEXT", name = "summary")
private String summary; // 과정 요약
@Enumerated(EnumType.STRING)
@Column(name = "participation_time", nullable = false)
private ParticipationTime participationTime; // 참여시간
@Column(name = "max_participants", nullable = false)
private int maxParticipants; // 모집정원 :: -1 값이면, 모집정원 없음
@Column(precision = 3, scale = 2) // 가능값 : 9.99, 0.01, 불가능값 : 10.00
private BigDecimal averageRating; // 해당 프로그램 평균별점 (리뷰로부터 계산)
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "course_id")
private List<UserCourseEntity> users = new ArrayList<>();
public void addUser(UserCourseEntity userCourse) {
users.add(userCourse);
userCourse.setCourseEntity(this);
}
}
@Entity
@Table(name = "users")
@Getter @Setter
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserEntity extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(length = 50, nullable = false, unique = true)
private String email;
@Column(length = 50, nullable = false)
private String password;
@Column(length = 50, nullable = false)
private String name;
@Column(length = 255, nullable = false)
private String address;
@Column(nullable = false, unique = true)
private String phoneNumber;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private RoleType roleType;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus userStatus;
@Column(name = "last_login_at")
private Long lastLoginAt;
@Column(name = "certification_code")
private String certificationCode;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "user_id") // 외래 키를 소유하는 쪽에서 JoinColumn 사용
private List<UserCourseEntity> courses = new ArrayList<>();
public void addCourse(UserCourseEntity course) {
courses.add(course);
course.setUserEntity(this);
}
public void removeCourse(UserCourseEntity course) {
courses.remove(course);
course.setUserEntity(null);
}
}
오류 : null value in column "user_id" of relation "user_courses"
유저코스에 user_id 값이 null이 들어가는 문제가 발생
수정 테이블
@Entity
@Table(name = "user_courses")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
public class UserCourseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_course_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private UserEntity userEntity;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id", nullable = false)
private CourseEntity courseEntity;
}
@Entity
@Table(name = "courses")
@Getter @Setter
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
public class CourseEntity extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "course_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bootcamp_id")
private BootCampEntity bootcampEntity; // 훈련과정을 주관하는 교육기관 매핑
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private CategoryEntity categoryEntity; // 훈련과정 카테고리 -- UX/UI 디자인, 백엔드/자바
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sub_course_id")
private SubCourseEntity subCourseEntity; // 각 코스가 어떤 subCategory에 속할지 추가
@Column(length = 255, nullable = false)
private String name; // 프로그램 이름
@Column(nullable = false, name = "start_date")
private LocalDate startDate; // 수업 시작일
@Column(nullable = false, name = "end_date")
private LocalDate endDate; // 수업 종료일
@Column(nullable = false, name = "closing_date")
private LocalDateTime closingDate; // 모집 마감일
@Column(name = "coding_test_exempt", nullable = false)
private boolean codingTestExempt; // 코딩 테스트 여부
@Column(nullable = false, name = "card_requirement")
private boolean cardRequirement; // 내일배움카드 여부
@Column(nullable = false, name = "online_offline")
private boolean onlineOffline; // 온라인,오프라인 여부 (T : 온라인, F : 오프라인)
@Column(length = 255, name = "location")
private String location; // 오프라인 경우의 장소 (온라인일 경우에, 해당필드 무시)
@Column(length = 255, nullable = false, name = "tuition_type")
private String tuitionType; // "유료" or "무료" (교육비용)
@Column(columnDefinition = "TEXT", name = "summary")
private String summary; // 과정 요약
@Enumerated(EnumType.STRING)
@Column(name = "participation_time", nullable = false)
private ParticipationTime participationTime; // 참여시간
@Column(name = "max_participants", nullable = false)
private int maxParticipants; // 모집정원 :: -1 값이면, 모집정원 없음
@Column(precision = 3, scale = 2) // 가능값 : 9.99, 0.01, 불가능값 : 10.00
private BigDecimal averageRating; // 해당 프로그램 평균별점 (리뷰로부터 계산)
@OneToMany(mappedBy = "courseEntity", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserCourseEntity> users = new ArrayList<>();
public void addUser(UserCourseEntity userCourse) {
if (userCourse == null) {
throw new IllegalArgumentException("UserCourseEntity cannot be null");
}
users.add(userCourse);
userCourse.setCourseEntity(this);
}
}
@Entity
@Table(name = "users")
@Getter @Setter
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserEntity extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(length = 50, nullable = false, unique = true)
private String email;
@Column(length = 50, nullable = false)
private String password;
@Column(length = 50, nullable = false)
private String name;
@Column(length = 255, nullable = false)
private String address;
@Column(nullable = false, unique = true)
private String phoneNumber;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private RoleType roleType;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus userStatus;
@Column(name = "last_login_at")
private Long lastLoginAt;
@Column(name = "certification_code")
private String certificationCode;
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserCourseEntity> courses = new ArrayList<>();
public void addCourse(UserCourseEntity course) {
if (courses == null) {
courses = new ArrayList<>(); // 이 줄은 기본적으로 필요하지 않지만, 안전하게 추가
}
courses.add(course);
course.setUserEntity(this);
}
}
mappedBy와 JoinColumn의 역할 차이
- 기존 방식:
@JoinColumn을 사용하여 관계의 소유권을CourseEntity와UserEntity쪽에서 각각 설정했지만, 이 방식은 양방향 관계에서 중복된 관계 설정을 유발할 수 있음. 따라서user_id와course_id에 대한 명확한 값이 설정되지 않을 수 있으며, 이는null value in column "user_id" of relation "user_courses"와 같은 오류를 초래함. - 변경된 방식:
mappedBy속성을 사용함으로써,UserCourseEntity가 관계의 주체임을 명확히 설정했. 즉, 이제UserCourseEntity는UserEntity와CourseEntity사이의 관계를 관리하는 역할을 맡고 있게됨. 따라서 데이터베이스에서user_id와course_id가 명확하게 관리될 수 있음.
핵심: mappedBy는 관계의 소유자가 UserCourseEntity임을 명확히 하고, 관계를 유지하는 주체를 하나로 통일하여 불필요한 null 값을 방지.
nullable = false의 중요성
- 기존 방식:
@JoinColumn에nullable = false가 설정되지 않아서, 관계 설정이 명확하지 않거나 잘못된 경우user_id나course_id가null로 들어가는 문제가 발생할 수 있음. 이로 인해 데이터베이스에서null값이 허용되지 않는 컬럼에null이 삽입되며 오류가 발생하게 됨. - 변경된 방식:
@JoinColumn(name = "user_id", nullable = false)및@JoinColumn(name = "course_id", nullable = false)로 설정함으로써, 해당 관계가 반드시 존재해야 하며,null값이 삽입되지 않도록 강제.
핵심: nullable = false로 데이터 무결성을 유지하고, 관계가 반드시 설정되도록 보장.
연관 관계 설정 방식 통일
- 기존 방식:
@JoinColumn을 사용하여 양쪽에서 관계를 각각 관리했기 때문에 양방향 관계에서 서로 다른 엔티티 간의 일관된 연결이 보장되지 않음. - 변경된 방식:
mappedBy를 통해 관계의 소유자가 명확해지면서, 관계를 관리하는 책임이UserCourseEntity에 집중. 엔티티 간의 관계를 더 명확하고 일관되게 설정할 수 있게함.
핵심: 관계의 관리 주체를 명확히 하여 불필요한 중복 설정 및 관계 설정 오류를 방지.