엔티티(Entity)와 엔티티매니저(EntityManager)
이번에는 엔티티(Entity) 객체와 엔티티를 다루는 엔티티매니저(EntityManager) 객체에 대해 알아보겠습니다.
엔티티(Entity)
이러한 역할을 하는 엔티티를 설정하는 방법은 2가지가 있는데 하나는 @Entity 애노테이션을 이용하는 것이고 다른 하나는 xml 설정을 이용하는 것입니다. 대부분 애노테이션을 이용하므로 애노테이션을 이용하여 엔티티를 설정하는 방법을 알아보도록 하겠습니다.
엔티티를 설정하기 위한 애노테이션
@Entity 애노테이션
이전에 JPA에서는 쿼리를 자동으로 생성해준다고 했었습니다. JPA는 클래스 이름을 테이블 이름으로 사용하는데 아래의 Room 엔티티는 기본적으로 Room 테이블과 매핑됩니다.
@Entity public class Room { //... }
@Table 애노테이션
@Entity @Table(name="room_info") public class Room{ // ... }
@Id 애노테이션
public OptionalfindRoom(int roomId) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpastart"); EntityManager em = emf.createEntityManager(); try { Room room = em.find(Room.class, roomId); // SELECT ... FROM Room r WHERE id = ? return Optional.ofNullable(room); } finally { em.close(); } }
@Basic 애노테이션
@Entity @Table(name="room_info") public class Room { @Id private String number; private String name; private String description; }
@Enumerated 애노테이션
// 호텔 등급 열거형 public enum Grade { STAR1, STAR2, STAR3, STAR4, STAR5 }
//호텔 엔티티 @Entity public class Hotel { @Id private String id; private String name; @Enumerated(EnumType.STRING) private Grade grade; }
Hotel 엔티티를 보면 @Enumerated의 속성으로 EnumType.STRING 으로 지정하고 있습니다. 이 속성은 @Enumerated 애노테이션으로 설정한 열거형을 DB로 저장할 때 어떤 값으로 저장할지 결정합니다. EnumType.STRING으로 저장하는 경우 hotel 테이블에는 grade 필드에 "STAR1, STAR2, STAR3, STAR4, STAR5"란 문자열이 저장될 것이고 EnumType.ORDINAL을 사용하는 경우 grade필드에는 0~4(인덱스)가 저장될 것입니다. 여기서 인덱스0은 STAR1, 인덱스4는 STAR5를 가리킵니다. 열거형의 순서는 바뀔 가능성이 있기때문에 ORDINAL을 사용하는 것보다는 STRING을 사용하는 것이 유지보수에 더 유리할 것으로 보입니다.
@Column 애노테이션
@Entity @Table(name="room_info") public class Room { @Id private String number; private String name; @Column(name="description") private String desc; }
좀 이후에 정리하겠지만 JPA는 INSERT, UPDATE, DELETE의 동작이 보통과 다르기 때문에 예상하지 못하거나 실수를 방지하기 위해 읽기 전용 매핑설정이 가능합니다. 읽기 전용 프로퍼티를 설정하는 방법은 insertable 속성과 updateble 속성을 false로 설정하면 됩니다. 읽기 전용 프로퍼티는 JPA가 자동으로 생성하는 쿼리에서 제외됩니다.
@Entity @Table(name="room_info") public class Room { //... @Column(name="id", insertable=false, updatable=false) private Long dbId; }
아래와 같이 insertable 속성과 updatable 속성을 false로 지정하면 JPA에서 쿼리를 생성할 때 해당 컬럼을 제외하는 것을 확인할 수 있습니다.
entityManager.persist(new Room("R202", "홍길동", "2번룸")); // insert into room_info (description, name, number) values (?, ?, ?);
- insert 제외 예시(id가 쿼리에서제외)
entityManager.getTransaction().begin(); Room room = entityManager.find(Room.class, "R202"); room.changeName("리뉴얼 2번룸"); // name을 바꾸는 메서드. entityManager.getTransaction().commit(); // update room_info set description = ?, name = ? where number = ?;- update 제외 예시(id가 쿼라에서 제외)
@Access 애노테이션
@Entity @Table(name="room_info") public class Room { private String number; private String name; private String desc; private Long dbId; @Id public String getNumber() { return number; } // setter.. @Column(name="description") public String getDesc(){ return desc; } // setter.. @Column(name="id", insertable=false, updatable = false) { public String getDbId() { return dbId; } // setter.. }
근데 일부는 필드에 일부는 프로퍼티에 설정해보았는데 각각 필드와 프로퍼티를 이용하여 매핑이 되는 것을 확인했습니다. @Access 애노테이션을 사용하여 접근 방식을 지정해야하는 필요성은 잘 모르겠는데 아마 클래스 단위로 접근 방식을 제어하기 위한 것이 아닌가 싶습니다.
@Entity @Table(name="room_info") @Access(AccessType.PROPERTY) // or @Access(AccessType.FIELD) public class Room { // @Id 만약 애노테이션을 필드에 사용하면 에러가 발생함.. private String number; private String name; private String desc; private Long dbId; @Id public String getNumber() { return number; } // setter.. @Column(name="description") public String getDesc(){ return desc; } // setter.. @Column(name="id", insertable=false, updatable = false) { public String getDbId() { return dbId; } // setter.. }
@Transient 애노테이션
@Entity @Table(name="room_info") public class Room { @Id private String number; //... @Transient private long timestamp = System.currentTimeMills(); }
@Transient 애노테이션을 사용한 엔티티를 조회하면 아래와 같이 쿼리에서 해당 필드가 제외되는 것을 확인할 수 있습니다.
Room find = em.find(Room.class, room.getNumber()); //Hibernate: select room0_.number as number1_0_0_ from room_info room0_ where room0_.number=?
엔티티매니저(EntityManager)
find() 메서드
public find(Class entityClass, Object primaryKey)
persist() 메서드
public void persist(Object entity)
persist() 메서드는 트랜잭션 범위 내에서 실행해야 합니다. persist() 메서드는 실행시점에 영속 컨텍스트에 엔티티를 저장하고, 트랜잭션을 commit() 하는 시점에 insert 쿼리가 실행되므로 트랜잭션 범위에서 실행하지 않는다면 실제로 DB에 반영되지 않습니다.
EntityManager entityManager = emf.createEntityManager(); EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); Hotel hotel = new Hotel("H101","서울호텔","STAR5"); entityManager.persist(hotel); transaction.commit(); } catch(Exception e) { transaction.rollback(); ... } finally { entityManager.close(); }
remove() 메서드
public void remove(Object entity)
remove() 메서드 역시 트랜잭션 범위 내에서 실행되어야 하며 트랜잭션이 commit() 하는 시점에 delete 쿼리가 실행됩니다.
EntityManager em = emf.createEntityanager(); EntityTransaction transaction = em.getTransaction(); try { transaction.begin(); Room room = em.find(Room.class, "R101"); if(room != null) { em.remove(room); } transaction.commit(); } catch(Exception e) { transaction.rollback(); ... } finally { em.close(); }
엔티티 수정
EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); try { transaction.begin(); Room room = em.find(Room.class, "R101"); if(room != null) { room.changeName("카프리"); } transaction.commit(); } catch (Exception e) { transaction.rollback(); //... } finally { em.close(); }