하이버네이트 매핑설정에는 lazy 옵션이 있습니다. 연관객체를 언제 로딩할거냐에 대한 옵션입니다. true로 설정하면 하이버네이트는 부모객체를 로딩할 때 그에 딸린 연관객체(자식객체)를 미리 로딩하지 않고 프록시 객체만 만들어둡니다. 그리고는 실제로 연관객체가 사용될 때 연관객체를 로딩하죠. false이면 부모객체가 로딩될 때 연관객체도 함께 미리 로딩합니다.

Employee(부모)와 Achievement(자식) 가 있습니다. 아래처럼 lazy=true 로 설정되어 있기 때문에 Achievement 목록은 실제 쓰이지 않는다면 로딩되지 않습니다.

<class name="Employee" table="EMP">
... ...
<set name="achievements"

       lazy="true" 

      ... ... >

    <key column="EMP_ID" />
    <one-to-many class="Achievement" />
</set>

그러나 연관객체를 미리 로딩하고 싶을 때도 있습니다. 크게 세가지 방법이 있습니다.

 (1) HQL fetch 키워드 사용
left outer join fetch 키워드를 사용하면 lazy=true, fetch=select 이더라도
조인을 통해 1번의 쿼리로 연관객체를 모두 가져옴.
 
Query q = session.createQuery(" from Employee as emp "
        + " left outer join fetch emp.achievements "
        + " where emp.id = :employeeId");

q.setParameter("employeeId", employeeId);
employee = (Employee) q.list().get(0);
 
(2) Criteria에서 fecth mode를 JOIN으로 설정
 
Criteria c = session.createCriteria(Employee.class);
c.setFetchMode("achievements", FetchMode.JOIN);
c.add(Restrictions.idEq(employeeId));
employee = (Employee) c.uniqueResult();
 
(3) Hibernate.initialize 메소드 이용
연관객체를 임의로 미리 로딩. 단, 세션안에서만 사용가능.
 
employee = (Employee) session.get(Employee.class, employeeId);
Hibernate.initialize(employee.getAchievements());

Posted by 에코지오
,
지금까지 트랜잭션 안에 있기만하면 준영속 상태의 객체에서 늦은 로딩이 가능한 줄 알았는데, 그게 아니었다.

늦은 로딩은 영속 상태에서만 즉, 객체가 세션과 연결된 상태에서만 적용된다.

이런 기초적인 것도 모르고 하이버네이트로 코딩을 한다는 것이 부끄럽다.

하이버네이트 객체 생명주기


그럼 준영속 상태의 객체를 파라미터로 받는 메소드(A)에서는 어떻게 늦은 로딩으로 설정된 연관 데이터를 갖고와야할까?

1. 준영속 객체를 session.update(), saveOrUpdate()를 이용하여 영속 상태로 만든 후에 늦은 로딩하는 방법.
- 이 방법은 필요없는 UPDATE 또는 SELECT 쿼리가 추가적으로 실행되는 문제점이 있다.

2. 준영속 객체의 키를 이용하여 session.get()을 통해 영속 객체를 로딩한 후 늦은 로딩하는 방법.
- 이것도 SELECT 쿼리가 추가적으로 실행이 된다.

3. 처음부터 연관데이터를 미리 로딩한 뒤 메소드에 넘겨주는 방법.
- 영속 객체를 메소드 A에 넘기기 전에 Query에서 fetch 키워드를 쓰거나 또는 Hibernate.initialize()를 이용하여 연관 데이터를 미리 객체에 담아둔다. 만약 메소드 A가 시간이 좀 걸리는 작업이고 연관 데이터가 수시로 변한다면 연관 데이터의 싱크가 문제가 될수 있다.

아마 대부분의 경우 별거 아닐텐데 내 경우에는 3번은 불가하고, 1,2번 중에서 골라야 한다. 음...
Posted by 에코지오
,

아래 글의 트랜잭션 처리와 관련하여 내가 겪었던 상황을 살펴볼까한다.

- 삽질했던 상황 -

DB로부터 목록을 조회한 후 목록안의 개별 아이템에 대한 작업을 수행한 뒤
개별 아이템별로 작업결과를 DB에 반영한다.

로직은 별로 복잡하지 않다. 근데 문제는 이거 2가지다.

- 전체 작업이 아니라 개별 작업결과를 커밋하길 원한다.
- 개별 작업은 1~10초 정도 시간이 걸리는 네트워크 작업이다.

처음에 아래처럼 코드를 작성했다.

public void 전체작업() {
  전체 목록 조회;
  for (아이템 : 전체목록) {
      try {
          아이템작업(아이템);
      } catch(에러) {
         //로깅
      }
  }
}

