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);
   }

}

mappedByJoinColumn의 역할 차이

  • 기존 방식: @JoinColumn을 사용하여 관계의 소유권을 CourseEntityUserEntity 쪽에서 각각 설정했지만, 이 방식은 양방향 관계에서 중복된 관계 설정을 유발할 수 있음. 따라서 user_idcourse_id에 대한 명확한 값이 설정되지 않을 수 있으며, 이는 null value in column "user_id" of relation "user_courses"와 같은 오류를 초래함.
  • 변경된 방식: mappedBy 속성을 사용함으로써, UserCourseEntity가 관계의 주체임을 명확히 설정했. 즉, 이제 UserCourseEntityUserEntityCourseEntity 사이의 관계를 관리하는 역할을 맡고 있게됨. 따라서 데이터베이스에서 user_idcourse_id가 명확하게 관리될 수 있음.

핵심: mappedBy는 관계의 소유자가 UserCourseEntity임을 명확히 하고, 관계를 유지하는 주체를 하나로 통일하여 불필요한 null 값을 방지.

nullable = false의 중요성

  • 기존 방식: @JoinColumnnullable = false가 설정되지 않아서, 관계 설정이 명확하지 않거나 잘못된 경우 user_idcourse_idnull로 들어가는 문제가 발생할 수 있음. 이로 인해 데이터베이스에서 null 값이 허용되지 않는 컬럼에 null이 삽입되며 오류가 발생하게 됨.
  • 변경된 방식: @JoinColumn(name = "user_id", nullable = false)@JoinColumn(name = "course_id", nullable = false)로 설정함으로써, 해당 관계가 반드시 존재해야 하며, null 값이 삽입되지 않도록 강제.

핵심: nullable = false로 데이터 무결성을 유지하고, 관계가 반드시 설정되도록 보장.

연관 관계 설정 방식 통일

  • 기존 방식: @JoinColumn을 사용하여 양쪽에서 관계를 각각 관리했기 때문에 양방향 관계에서 서로 다른 엔티티 간의 일관된 연결이 보장되지 않음.
  • 변경된 방식: mappedBy를 통해 관계의 소유자가 명확해지면서, 관계를 관리하는 책임이 UserCourseEntity에 집중. 엔티티 간의 관계를 더 명확하고 일관되게 설정할 수 있게함.

핵심: 관계의 관리 주체를 명확히 하여 불필요한 중복 설정 및 관계 설정 오류를 방지.