1. 소개

이 사용방법(예제)에서는 Hibernate로 작업하는 동안 발생할 수 있는 몇 가지 일반적인 예외에 대해 설명합니다.

우리는 그들의 목적과 몇 가지 일반적인 원인을 검토할 것입니다. 또한 솔루션을 살펴볼 것입니다.

2. Hibernate 예외 개요

Hibernate를 사용하는 동안 많은 조건으로 인해 예외가 발생할 수 있습니다. 매핑 오류, 인프라 문제, SQL 오류, 데이터 무결성 위반, 세션 문제 및 트랜잭션 오류일 수 있습니다.

이러한 예외는 대부분 HibernateException 에서 확장됩니다 . 그러나 Hibernate를 JPA 지속성 공급자로 사용하는 경우 이러한 예외는 PersistenceException 으로 래핑될 수 있습니다  .

이러한 기본 클래스는 모두 RuntimeException 에서 확장됩니다 . 따라서 모두 선택 해제되어 있습니다. 따라서 우리는 그것들이 사용되는 모든 장소에서 그것들을 잡거나 선언할 필요가 없습니다.

더욱이 이들 중 대부분은 복구할 수 없습니다. 결과적으로 작업을 재시도해도 도움이 되지 않습니다. 이것은 우리가 그들을 만나면 현재 세션을 포기해야 한다는 것을 의미합니다.

이제 이들 각각을 한 번에 하나씩 살펴보겠습니다.

3. 매핑 오류

객체-관계형 매핑은 Hibernate의 주요 이점입니다. 특히 SQL 문을 수동으로 작성하지 않아도 됩니다.

동시에 Java 개체와 데이터베이스 테이블 간의 매핑을 지정해야 합니다. 따라서 어노테이션을 사용하거나 매핑 문서를 통해 지정합니다. 이러한 매핑은 수동으로 코딩할 수 있습니다. 또는 도구를 사용하여 생성할 수 있습니다.

이러한 매핑을 지정하는 동안 실수할 수 있습니다. 매핑 사양에 있을 수 있습니다. 또는 Java 개체와 해당 데이터베이스 테이블 간에 불일치가 있을 수 있습니다.

이러한 매핑 오류는 예외를 생성합니다. 초기 개발 중에 자주 접하게 됩니다. 또한 환경 간에 변경 사항을 마이그레이션하는 동안 문제가 발생할 수 있습니다.

몇 가지 예를 통해 이러한 오류를 살펴보겠습니다.

3.1. 매핑 예외

개체-관계형 매핑 문제로 인해  MappingException 이 발생합니다 .

public void whenQueryExecutedWithUnmappedEntity_thenMappingException() {
    thrown.expectCause(isA(MappingException.class));
    thrown.expectMessage("Unknown entity: java.lang.String");

    Session session = sessionFactory.getCurrentSession();
    NativeQuery<String> query = session
      .createNativeQuery("select name from PRODUCT", String.class);
    query.getResultList();
}

위의 코드에서  createNativeQuery 메서드는 쿼리 결과를 지정된 Java 유형  문자열에 매핑하려고 시도합니다. Metamodel 에서 String 클래스 의 암시적 매핑을 사용하여 매핑  을 수행합니다.

그러나 String 클래스에는 지정된 매핑이 없습니다. 따라서 Hibernate는 name  열을 String 에 매핑하는 방법을 모르고 예외를 throw합니다.

가능한 원인 및 솔루션에 대한 자세한 분석은  Hibernate Mapping Exception – Unknown Entity 를 확인하십시오 .

마찬가지로 다른 오류로 인해 이 예외가 발생할 수도 있습니다.

  • 필드 및 메소드에 대한 어노테이션 혼합
  • @ManyToMany 연관 에 대한 @JoinTable 지정 실패 
  • 매핑된 클래스의 기본 생성자가 매핑 처리 중에 예외를 throw합니다.

또한 MappingException 에는 특정 매핑 문제를 나타낼 수 있는 몇 가지 하위 클래스가 있습니다.

  • AnnotationException – 어노테이션  관련 문제
  • DuplicateMappingException – 클래스, 테이블 또는 속성 이름에 대한 중복 매핑
  • InvalidMappingException –  매핑이 잘못 되었습니다.
  • MappingNotFoundException –  매핑 리소스를 찾을 수 없습니다.
  • PropertyNotFoundException –  클래스에서 예상되는 getter 또는 setter 메서드를 찾을 수 없습니다.

