이전글




엔티티 식별자
JPA는 엔티티를 관리하는 영속성 컨텍스트라는 공간이 있고 persist()를 통해 이곳에 저장된 엔티티 객체들은 JPA의 관리 대상이 됩니다. JPA의 관리 대상이 되었다는것은 엔티티의 상태가 변경되면 DB에 해당 엔티티에 대한 데이터가 JPA가 생성한 쿼리에 의해 UPDATE 되어진다는 뜻이며, EntityManager를 통해 삭제, 삽입, SELECTE 되어진다는 뜻입니다.


그런데 JPA는 영속성 컨텍스트에서 관리되는 엔티티 객체들을 구분할 필요가 있는데, 이때 필요한 것이 식별자 입니다. 식별자로 지정되는 속성@Id 애노테이션이 맵핑되어 있어야 합니다. 또한 속성에 맵핑되는 DB 테이블의 컬럼은 UNIQUE한 컬럼이어야 합니다. 즉 데이터가 중복되거나 NULL이 허용되는 컬럼이어서는 안됩니다.



식별자 할당 전략을 선택하기 - @GeneratedValue
JPA에는 엔티티에 식별자를 할당하는 몇 가지 전략을 제공합니다. 식별자 필드에는 @Id 애노테이션이 붙어있어야 하며, 식별자를 할당하는 전략을 지정할때에는 @GeneratedValue 애노테이션의 strategy 속성에 전략을 지정하면 됩니다.
여기서는 식별자 생성을 db한테 위임하는 전략에 대해서 알아보고 그 중에서도 GenerationType.IDENTITY 전략에 대해 설명합니다. 
@Entity
@Table(name = "product"
public class Product { 
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)                                 
    private Integer id; 
cs

 





DB에게 식별자 위임하기 - GenerationType.IDENTITY
JPA에서 엔티티 식별자를 할당하는 방법은 이전글에서 설명한 직접 할당하는 방법도 있지만, MySQL 의 AUTO_INCREMENT 컬럼과 같이 DBMS에서 자동으로 할당하는 값을 식별자로 사용하는 방법이 있습니다.

다음과 같이 생성된 product 테이블이 있습니다.
CREATE TABLE product (
  number INT NOT NULL AUTO_INCREMENT,                                                        
  name varchar(50),
  PRIMARY KEY (`number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
cs

 

 



그에 맞는 Entity 클래스를 작성합니다. 간단하게 number, name 필드가 있고 number 필드를 식별자로 사용하기 위해 @Id 애노테이션을 걸어두었습니다. number 필드는 JPA가 엔티티를 관리함에 있어서 각 엔티티를 구분할 식별자로 사용됩니다.

식별자를 할당하는 방법은 DB에 데이터를 Insert 하면서 DB가 auto_incerement를 통해 생성한 값을 식별자로 사용하도록 @GeneratedValue 애노테이션에 GenerationType.IDENTITY 전략을 지정해 주었습니다. @Id 애노테이션이 지정되어 있지 않은 필드에 @GeneratedValue 애노테이션을 지정하게 되면 예외가 발생합니다.
@Entity
@Table(name = "product"
public class Product { 
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)                                 
    private Integer number; 
    
    private String name; 
 
    public Product(Integer number, String name) { 
        super(); 
        this.number = number; 
        this.name = name; 
    }
    
    public Product(String name) { 
        super(); 
        this.name = name; 
    }
    
    public Product() {} 
 
}
 
cs

 


식별자를 DB에게 할당받기 때문에 엔티티 생성시에는 식별자를 제외한 나머지 필드에 대해서만 값을 주입하고 persist() 합니다. 이때 중요한 사실이 있는데 JPA 트랜잭션이 커밋되기 전 persist() 가 실행되는 시점에 insert 쿼리가 실행된다는 점 입니다.
transaction.begin();
Product product = new Product("notebook");                                                 
entityManager.persist(product); //commit 되기 전에 DB에 insert 하고 식별자값을 얻음
transaction.commit();
cs

 




JPA는 엔티티를 영속성 컨텍스트에 persist()하는 시점에 엔티티를 구분할 수 있는 식별자가 필요한데, IDENTITY 전략을 사용한 경우 DB에 insert 해보지 않는 이상 식별자를 할당할 수 없으므로 insert와 동시에 할당받은 값을 식별자로 사용하는 것입니다. 하이버네이트의 경우는 JDBC3에서 제공하는 API의 Statement.getGeneratedKeys() 메서드를 사용하여 데이터를 저장함과 동시에 생성된 키값을 얻어옵니다. 따라서 이 경우에 트랜잭션 쓰기 지연은 동작하지 않습니다.
22:54:21.528 [main] DEBUG org.hibernate.SQL - insert into product (name) values (?)
22:54:21.542 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [notebook]
22:54:21.546 [main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 7
cs

 

마지막에 Natively generated identity: 7 에서 7이 MySQL의 AUTO_INCREMENT 를 통해 생성된 값입니다.



IDENTITY 전략에 직접 식별자를 할당하는 경우
만약 IDENTITY 전략을 사용한 엔티티 객체에 다음과 같이 직접 식별자를 할당하고 persist() 하게되는 경우는 어떤 일이 일어날까요?
transaction.begin();
Product product = new Product(0"notebook");                                                 
entityManager.persist(product);
transaction.commit();
cs

 


하이버네이트에서는 다음과 같이 detached entity passed to persist 라는 메시지와 함께 PersistentObjectException 예외가 발생하게 됩니다. IDENTITY 전략을 사용한 엔티티의 식별자는 DB가 알아서 할당하도록 값을 주입하면 안됩니다.
javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: jpatest.model.Product
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:147)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:155)
    ...생략...
cs

 




IDENTITY 전략에서 persist() 이후 트랜잭션 commit() 이전에 예외가 발생하면?
트랜잭션이 커밋되기 전에 쿼리를 실행하는 IDENTITY 전략에서도 insert 쿼리 직후 예외가 발생하면 다음과 같이 트랜잭션이 롤백됩니다.
try { 
    transaction.begin();
    Product product = new Product("notebook");                                         
    entityManager.persist(product);        //insert 쿼리 실행
    if(truethrow new RuntimeException(); //예외 발생 
    transaction.commit();
catch (Exception ex) { 
    ex.printStackTrace();
    transaction.rollback();                //롤백
finally { 
    entityManager.close();
}
cs

 


그러나 INSERT시에 AUTO_INCREMENT 에 의해 8이라는 값이 DB로부터 이미 생성되었기 때문에, MySQL에서 다음 Insert시에는 8 이후의 값을 할당합니다.
23:16:17.750 [main] DEBUG org.hibernate.SQL - insert into product (name) values (?)
23:16:17.762 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [notebook]
23:16:17.766 [main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 8
java.lang.RuntimeException
    at jpatest.main.ProductMain.main(ProductMain.java:22)
23:16:17.776 [main] TRACE o.h.engine.query.spi.QueryPlanCache - Cleaning QueryPlan Cache
cs

 

블로그 이미지

도로락

IT, 프로그래밍, 컴퓨터 활용 정보 등을 위한 블로그

,