public void 아이템작업(아이템) throws 네트워크에러, DB에러 {
    네트워크작업(아이템);
    작업결과처리(아이템);
}

@Transactional
private void 작업결과처리(아이템) {
     // 아이템 가공
     dao.save(아이템);
}

나름 트랜잭션 스코프도 최소화하고 독립적으로 개별 아이템작업을 처리하기 위해 신경썼는데,

무지막지하게 삽질을 해댔으니... RTFM이 떠오른다.

이제 위 코드에서 왜 트랜잭션이 작동하지 않는지 이유를 알았으니 해결방안을 찾아야겠다.

Posted by 에코지오
,
http://static.springframework.org/spring/docs/2.0.x/reference/transaction.html#transaction-declarative

위 사이트의 9.5.6. Using @Transactional 부분.

첫째, @Transactional은 인터페이스(및 메소드), 구현클래스(및 public 메소드)에 붙일 수 있다.
특히나 public 메소드에만 적용되는 점은 아래와 같이 설명하고 있다.
Method visibility and @Transactional

When using proxies, the @Transactional annotation should only be applied to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error will be raised, but the annotated method will not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

둘째, self-invocation 상황(내부 호출)에서는 적용되지 않는다. 즉 외부의 클래스에서 그 메소드를
호출해야 트랜잭션이 걸린다.

Note: Since this mechanism is based on proxies, only 'external' method calls coming in through the proxy will be intercepted. This means that 'self-invocation', i.e. a method within the target object calling some other method of the target object, won't lead to an actual transaction at runtime even if the invoked method is marked with @Transactional!

며칠 동안 두가지 상황을 모두 겪으면서 삽집한 생각을 하면... 으 ...... 바보같다.. ㅠ.ㅠ

궁금점 : @Transactional을 Class 선언부에 안붙이고 메소드에만 붙이면 어떻게 되는거지?
여기저기 찾아보면 당최 메소드에만 붙어있는 예제가 없다.


Posted by 에코지오
,
하이버네이트는 항상 느끼는 거지만 코딩할 때마다 헷갈리는 부분이 많다.

늦은로딩, 페칭, 세션, 캐쉬, 객체연관, 객체상태, DB와 동기화 시점 등등.

어떡하지...? 뭘 어떡하나. 부지런히 자료찾아보고 테스트하고 메모해나가는 수밖에....

이미 그런 분들이 꽤 계시다...  특히 기선님하이버네이트3 책 요약 시리즈

나처럼 우매한 대중에게는 너무 고마운 자료다.

일단 기초적인거 하나 붙잡고 읽어보자.
 
하이버네이트 API: 저장하고 읽어들이기

session.load() 메소드 사용팁을 잘 정리해주셨다.

DB에 있는 객체 하나를 꺼내서 다른 객체에 세팅해줘야 하는 경우. Comment.setForAuction(item). 이 경우 굳이 item은 load()로 가져와도 된다. 굳이 DB에서 전부 가져올 필요가 없다. Comment를 저장할 때, item의 id를 외례키로 저장하게 되는데, load()로 가져온 Proxy가 딱 그 id만 가지고 있기 때문이다

session.delete() 호출후 식별자를 제거하기 위한 팁도 있다.

Transient 상태가 될 때, 식별자도 제거하려면 hibernate.use_identifier_rollback 이 설정을 해줘야돼.

다른 글도 읽어보자. 다 피가 되고 살이 된다.
 

Posted by 에코지오
,
Spring+Hibernate 조합 환경에서 단위테스트에 대한 토비님의 글.

AppFuse DAO Test 코드의 문제점

Rod Johnson의 Testing with Spring

Spring기반의 Hibernate DAO Unit Test 만들기

특히나 하이버네이트의 1차레벨 캐쉬 때문에

C/U/D 한 뒤 다시 객체를 읽어오는 경우 읽기전에 세션을 flush 해주어야 한다는 지적은

AppFuse 만든 Matt도 미처 몰랐던 사실!


spring-test 모듈의 AbstractTransactionalJUnit4SpringContextTests 사용환경에서는 아래처럼
만들어 놓고 중간중간 flushAndClearSession(); 해주면 될 듯. 
 @Autowired
 protected SessionFactory sessionFactory;

 protected Session getCurrentSession() {
  return SessionFactoryUtils.getSession(sessionFactory, true);
 }

 protected void flushSession() {
  getCurrentSession().flush();
 }

 protected void flushAndClearSession() {
  Session s = getCurrentSession();
  s.flush();
  s.clear();
 }



Posted by 에코지오
,