따라서  이 예외가 발생하면 먼저 매핑을 확인해야 합니다 .

3.2. 어노테이션 예외

AnnotationException 을 이해하기 위해  필드나 속성에 식별자 어노테이션이 없는 항목을 만들어 보겠습니다.

@Entity
public class EntityWithNoId {
    private int id;
    public int getId() {
        return id;
    }

    // standard setter
}

Hibernate는 모든 엔터티가 식별자 를 가질 것으로 예상 하므로 엔터티를 사용할 때 AnnotationException 을 얻게  됩니다.

public void givenEntityWithoutId_whenSessionFactoryCreated_thenAnnotationException() {
    thrown.expect(AnnotationException.class);
    thrown.expectMessage("No identifier specified for entity");

    Configuration cfg = getConfiguration();
    cfg.addAnnotatedClass(EntityWithNoId.class);
    cfg.buildSessionFactory();
}

또한 다른 가능한 원인은 다음과 같습니다.

  • @GeneratedValue 어노테이션 에 사용된 알 수 없는 시퀀스 생성기
  • Java 8 날짜 / 시간 클래스 와 함께 사용되는 @Temporal 어노테이션
  • @ManyToOne 또는 @OneToMany 에 대한 대상 엔티티가 없거나 존재하지 않음
  • 관계 어노테이션 @OneToMany 또는 @ManyToMany 와 함께 사용되는 원시 컬렉션 클래스
  • Hibernate가 컬렉션 인터페이스를 예상하므로 컬렉션 어노테이션 @OneToMany , @ManyToMany 또는 @ElementCollection 과 함께 사용되는 구체적인 클래스

이 예외를 해결하려면 먼저 오류 메시지에 언급된 특정 어노테이션을 확인해야 합니다.

3.3. QuerySyntaxException

자세한 내용을 살펴보기 전에 예외가 무엇을 의미하는지 이해해 봅시다.

QuerySyntaxException 은 이름에서 알 수 있듯이 지정된 쿼리에 잘못된 구문이 있음을 알려줍니다 .

이 예외의 가장 일반적인 원인은 HQL 쿼리에서 클래스 이름 대신 테이블 이름을 사용하기 때문입니다 .

예를 들어 Product 엔터티를 고려해 보겠습니다.

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "PRODUCT")
public class Product {

    private int id;
    private String name;
    private String description;

    // Getters and setters
}

@Entity 는 어노테이션이 달린 클래스가 엔티티 임을 나타냅니다 . 이 클래스는 데이터베이스에 저장된 테이블을 나타냅니다.

일반적으로 테이블 이름은 엔터티 이름과 다를 수 있습니다. 그래서 @Table 이 구출되는 곳입니다. 데이터베이스에 있는 테이블의 정확한 이름을 지정할 수 있습니다.

이제 테스트 사례를 사용하여 예외를 예시해 보겠습니다.

@Test
public void whenQueryExecutedWithInvalidClassName_thenQuerySyntaxException() {
    thrown.expectCause(isA(QuerySyntaxException.class));
    thrown.expectMessage("PRODUCT is not mapped [from PRODUCT]");

    Session session = sessionFactory.openSession();
    List<Product> result = session.createQuery("from PRODUCT", Product.class)
        .getResultList();
}

알 수 있듯이 Hibernate는 쿼리에서 Product 대신 PRODUCT 를 사용했기 때문에 QuerySyntaxException 과 함께 실패합니다 . 즉, 쿼리에서 테이블 이름이 아닌 엔터티 이름을 사용해야 합니다.

4. 스키마 관리 오류

자동 데이터베이스 스키마 관리는 Hibernate의 또 다른 이점입니다. 예를 들어 데이터베이스 개체를 생성하거나 유효성을 검사하기 위해 DDL 문을 생성할 수 있습니다.

이 기능을 사용하려면 hibernate.hbm2ddl.auto  속성을 적절하게 설정해야 합니다.

스키마 관리를 수행하는 동안 문제가 발생하면 예외가 발생합니다. 이러한 오류를 살펴보겠습니다.

4.1. 스키마 관리 예외

스키마 관리를 수행할 때 인프라 관련 문제가 발생하면 SchemaManagementException 이 발생합니다 .

시연을 위해 데이터베이스 스키마를 검증하도록 Hibernate에 지시해 봅시다.

public void givenMissingTable_whenSchemaValidated_thenSchemaManagementException() {
    thrown.expect(SchemaManagementException.class);
    thrown.expectMessage("Schema-validation: missing table");

    Configuration cfg = getConfiguration();
    cfg.setProperty(AvailableSettings.HBM2DDL_AUTO, "validate");
    cfg.addAnnotatedClass(Product.class);
    cfg.buildSessionFactory();
}

Product 에 해당하는 테이블 이 데이터베이스에 없기 때문에 S essionFactory  를 빌드하는 동안 스키마 유효성 검사 예외가 발생합니다 .

또한 이 예외에 대한 다른 가능한 시나리오가 있습니다.

  • 스키마 관리 작업을 수행하기 위해 데이터베이스에 연결할 수 없습니다.
  • 스키마가 데이터베이스에 없습니다.

4.2. CommandAcceptanceException

특정 스키마 관리 명령에 해당하는 DDL을 실행하는 데 문제가 있으면 CommandAcceptanceException 이 발생할 수 있습니다 .

예를 들어 SessionFactory 를 설정하는 동안 잘못된 방언을 지정해 보겠습니다  .

public void whenWrongDialectSpecified_thenCommandAcceptanceException() {
    thrown.expect(SchemaManagementException.class);
        
    thrown.expectCause(isA(CommandAcceptanceException.class));
    thrown.expectMessage("Halting on error : Error executing DDL");

    Configuration cfg = getConfiguration();
    cfg.setProperty(AvailableSettings.DIALECT,
      "org.hibernate.dialect.MySQLDialect");
    cfg.setProperty(AvailableSettings.HBM2DDL_AUTO, "update");
    cfg.setProperty(AvailableSettings.HBM2DDL_HALT_ON_ERROR,"true");
    cfg.getProperties()
      .put(AvailableSettings.HBM2DDL_HALT_ON_ERROR, true);

    cfg.addAnnotatedClass(Product.class);
    cfg.buildSessionFactory();
}

여기에서 잘못된 방언인  MySQLDialect를 지정했습니다. 또한 스키마 개체를 업데이트하도록 Hibernate에 지시하고 있습니다. 결과적으로 Hibernate가 H2 데이터베이스를 업데이트하기 위해 실행한 DDL 문은 실패하고 예외가 발생합니다.

기본적으로 Hibernate는 이 예외를 자동으로 기록하고 계속 진행합니다.  나중에 SessionFactory를  사용 하면 예외가 발생합니다.

이 오류에서 예외가 발생하도록  HBM2DDL_HALT_ON_ERROR 속성 을 true 로 설정했습니다 .

마찬가지로 이 오류의 다른 일반적인 원인은 다음과 같습니다.

  • 매핑과 데이터베이스 간에 열 이름이 일치하지 않습니다.
  • 두 클래스가 동일한 테이블에 매핑됨
  • 클래스 또는 테이블에 사용되는 이름 은 예를 들어 USER 와 같이 데이터베이스의 예약어입니다.
  • 데이터베이스에 연결하는 데 사용된 사용자에게 필요한 권한이 없습니다.

5. SQL 실행 오류

Hibernate를 사용하여 데이터를 삽입, 업데이트, 삭제 또는 쿼리할 때 JDBC를 사용하여 데이터베이스에 대해 DML 문을 실행합니다 . 작업 결과 오류 또는 경고가 발생하면 이 API는 SQLException 을 발생시킵니다.

Hibernate는 이 예외를 JDBCException 또는 적절한 하위 클래스 중 하나로 변환합니다.

  • ConstraintViolationException
  • 데이터 예외
  • JDBC연결 예외
  • LockAcquisitionException
  • PessimisticLockException
  • QueryTimeout 예외
  • SQL문법 예외
  • 일반JDBC예외

일반적인 오류에 대해 논의해 봅시다.

5.1. JDBC예외

JDBCException 은 항상 특정 SQL 문으로 인해 발생합니다. 문제가 되는 SQL 문을 가져오기 위해 getSQL 메서드를 호출할 수 있습니다 .

또한 getSQLException 메소드 를 사용 하여 기본 SQLException 을 검색할 수 있습니다.

5.2. SQL문법 예외

SQLGrammarException 은 데이터베이스로 전송된 SQL이 유효하지 않음을 나타냅니다. 구문 오류 또는 잘못된 개체 참조 때문일 수 있습니다.

예를 들어 누락된 테이블로 인해 데이터를 쿼리하는 동안 다음 오류가 발생할 수 있습니다 .

public void givenMissingTable_whenQueryExecuted_thenSQLGrammarException() {
    thrown.expect(isA(PersistenceException.class));
    thrown.expectCause(isA(SQLGrammarException.class));
    thrown.expectMessage("SQLGrammarException: could not prepare statement");

    Session session = sessionFactory.getCurrentSession();
    NativeQuery<Product> query = session.createNativeQuery(
      "select * from NON_EXISTING_TABLE", Product.class);
    query.getResultList();
}

또한 테이블이 누락된 경우 데이터를 저장하는 동안 이 오류가 발생할 수 있습니다.

public void givenMissingTable_whenEntitySaved_thenSQLGrammarException() {
    thrown.expect(isA(PersistenceException.class));
    thrown.expectCause(isA(SQLGrammarException.class));
    thrown
      .expectMessage("SQLGrammarException: could not prepare statement");

    Configuration cfg = getConfiguration();
    cfg.addAnnotatedClass(Product.class);

    SessionFactory sessionFactory = cfg.buildSessionFactory();
    Session session = null;
    Transaction transaction = null;
    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();
        Product product = new Product();
        product.setId(1);
        product.setName("Product 1");
        session.save(product);
        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
        closeSessionFactoryQuietly(sessionFactory);
    }
}

다른 가능한 원인은 다음과 같습니다.

  • 사용된 이름 지정 전략이 클래스를 올바른 테이블에 매핑하지 않음
  • @JoinColumn 에 지정된 열 이 존재하지 않습니다.

5.3. ConstraintViolationException

ConstraintViolationException 은 요청 된 DML 작업으로 인해 무결성 제약 조건이 위반되었음을 나타냅니다. getConstraintName 메서드 를 호출하여 이 제약 조건의 이름을 가져올 수 있습니다 .

이 예외의 일반적인 원인은 중복 레코드를 저장하려는 것입니다.

public void whenDuplicateIdSaved_thenConstraintViolationException() {
    thrown.expect(isA(PersistenceException.class));
    thrown.expectCause(isA(ConstraintViolationException.class));
    thrown.expectMessage(
      "ConstraintViolationException: could not execute statement");

    Session session = null;
    Transaction transaction = null;

    for (int i = 1; i <= 2; i++) {
        try {
            session = sessionFactory.openSession();
            transaction = session.beginTransaction();
            Product product = new Product();
            product.setId(1);
            product.setName("Product " + i);
            session.save(product);
            transaction.commit();
        } catch (Exception e) {
            rollbackTransactionQuietly(transaction);
            throw (e);
        } finally {
            closeSessionQuietly(session);
        }
    }
}

또한 데이터베이스의 NOT NULL 열에 null 값을 저장하면 이 오류가 발생할 수 있습니다.

이 오류를 해결 하려면 비즈니스 계층에서 모든 유효성 검사를 수행해야 합니다 . 또한 애플리케이션 유효성 검사를 수행하는 데 데이터베이스 제약 조건을 사용해서는 안 됩니다.

5.4. 데이터 예외

DataException 은 SQL 문의 평가 결과 일부 잘못된 작업, 유형 불일치 또는 잘못된 카디널리티가 발생했음을 나타냅니다.

예를 들어 숫자 열에 대해 문자 데이터를 사용하면 다음 오류가 발생할 수 있습니다.

public void givenQueryWithDataTypeMismatch_WhenQueryExecuted_thenDataException() {
    thrown.expectCause(isA(DataException.class));
    thrown.expectMessage(
      "org.hibernate.exception.DataException: could not prepare statement");

    Session session = sessionFactory.getCurrentSession();
    NativeQuery<Product> query = session.createNativeQuery(
      "select * from PRODUCT where id='wrongTypeId'", Product.class);
    query.getResultList();
}

이 오류를 수정하려면 애플리케이션 코드와 데이터베이스 간에 데이터 유형과 길이가 일치하는지 확인해야 합니다 .

5.5. JDBC연결 예외

JDBCConectionException 은 데이터베이스와 통신하는 데 문제가 있음을 나타냅니다 .

예를 들어 데이터베이스나 네트워크가 다운되면 이 예외가 발생할 수 있습니다.

또한 잘못된 데이터베이스 설정으로 인해 이 예외가 발생할 수 있습니다. 이러한 경우 중 하나는 오랫동안 유휴 상태였기 때문에 서버에서 데이터베이스 연결을 닫는 것입니다. 이는 연결 풀링 을 사용 중이고 풀 의 유휴 시간 제한 설정이 데이터베이스의 연결 시간 제한 값보다 큰 경우에 발생할 수 있습니다.

이 문제를 해결하려면 먼저 데이터베이스 호스트가 있고 작동하는지 확인해야 합니다. 그런 다음 데이터베이스 연결에 올바른 인증이 사용되었는지 확인해야 합니다. 마지막으로 연결 풀에 시간 초과 값이 올바르게 설정되어 있는지 확인해야 합니다.

5.6. QueryTimeout 예외

데이터베이스 쿼리 시간이 초과되면 이 예외가 발생합니다. 테이블스페이스가 가득 차는 것과 같은 다른 오류로 인해 이를 볼 수도 있습니다.

이는 몇 가지 복구 가능한 오류 중 하나이며 동일한 트랜잭션에서 명령문을 다시 시도할 수 있음을 의미합니다.

이 문제를 해결하기 위해  여러 가지 방법으로 장기 실행 쿼리에 대한 쿼리 제한 시간을 늘릴 수 있습니다 .

  • @NamedQuery 또는 @NamedNativeQuery 어노테이션 에서 시간 제한 요소 설정
  • Query 인터페이스 의 setHint 메소드 호출 
  • Transaction 인터페이스 의 setTimeout 메소드  호출
  • 조회 인터페이스 의 setTimeout 메소드 호출

6. 세션 상태 관련 오류

이제 Hibernate 세션 사용 오류로 인한 오류를 살펴보겠습니다.

6.1. 고유하지 않은 객체 예외

최대 절전 모드는 단일 세션에서 동일한 식별자를 가진 두 개체를 허용하지 않습니다.

동일한 Java 클래스의 두 인스턴스를 단일 세션에서 동일한 식별자와 연결하려고 하면 NonUniqueObjectException 이 발생 합니다. getEntityName()getIdentifier() 메서드 를 호출하여 엔터티의 이름과 식별자를 가져올 수 있습니다  .

이 오류를 재현하기 위해 동일한 ID를 가진 두 개의 제품 인스턴스를 세션과 함께 저장해 보겠습니다.

public void 
givenSessionContainingAnId_whenIdAssociatedAgain_thenNonUniqueObjectException() {
    thrown.expect(isA(NonUniqueObjectException.class));
    thrown.expectMessage(
      "A different object with the same identifier value was already associated with the session");

    Session session = null;
    Transaction transaction = null;

    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();

        Product product = new Product();
        product.setId(1);
        product.setName("Product 1");
        session.save(product);

        product = new Product();
        product.setId(1);
        product.setName("Product 2");
        session.save(product);

        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
    }
}

예상대로 NonUniqueObjectException  이  발생합니다.

이 예외는 업데이트 메서드 를 호출하여 분리된 개체를 세션과 다시 연결하는 동안 자주 발생합니다 . 세션에 동일한 식별자가 로드된 다른 인스턴스가 있는 경우 이 오류가 발생합니다. 이 문제를 해결하기 위해 병합 방법 을 사용 하여 분리된 개체를 다시 연결할 수 있습니다.

6.2. StaleStateException

Hibernate는 버전 번호 또는 타임스탬프 확인이 실패할 때 StaleStateException 을 발생시킵니다. 세션에 오래된 데이터가 포함되어 있음을 나타냅니다.

때때로 이것은  OptimisticLockException 으로 래핑됩니다 .

이 오류는 일반적으로 버전 관리와 함께 장기 실행 트랜잭션을 사용하는 동안 발생합니다.

또한 해당 데이터베이스 행이 존재하지 않는 경우 엔터티를 업데이트하거나 삭제하려고 시도하는 동안에도 발생할 수 있습니다.

public void whenUpdatingNonExistingObject_thenStaleStateException() {
    thrown.expect(isA(OptimisticLockException.class));
    thrown.expectMessage(
      "Batch update returned unexpected row count from update");
    thrown.expectCause(isA(StaleStateException.class));

    Session session = null;
    Transaction transaction = null;

    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();

        Product product = new Product();
        product.setId(15);
        product.setName("Product1");
        session.update(product);
        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
    }
}

다른 가능한 시나리오는 다음과 같습니다.

  • 엔터티에 대한 적절한 미저장 가치 전략을 지정하지 않았습니다.
  • 두 명의 사용자가 거의 동시에 동일한 행을 삭제하려고 했습니다.
  • 자동 생성된 ID 또는 버전 필드에 수동으로 값을 설정합니다.

7. 지연 초기화 오류

우리는 일반적으로 응용 프로그램 성능을 향상시키기 위해 연결이 느리게 로드되도록 구성합니다. 연결은 처음 사용될 때만 가져옵니다.

그러나 Hibernate는 데이터를 가져오기 위해 활성 세션이 필요합니다. 초기화되지 않은 연결에 액세스하려고 할 때 세션이 이미 닫혀 있으면 예외가 발생합니다.

이 예외와 이를 수정하는 다양한 방법을 살펴보겠습니다.

7.1. Lazy 초기화 예외

LazyInitializationException 은 활성 세션 외부에서 초기화되지 않은 데이터를 로드하려는 시도를 나타냅니다. 많은 시나리오에서 이 오류가 발생할 수 있습니다.

첫째, 표현 계층에서 Lazy 관계에 액세스하는 동안 이 예외를 얻을 수 있습니다. 그 이유는 엔터티가 비즈니스 계층에 부분적으로 로드되고 세션이 닫혔기 때문입니다.

둘째, getOne 메서드 를 사용하면 Spring Data 에서 이 오류를 얻을 수 있습니다 . 이 메서드는 인스턴스를 느리게 가져옵니다.

이 예외를 해결하는 방법에는 여러 가지가 있습니다.

우선, 우리는 모든 관계를 간절히 불러일으킬 수 있습니다. 그러나 이것은 사용되지 않을 데이터를 로드하게 되므로 애플리케이션 성능에 영향을 미칩니다.

둘째, 뷰가 렌더링될 때까지 세션을 열린 상태로 유지할 수 있습니다. 이것은 " Open Session in View "로 알려져 있으며 안티 패턴입니다. 몇 가지 단점이 있으므로 이를 피해야 합니다.

셋째, 관계를 가져오기 위해 다른 세션을 열고 엔터티를 다시 연결할 수 있습니다. 세션 에서 병합 방법을 사용하여 그렇게 할 수 있습니다 .

마지막으로 비즈니스 계층에서 필요한 연결을 초기화할 수 있습니다. 다음 섹션에서 이에 대해 논의하겠습니다.

7.2. 비즈니스 계층에서 관련 지연 관계 초기화

Lazy 관계를 초기화하는 방법에는 여러 가지가 있습니다.

한 가지 옵션은 엔터티에서 해당 메서드를 호출하여 초기화하는 것입니다. 이 경우 Hibernate는 성능 저하를 유발하는 여러 데이터베이스 쿼리를 발행합니다. 이를 "N+1 SELECT" 문제라고 합니다.

둘째, Fetch Join 을 사용하여 단일 쿼리로 데이터를 가져올 수 있습니다. 그러나 이를 달성하려면 사용자 지정 코드를 작성해야 합니다.

마지막으로 엔티티 그래프 를 사용 하여 가져올 모든 속성을 정의할 수 있습니다 . @NamedEntityGraph, @NamedAttributeNode@NamedEntitySubgraph 어노테이션을 사용 하여 엔티티 그래프를 선언적으로 정의할 수 있습니다. JPA API를 사용하여 프로그래밍 방식으로 정의할 수도 있습니다. 그런 다음 가져오기 작업에서 지정하여 단일 호출로 전체 그래프를 검색합니다 .

8. 거래 문제

트랜잭션 은 작업 단위와 동시 활동 간의 격리를 정의합니다. 두 가지 방법으로 구분할 수 있습니다. 첫째, 어노테이션을 사용하여 선언적으로 정의할 수 있습니다. 둘째, Hibernate Transaction API 를 사용하여 프로그래밍 방식으로 관리할 수 있습니다 .

게다가 Hibernate는 트랜잭션 관리를 트랜잭션 관리자에게 위임합니다. 어떤 이유로든 트랜잭션을 시작, 커밋 또는 롤백할 수 없는 경우 Hibernate는 예외를 발생시킵니다.

일반적으로  트랜잭션 관리자에 따라 TransactionException  또는  IllegalArgumentException 이 발생합니다.

예를 들어 롤백용으로 표시된 트랜잭션을 커밋해 보겠습니다.

public void 
givenTxnMarkedRollbackOnly_whenCommitted_thenTransactionException() {
    thrown.expect(isA(TransactionException.class));
    thrown.expectMessage(
        "Transaction was marked for rollback only; cannot commit");

    Session session = null;
    Transaction transaction = null;
    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();

        Product product = new Product();
        product.setId(15);
        product.setName("Product1");
        session.save(product);
        transaction.setRollbackOnly();

        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
    }
}

마찬가지로 다른 오류로 인해 예외가 발생할 수도 있습니다.

  • 선언적 및 프로그래밍 방식 트랜잭션 혼합
  • 세션에서 다른 트랜잭션이 이미 활성 상태일 때 트랜잭션 시작 시도
  • 트랜잭션을 시작하지 않고 커밋 또는 롤백 시도
  • 트랜잭션 커밋 또는 롤백을 여러 번 시도

9. 동시성 문제

Hibernate는 동시 트랜잭션으로 인한 데이터베이스 불일치를 방지하기 위해 낙관적비관적 두 가지 잠금 전략을 지원합니다 . 둘 다 잠금 충돌의 경우 예외를 발생시킵니다.

높은 동시성과 높은 확장성을 지원하기 위해 일반적으로 버전 확인과 함께 낙관적 동시성 제어를 사용합니다. 충돌하는 업데이트를 감지하기 위해 버전 번호 또는 타임스탬프를 사용합니다.

낙관적 잠금 충돌을 나타내기 위해 OptimisticLockingException 이 발생합니다 . 예를 들어 첫 번째 작업 후 새로 고치지 않고 동일한 엔터티를 두 번 업데이트하거나 삭제하면 이 오류가 발생합니다.

public void whenDeletingADeletedObject_thenOptimisticLockException() {
    thrown.expect(isA(OptimisticLockException.class));
    thrown.expectMessage(
        "Batch update returned unexpected row count from update");
    thrown.expectCause(isA(StaleStateException.class));

    Session session = null;
    Transaction transaction = null;

    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();

        Product product = new Product();
        product.setId(12);
        product.setName("Product 12");
        session.save(product1);
        transaction.commit();
        session.close();

        session = sessionFactory.openSession();
        transaction = session.beginTransaction();
        product = session.get(Product.class, 12);
        session.createNativeQuery("delete from Product where id=12")
          .executeUpdate();
        // We need to refresh to fix the error.
        // session.refresh(product);
        session.delete(product);
        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
    }
}

마찬가지로 두 명의 사용자가 거의 동시에 동일한 엔터티를 업데이트하려고 하면 이 오류가 발생할 수 있습니다. 이 경우 첫 번째는 성공할 수 있고 두 번째는 이 오류를 발생시킵니다.

따라서 비관적 잠금을 도입하지 않고는 이 오류를 완전히 피할 수 없습니다 . 그러나 다음을 수행하여 발생 가능성을 최소화할 수 있습니다.

  • 업데이트 작업을 가능한 한 짧게 유지
  • 가능한 한 자주 클라이언트의 엔터티 표현을 업데이트합니다.
  • 엔터티 또는 엔터티를 나타내는 값 개체를 캐시하지 마십시오.
  • 업데이트 후 항상 클라이언트에서 엔터티 표현을 새로 고칩니다.

10. 결론

이 기사에서는 Hibernate를 사용하는 동안 발생하는 몇 가지 일반적인 예외를 살펴보았습니다. 또한 가능한 원인과 해결 방법을 조사했습니다.

늘 그렇듯이 전체 소스 코드는  GitHub 에서 찾을 수 있습니다 .

Persistence footer banner