@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public void changeTeam(Team team) {
this.team = team;
}
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}
핵심 포인트:
fetch = FetchType.LAZY 필수@JoinColumn으로 외래키 이름 지정일대다 단방향은 권장하지 않음. 외래키가 다른 테이블에 있어서 추가 UPDATE SQL이 실행됨.
// 권장하지 않는 방식
@Entity
public class Team {
@OneToMany
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>();
}
문제점: 연관관계 관리를 위해 추가 UPDATE SQL 실행
해결책: 일대다 양방향 사용
@Entity
public class Team {
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public void addMember(Member member) {
members.add(member);
member.changeTeam(this);
}
}
@Entity
public class Member {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public void changeTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
if (team != null && !team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
}
주 테이블에 외래키:
@Entity
public class Member {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "locker_id")
private Locker locker;
}
@Entity
public class Locker {
@OneToOne(mappedBy = "locker")
private Member member;
}
대상 테이블에 외래키:
@Entity
public class Member {
@OneToOne(mappedBy = "member")
private Locker locker;
}
@Entity
public class Locker {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
선택 기준:
주의사항: 일대일 관계에서 지연 로딩이 제대로 작동하지 않는 경우가 있음
실무에서 사용 금지! 연결 테이블이 단순 연결만으로 끝나지 않음.
// 이렇게 하지 말 것
@Entity
public class Member {
@ManyToMany
@JoinTable(name = "member_product")
private List<Product> products = new ArrayList<>();
}
해결책: 연결 엔티티 생성
@Entity
public class Member {
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
@Entity
public class Product {
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
@Entity
public class MemberProduct {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
private int count;
private int price;
private LocalDateTime orderDateTime;
}
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
}
장점:
단점:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
장점:
단점:
공통 매핑 정보가 필요할 때 사용. 엔티티가 아니고 테이블과 매핑되지 않음.
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
@Entity
public class Member extends BaseEntity {
@Id
@GeneratedValue
private Long id;
private String username;
}
@Entity
public class Team extends BaseEntity {
@Id
@GeneratedValue
private Long id;
private String name;
}
Member member = em.find(Member.class, 1L); // 즉시 로딩
Member reference = em.getReference(Member.class, 1L); // 프록시 객체 반환
프록시 특징:
Member refMember = em.getReference(Member.class, 1L);
System.out.println(refMember.getClass()); // class Member$HibernateProxy$...
// 이때 실제 쿼리 실행
System.out.println(refMember.getUsername());
프록시 확인 방법:
// 프록시 인스턴스 초기화 여부 확인
emf.getPersistenceUnitUtil().isLoaded(refMember);
// 프록시 클래스 확인
refMember.getClass().getName(); // Member$HibernateProxy$...
// 프록시 강제 초기화 (하이버네이트 전용)
Hibernate.initialize(refMember);
@Entity
public class Member {
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
@JoinColumn(name = "team_id")
private Team team;
}
// 지연 로딩
Member member = em.find(Member.class, 1L); // SELECT MEMBER만 실행
Team team = member.getTeam(); // 프록시 객체 반환
System.out.println(team.getName()); // 이때 SELECT TEAM 실행
실무 권장사항:
@ManyToOne, @OneToOne: 기본이 즉시 로딩 → LAZY로 설정@OneToMany, @ManyToMany: 기본이 지연 로딩@Entity
public class Parent {
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children = new ArrayList<>();
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}
}
@Entity
public class Child {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
}
// CASCADE 없이는
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.persist(child1); // 각각 persist 해야 함
em.persist(child2);
// CASCADE.ALL이 있으면
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // 이것만으로 자식들도 함께 저장
CASCADE 종류:
사용 시기:
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); // 자식 엔티티를 컬렉션에서 제거
// DELETE FROM CHILD WHERE ID = ? 실행됨
주의사항:
@OneToOne, @OneToMany만 가능@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
두 옵션 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음.
1. 프록시 타입 비교 시 주의:
Member member1 = em.find(Member.class, 1L);
Member member2 = em.getReference(Member.class, 1L);
System.out.println(member1.getClass() == member2.getClass()); // false
System.out.println(member1 instanceof Member); // true
System.out.println(member2 instanceof Member); // true - 이걸 사용
2. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 실제 엔티티 반환:
Member member1 = em.find(Member.class, 1L); // 실제 엔티티
Member member2 = em.getReference(Member.class, 1L); // 실제 엔티티 반환
System.out.println(member1 == member2); // true
3. 프록시를 먼저 조회하면 find()도 프록시 반환:
Member reference = em.getReference(Member.class, 1L); // 프록시
Member member = em.find(Member.class, 1L); // 프록시 반환
System.out.println(reference == member); // true
4. 준영속 상태일 때 프록시 초기화하면 예외:
Member refMember = em.getReference(Member.class, 1L);
em.detach(refMember); // 준영속 상태
refMember.getUsername(); // LazyInitializationException 발생
@Entity
public class Member {
@Id
@GeneratedValue
private Long id; // 엔티티 타입
private String username; // 값 타입
private int age; // 값 타입
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
})
private Address workAddress;
}
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Period() {}
public Period(LocalDateTime startDate, LocalDateTime endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
public boolean isWork(LocalDateTime date) {
return date.isAfter(startDate) && date.isBefore(endDate);
}
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
public String getFullAddress() {
return city + " " + street + " " + zipcode;
}
}
장점:
값 타입 공유 참조의 위험성:
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setHomeAddress(address);
Member member2 = new Member();
member2.setHomeAddress(address); // 같은 address 인스턴스 공유
member1.getHomeAddress().setCity("NewCity"); // member2의 주소도 바뀜
해결책: 값을 복사해서 사용
Address address = new Address("city", "street", "10000");
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
member2.setHomeAddress(copyAddress); // 복사본 사용
불변 객체로 설계:
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// Getter만 제공, Setter는 제공하지 않음
public String getCity() { return city; }
public String getStreet() { return street; }
public String getZipcode() { return zipcode; }
}
값 변경 시:
Address oldAddress = member.getHomeAddress();
Address newAddress = new Address("NewCity", oldAddress.getStreet(), oldAddress.getZipcode());
member.setHomeAddress(newAddress);
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) &&
Objects.equals(street, address.street) &&
Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
}
@Entity
public class Member {
@ElementCollection
@CollectionTable(name = "favorite_food",
joinColumns = @JoinColumn(name = "member_id"))
@Column(name = "food_name")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "address",
joinColumns = @JoinColumn(name = "member_id"))
private List<Address> addressHistory = new ArrayList<>();
}
사용 예시:
Member member = new Member();
member.setUsername("김개발");
member.setHomeAddress(new Address("city1", "street1", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street1", "10001"));
member.getAddressHistory().add(new Address("old2", "street2", "10002"));
em.persist(member);
값 타입 컬렉션 수정:
Member findMember = em.find(Member.class, 1L);
// 기본값 타입 컬렉션 수정
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
// 임베디드 값 타입 컬렉션 수정
findMember.getAddressHistory().remove(new Address("old1", "street1", "10001"));
findMember.getAddressHistory().add(new Address("newCity1", "street1", "10001"));
값 타입 컬렉션의 제약사항:
실무에서는 일대다 관계를 고려:
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
@Embedded
private Address address;
public AddressEntity() {}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
}
@Entity
public class Member {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "member_id")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
값 타입 컬렉션 정리: