Skip to content

JPA Tips (Lombok, ...)

정명주(myeongju.jung) edited this page Oct 8, 2017 · 3 revisions

Basic

  • 테이블명은 명시적으로 선언하는 것이 좋다. : @Table(name = "COMPANY")
  • EAGER 전략을 위한 @NamedEntityGraph는 Entity에 정의해서 재사용하자.
  • 오프라인 낙관적 잠금이 필요하면 @Version 을 사용한다.
  • enum 의 경우 String으로 하되 여유가 되면 @Convert를 사용하는 것이 더 낫다.
  • 등록 수정 정보는 상속을 이용하기 보다는 등록 & 수정 값객체를 이용하는 것이 더 나은 것 같다.
  • 도메인 표현력을 가지는 정적 생성자 메소드가 좋은 것 같다. 생성자가 한 개 이상인 경우가 많다.

Lombok

  • @Data 는 사용하지 않는다.
    • 물론 클래스 레벨로 @Setter도 사용하지 않는다.
    • @Getter 만 선언하고 변경이 필요한 경우에 해당하는 변경메소드를 제공한다.
  • @EqualsAndHashCode 연관관계 필드는 무조건 exclude에 포함
    • @EqualsAndHashCode(exclude = {"partner", "ads"})
  • @ToString 연관관계 필드는 무조건 exclude에 포함
    • @ToString(exclude = {"partner", "ads"})
  • 기본 생성자는 존재(JPA 스팩)하되 가능한 노출하지 않는다
    • @NoArgsConstructor(access = AccessLevel.PROTECTED)

Sample source

/**
 * 업체 Entity
 */
