이 글은 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 듣고 정리한 글입니다.
@Entity
로 정의하는 객체int a = 10;
int b = a;
a = 20;
System.out.println("a = " + a); // 20
System.out.println("b = " + b); // 10 // 공유되지 않음
Integer a = new Integer(10);
Integer b = a; // 참조값이 넘어감. 즉, 같은 인스턴스를 공유
System.out.println("a = " + a); // 10
System.out.println("b = " + b); // 10
// 예시
a.setValue(20);
System.out.println("a = " + a); // 20
System.out.println("b = " + b); // 20
// 하지만 값을 변경할 수 있는 방법이 없다.
@Embeddable
: 값 타입을 정의하는 곳에 표시@Embedded
: 값 타입을 사용하는 곳에 표시@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
private LocalDateTime startDate;
private LocalDateTime endDate;
private String city;
private String street;
private String zipcode;
}
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded // 사용
private Period workPeriod;
@Embedded // 사용
private Address homeAddress;
}
@Embeddable // 정의
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
}
@Embeddable // 정의
public class Address {
private String city;
private String street;
private String zipcode;
}
Period.isWork()
처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음@AttributeOverride
: 속성 재정의@Entity
public class Member {
// ...
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
}
@AttributeOverrides
, @AttributeOverride
를 사용해서 컬럼 명 속성을 재정의@Entity
public class Member {
// ...
@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;
}
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Address address = new Address("city", "street", "10000");
Member member = new Member ();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member ();
member2.setUsername("member2");
member2.setHomeAddress(address); // 공유 참조
em.persist(member2);
member.getHomeAddress().setCity("newCity");
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
try {
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);
// 복사해서 사용
Address copyAddress = new Address(address.getCity(), address.getCity(), address.getZipcode());
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);
member.getHomeAddress().setCity("newCity");
tx.commit();
}
// 기본 타입(primitive type)
int a = 10;
int b = a; // 기본 타입은 값을 복사
b = 4; // b 변경해도, a 값 유지
// 객체 타입
Address a = new Address("Old");
Address b = a; // 객체 타입은 참조를 전달
b.setCity("New"); // b 변경하면 a의 값도 바뀜
Address newAddress = new Address("NewCity", address.getCity(), address.getZipcode());
member.setHomeAddress(newAddress);
int a = 10;
int b = 10;
System.out.println("a == b: " + (a==b)); // true
Address addr1 = new Address("서울시");
Address addr2 = new Address("서울시");
System.out.println("addr1 == addr2: " + (addr1==addr2)); // false
System.out.println("addr1 equals addr2: " + (addr1.equals(addr2))); // true
@ElementCollection
, @CollectionTable
사용@Entity
public class Member {
// ...
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME") // String으로 값이 하나이고 재정의하지 않았기 때문에, 예외적으로 처리해줌
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
persist()
해도, 값 타입 컬렉션들이 자동으로 같이 들어감. 다른 값 타입들처럼 라이프사이클이 member에 소속됨. try {
// ...
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId()); // SELECT 문
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) { // iter
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
tx.commit();
}
// homeCity -> newCity
// findMember.getHomeAddress().setCity("newCity"); // 부작용 위험! 하면 안됨
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
// 치킨 -> 한식
// 단순 String이며, String 자체가 값 타입. 통째로 갈아끼어야 함.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
// old1 -> newCity
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
// equals()와 hashCode()가 제대로 구현되어야 하는 이유
findMember.getAddressHistory().add(new Address("newCity", "street", "10000"));
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
public AddressEntity(Address address) {
this.address = address;
}
}
@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;
}
}
@Entity(name = "Member")
public class Member_Type {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME") // String으로 값이 하나이고 정의하지 않았기 때문에, 예외적으로 처리해줌
private Set<String> favoriteFoods = new HashSet<>();
// 값 타입 컬렉션 대신에 일대다 관계 고려해 엔티티 만듦
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
try {
// ...
member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));
// ...
}