@Entity
@Table(name = "COMPANY")    // **테이블명은 명시적으로 선언하는 것이 좋다.**
@SequenceGenerator(name = "COMPANY_SEQ", sequenceName = "COMPANY_SEQ", allocationSize = 1)  // sequence
@NamedEntityGraphs({    // **EAGER 전략을 위한 NamedEntityGraph는 Entity에 정의해서 재사용하자.**
        @NamedEntityGraph(name = "Company.withPartnerAndAds", attributeNodes = {
                @NamedAttributeNode("partner"),
                @NamedAttributeNode("ads")
        })
})
@Getter
@EqualsAndHashCode(exclude = {"partner", "ads"})    // **@EqualsAndHashCode 연관관계 필드는 무조건 exclude에 포함**
@ToString(exclude = {"partner", "ads"})    // **@ToString 연관관계 필드는 무조건 exclude에 포함**
@NoArgsConstructor(access = AccessLevel.PROTECTED)    // **기본 생성자는 존재하되 가능한 노출하지 않는다.**
@Slf4j
public class Company implements Serializable {
    // **직렬화 마커 인터페이스를 구현하고 serialVersionUID 꼭 선언한다.**
    private static final long serialVersionUID = 2415772833217876197L;
    /**
     * 업체 번호 (PK)
     */
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "COMPANY_SEQ")
    @Column(name = "COMPANY_NO")
    private Long companyNo;
    /**
     * 버전 for LOCK **필요하면 Version 속성을 사용한다**
     */
    @Version
    @Column(name = "version", nullable = false)
    private long version;
    /**
     * 담당 협력사 (fk)
     */
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "PARTNER_NO", nullable = false, foreignKey = @ForeignKey(name = "FK_COMPANY_PARTNER_NO"))
    private Partner partner;
    /**
     * 업체에 등록된 광고들 (fk)
     */
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "company")
    private Set<Ad> ads;
    /**
     * 카테고리
     */
    @Enumerated(EnumType.STRING)    // **String으로 하되 여유가 되면 @Convert를 사용하는 것이 더 낫다.**
    @Column(name = "CATEGORY", length = 50, nullable = false)
    private CompanyCategory category;
    /**
     * 업체명
     */
    @Column(name = "COMPANY_NAME", length = 100, nullable = false)
    private String companyName;
    /**
     * 전화번호
     */
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "phone", column = @Column(name = "PHONE", length = 20, nullable = false))
    })
    private Phone phone;
    /**
     * 주소
     */
    @AttributeOverrides({
            @AttributeOverride(name = "zipCode", column = @Column(name = "ZIP_CODE", length = 5)),
            @AttributeOverride(name = "address", column = @Column(name = "ADDRESS", length = 200, nullable = false)),
            @AttributeOverride(name = "detailAddress", column = @Column(name = "DETAIL_ADDRESS", length = 200))
    })
    private Address address;
    /**
     * 영업 시작 시간
     */
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "hour", column = @Column(name = "SALES_START_TIME_HOUR", length = 2, nullable = false)),
            @AttributeOverride(name = "minute", column = @Column(name = "SALES_START_TIME_MINUTE", length = 2, nullable = false)),
    })
    private Time salesStartTime;
    /**
     * 영업 마감 시간
     */
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "hour", column = @Column(name = "SALES_END_TIME_HOUR", length = 2, nullable = false)),
            @AttributeOverride(name = "minute", column = @Column(name = "SALES_END_TIME_MINUTE", length = 2, nullable = false)),
    })
    private Time salesEndTime;
    /**
     * 최소주문금액
     */
    @Column(name = "MINIMUM_ORDER_AMOUNT", nullable = false)
    private long minimumOrderAmount;    // **숫자형 NOT NULL 타입이면서 연산이 요구되면 primitive type으로 선언하는 것이 좋다.**
    /**
     * 계약서 이미지
     */
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "originName", column = @Column(name = "CONTRACT_IMAGE_ORIGIN", nullable = false)),
            @AttributeOverride(name = "storageName", column = @Column(name = "CONTRACT_IMAGE_STORAGE", nullable = false))})
    /**
     * 메뉴1 이미지
     */
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "originName", column = @Column(name = "MENU1_IMAGE_ORIGIN")),
            @AttributeOverride(name = "storageName", column = @Column(name = "MENU1_IMAGE_STORAGE"))})
    private SaveFile menu1Image;
    /**
     * 차단여부
     */
    @ColumnDefault("'N'")    // **hibernate의 @ColumnDefault는 가독성 향상에 도움**
    @Column(name = "BLOCK_YN", length = 1, nullable = false, insertable = false)
    @Convert(converter = YnAttributeConverter.class)
    private boolean block;
    /**
     * 등록 & 수정 Value
     */
    @Embedded
    private CreateAndUpdate cau;

    /**
     * 등록을 위한 정적 생성자 **꼭 필요한 파라미터를 받는 생성자를 제공한다. 도메인 표현력을 위해서 정적 생성자를 추천한다.**
     */
    public static Company create(@NonNull Partner partner,
                                 @NonNull CompanyCategory category,
                                 @NonNull String companyName,
                                 @NonNull Phone phone,
                                 @NonNull Address address,
                                 @NonNull Time salesStartTime,
                                 @NonNull Time salesEndTime,
                                 @NonNull Long minimumOrderAmount,
                                 @NonNull SaveFile contractImage,
                                 Business business,
                                 SaveFile menu1Image,
                                 @NonNull Long createMemberNo) {
        // ...
        result.cau = CreateAndUpdate.create(createMemberNo);
        return result;
    }

    // 등록 시 유효성 체크
    @PrePersist
    protected void onPrePersist() {
        cau.checkPreInsert();
    }

    // 수정 시 유효성 체크
    @PreUpdate
    protected void onPreUpdate() {
        cau.checkPreUpdate();
    }

    /**
     * 배달업체 수정 **모든 항목에 대한 setter를 제공하지 않고 변경을 하는 도메인 표현력을 가지는 변경 메소드를 제공한다.**
     *
     * @param update 수정 파라미터
     */
    public void update(CompanyUpdate update) {
        if (companyNo == null) {
            throw new IllegalStateException("'companyNo' must not be null");
        }
        // Check version for optimistic lock
        if (version != update.getVersion()) {
            throw new OptimisticLockException("Conflict version");
        }
        this.version = update.getVersion();
        this.partner = update.getPartner();
        this.category = update.getCategory();
        this.companyName = update.getCompanyName();
        // 관리자인 경우에만 전화번호를 수정할 수 있음
        if (update.getUser().isAdmin()) {
            this.phone = update.getPhone();
        }
        // ...
        if (update.getMenu1Image() != null) {
            this.menu1Image = update.getMenu1Image();
        }
        this.cau.update(update.getUser().getUserNo());
    }

    /**
     * 관리 협력사 변경
     *
     * @param newPartner 변경할 협력사
     */
    public void changePartner(Partner newPartner, Long updateUserNo) {
        if (this.partner == newPartner) {
            return;
        }
        this.partner = newPartner;
        this.cau.update(updateUserNo);
    }

    public boolean hasContractImage() {
        return contractImage != null && contractImage.isValid();
    }

    public boolean hasBusinessImage() {
        return business != null && business.getImage() != null && business.getImage().isValid();
    }

    public boolean hasMenu1Image() {
        return menu1Image != null && menu1Image.isValid();
    }

    public boolean hasMenu2Image() {
        return menu2Image != null && menu2Image.isValid();
    }

    public void block(Partner user) {
        if (cau == null) {
            throw new IllegalStateException("cau must not be null");
        }
        cau.update(user.getUserNo());
        block = true;
    }

    public boolean hasPerfectBusiness() {
        return business != null && business.isPerfect();
    }

    /**
     * 완벽한 사업자 정보를 가지고 있는지 체크
     * @throws IllegalStateException 사업자 정보가 완벽하지 않으면 발생
     */
    public void checkPerfectBusiness() {
        if (!hasPerfectBusiness()) {
            throw new IllegalStateException("Company is invalid : " + companyNo);
        }
    }

    /**
     * 광고 중인 광고가 존재하는가?
     */
    public boolean hasOpenAd() {
        for (Ad ad : ads) {
            if (ad.getAdDisplayStatus() == AdDisplayStatus.OPEN) {
                return true;
            }
        }
        return false;
    }
}
Clone this wiki locally