페칭 방도는 어플리케이션이 연관을 네비게이트할 필요가 있을 때 Hibernate가 연관된 객체들을 검색하는데 사용하게 될 방도이다.페치 방도들은 O/R 매핑 메타데이터 내에서 선언될 수 있거나 하나의 특정 HQL 또는 Criteria 질의에 의해 오버라이드 될 수도 있다.
Hibernate3는 다음 페칭 방도들을 정의한다:
Join 페칭 - Hibernate는 OUTER JOIN을 사용하여 연관된 인스턴스 또는 동일한 SELECT 내에서 콜렉션을 검색한다.
Select 페칭 - 두 번째 SELECT는 연과된 엔티티 또는 콜렉션을 검색하는데 사용된다. 당신이 lazy="false"를 지정함으로써 명시적으로 lazy 페칭을 사용 불가능하게 하지 않는 한, 이 두 번째 select는 당신이 그 연관에 실제로 액세스할 때 오직 실행될 것이다.
Subselect 페칭 - 두 번째 SELECT는 이전 질의 또는 페치에서 검색된 모든 엔티티들에 대해 연관된 콜렉션들을 검색하는데 사용된다. 당신이 lazy="false"를 지정하여 명시적으로 lazy 페칭을 사용 불가능하게 하지 않는 한, 이 두 번째 select는 당신이 실제로 그 연관에 접근할 때 오직 실행될 것이다.
Batch 페칭 - select 페칭을 위한 최적화 방도 - Hibernate는 프라이머리 키들이나 foreign 키들의 리스트를 지정함으로써 하나의SELECT 내에서 엔티티 인스턴스들이나 콜렉션들에 대한 batch를 검색한다.
Hibernate는 또한 다음 사이를 구별 짓는다:
즉각적인 페칭 - 소유자가 로드될 때, 연관, 콜렉션 또는 속성이 즉시 페치된다.
Lazy 콜렉션 페칭 - 어플리케이션이 그 콜렉션에 대해 하나의 오퍼레이션을 호출할 때 콜렉션이 페치된다.(이것은 콜렉션들에 대해 디폴트이다.)
"Extra-lazy" 콜렉션 페칭 - 콜렉션의 개별 요소들은 필요할 때 데이터베이스로부터 접근된다. Hibernate는 절대적으로 필요하지 않은 한 전체 콜렉션을 메모리 내로 페치하려고 시도하지 않는다(매우 큰 콜렉션에 적합함)
프락시 페칭 - 식별자 getter가 아닌 다른 메소드가 연관된 객체에 대해 호출될 때 단일 값 연관이 페치된다.
"No-proxy" 페칭 - 인스턴스 변수가 접근될 때 단일 값 연관이 페치된다. 프락시 페칭과 비교할 때, 이 접근법은 다소 덜 lazy하지만(그 연관은 심지어 유일하게 식별자가 접근될 때에도 페치된다)보다 투명하다. 왜냐하면 프락시는 어플리케이션에 가시적이지 않기 때문이다. 이 접근법은 빌드 시 바이트코드 수단을 필요로 하며 드물게 필요하다.
Lazy 속성 페칭 - 인스턴스 변수가 접근될 때 속성 또는 단일 값 연관이 페치된다 이 접근법은 빌드시 바이트코드 수단을 필요로 하며 드물게 필요하다.
우리는 여기서 두 개의 직교하는 개념들을 갖는다: 연관이 페치될 때, 그리고 그것이 페치되는 방법(사용되는 SQL). 그것들을 혼동하지 말라! 우리는 퍼포먼스를 튜팅하는데 페치를 사용한다. 우리는 특정 클래스의 어떤 detached 인스턴스 내에서 항상 이용 가능한 데이터가 무엇인지에 대한 계약을 정의하는데 lazy를 사용할 수 있다.
디폴트로 Hibernate3는 콜렉션들에 대해 lazy select 페칭을 사용하고, 단일 값 연관들에 대해 lazy 프락시 페칭을 사용한다. 이들 디폴트들은 거의 모든 어플리케이션들에서 거의 모든 연관들에 대해 유의미하다.
노트: 만일 당신이 hibernate.default_batch_fetch_size를 설정하는 경우, Hibernate는 lazy 페칭을 위한 batch 페치 최적화를 사용할 것이다(이 최적화는 또한 더 많은 과립상의 레벨에서 이용 가능할 수 있다).
하지만, lazy 페칭은 당신이 알고 있어야 하는 한 가지 문제를 제기한다. 열려진 Hibernate 세션 컨텍스트 외부에서 lazy 연관에 대한 접근은 예외상황으로 귀결될 것이다. 예를 들면 :
s = sessions.openSession(); Transaction tx = s.beginTransaction(); User u = (User) s.createQuery("from User u where u.name=:userName") .setString("userName", userName).uniqueResult(); Map permissions = u.getPermissions(); tx.commit(); s.close(); Integer accessLevel = (Integer) permissions.get("accounts"); // Error!
Session이 닫혔을 때 permissions 콜렉션이 초기화 되지 않았으므로, 그 콜렉션은 그것의 상태를 로드시킬 수가 없을 것이다. Hibernate 는 detached 객체들에 대한 lazy 초기화를 지원하지 않는다. 정정은 콜렉션으로부터 읽어들이는 코드를 커밋 바로 직전으로 이동시키는 것이다.
다른 방법으로 연관 매핑에 대해 lazy="false"를 지정함으로써, non-lazy 콜렉션 또는 non-lazy 연관을 사용할 수 있다. 하지만 lazy 초기화는 거의 모든 콜렉션들과 연관들에 대해 사용되도록 고안되어 있다. 만일 당신이 당신의 객체 모형 내에 너무 많은 non-lazy 연관들을 정의할 경우, Hibernate는 모든 트랜잭션에서 전체 데이터베이스를 메모리 속으로 페치하는 필요성을 끝내게 될 것이다!
다른 한편으로, 우리는 특정 트랜잭션 내에서 select 페칭 대신에 (고유하게 non-lazy인) join 페칭을 선택하기를 자주 원한다. 우리는 이제 페칭 방도를 맞춤화 시키는 방법을 알게 될 것이다. Hibernate3에서, 페치 방도를 선택하는 메커니즘은 단일 값 연관들과 콜렉션들에 대해 동일하다.
select 페칭(디폴트)은 N+1 selects 문제점들에 매우 취약해서, 우리는 매핑 문서에서 join 페칭을 사용 가능하게 하기를 원할 수도 있다:
<set name="permissions" fetch="join"> <key column="userId"/> <one-to-many class="Permission"/> </set
<many-to-one name="mother" class="Cat" fetch="join"/>
매핑 문서 내에 정의된 fetch 방도는 다음에 영향을 준다:
get() 또는 load()를 통한 검색
연관이 네비게이트될 때 함축적으로 발생하는 검색
Criteria 질의들
subselect 페칭이 사용될 경우에 HQL 질의들
당신이 사용하는 페칭 방도가 무엇인가에 상관없이, 정의된 비-lazy 그래프가 메모리 내로 로드되는 것이 보장된다. 이것은 하나의 특별한 HQL 질의를 실행시키는데 사용되는 몇몇 즉시적인 select들로 귀결될 수 있음을 노트하라.
대개, 우리는 페칭을 맞춤화 시키는데 매핑 문서를 사용하지 않는다. 대신에, 우리는 디폴트 특징을 유지하고, HQL에서 left join fetch를 사용하여, 특정 트랜잭션에 대해 그것을 오버라이드 시킨다. 이것은 outer join을 사용하여 첫 번째 select에서 초기에 그 연관을 eagerly 페치시킬 것을 Hibernate에게 알려준다. Criteria query API에서, 우리는 setFetchMode(FetchMode.JOIN)을 사용한다.
만일 당신이 get() 또는 load()에 의해 사용된 페칭 방도를 변경시킬 수 있기를 당신이 원한다고 느낄 경우, 단순하게 Criteria 질의를 사용하라. 예를 들면:
User user = (User) session.createCriteria(User.class) .setFetchMode("permissions", FetchMode.JOIN) .add( Restrictions.idEq(userId) ) .uniqueResult();
(이것은 몇몇 ORM 솔루션들이 "페치 계획"이라고 부르는 것에 대한 Hibernate의 등가물이다.)
N+1 개의 select들을 가진 문제점들을 피하는 완전히 다른 방법은 second-level 캐시를 사용하는 것이다.
콜렉션들에 대한 Lazy 페칭은 영속 콜렉션들에 대한 Hibernate 자신의 구현을 사용하여 구현된다. 하지만 다른 메커니즘은 single-ended 연관들에서 lazy 특징에 필요하다. 연관의 대상 엔티티는 프락시 되어야 한다. Hibernate는 (훌륭한 CGLIB 라이브러리를 통해) 런타임 바이트코드 증진을 사용하여 영속 객체들에 대한 lazy 초기화 프락시들을 구현한다.
디폴트로, Hibernate3는 모든 영속 클래스들에 대해 (시작 시에) 프락시들을 생성시키고 many-to-one 연관과 one-to-one 연관에 대해 lazy 페칭을 이용 가능하게 하는데 그것들을 사용한다.
매핑 파일은 그 클래스에 대한 프락시 인터페이스로서 사용할, proxy 속성을 가진, 인터페이스를 선언할 수도 있다. 디폴트로 Hibernate는 그 클래스의 서브클래스를 사용한다. 프락시된 클래스는 최소한의 패키지 가시성 (visibility)을 가진 디폴트 생성자를 구현해야 함을 노트하라. 우리는 모든 영속 클래스들에 대해 이 생성자를 권장한다!
다형성 클래스들에 대해 이 접근법을 확장할 때 의식해야 하는 몇몇 난처함들이 존재한다. 예를 들면.
<class name="Cat" proxy="Cat"> ...... <subclass name="DomesticCat"> ..... </subclass> </class>
첫 번째로, 심지어 기본 인스턴스가 DomesticCat의 인스턴스인 경우조차도, Cat의 인스턴스들은 결코 DomesticCat으로 타입캐스트가 가능하지 않을 것이다:
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a proxy (does not hit the db) if ( cat.isDomesticCat() ) { // hit the db to initialize the proxy DomesticCat dc = (DomesticCat) cat; // Error! .... }
두번째로, 프락시 ==를 파기할 가능성이 있다.
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a Cat proxy DomesticCat dc = (DomesticCat) session.load(DomesticCat.class, id); // acquire new DomesticCat proxy! System.out.println(cat==dc); // false
하지만, 그 경우는 보이는 만큼 그렇게 나쁘지는 않다. 심지어 우리가 이제 다른 프락시 객체들에 대한 두 개의 참조를 가질지라도, 기본 인스턴스는 여전히 동일한 객체들일 것이다:
cat.setWeight(11.0); // hit the db to initialize the proxy System.out.println( dc.getWeight() ); // 11.0
세번째로, 당신은 final 클래스 또는 임의의 final 메소드들을 가진 클래스에 대해 CGLIB 프락시를 사용하지 않을 수 있다.
마지막으로, 만일 당신의 영속 객체가 초기화 시에 어떤 리소스들을 필요로 할 경우(예를 들어, initializer들 또는 디폴트 생성자 내에서), 그때 그들 리소스들이 또한 프락시에 의해 획득될 것이다. 프락시 클래스는 영속 클래스에 대한 실제 서브클래스이다.
이들 문제점들은 모두 자바의 단일 상속 모형의 기본적인 제약 때문이다. 만일 당신이 이들 문제점들을 피하고자 원할 경우 당신의 영속 클래스들은 각각 그것의 비지니스 메소드들을 선언하는 인터페이스를 구현해야 한다. 당신은 매핑 파일 속에 이들 인터페이스들을 지정해야 한다. 예를 들면.
<class name="CatImpl" proxy="Cat"> ...... <subclass name="DomesticCatImpl" proxy="DomesticCat"> ..... </subclass> </class>
여기서 CatImpl은 Cat 인터페이스를 구현하고 DomesticCatImpl은 DomesticCat 인터페이스를 구현한다. 그때 Cat과 DomesticCat의 인스턴스들에 대한 프락시들은 load() 또는 iterate()에 의해 반환될 수 있다. (list()가 대개 프락시들을 반환하지 않음을 노트하라.)
Cat cat = (Cat) session.load(CatImpl.class, catid); Iterator iter = session.iterate("from CatImpl as cat where cat.name='fritz'"); Cat fritz = (Cat) iter.next();
관계들은 또한 lazy 초기화 된다. 이것은 당신이 임의의 프로퍼티들을 CatImpl 타입이 아닌 Cat 타입으로 선언해야 함을 의미한다.
어떤 오퍼레이션들은 프락시 초기화를 필요로 하지 않는다
equals(), 만일 영속 클래스가 equals()를 오버라이드 시키지 않는 경우
hashCode(), 만일 영속 클래스가hashCode()를 오버라이드 시키지 않는 경우
식별자 getter 메소드
Hibernate는 equals() 또는 hashCode()를 오버라이드 시키는 영속 클래스들을 검출할 것이다.
디폴트 lazy="proxy" 대신에 lazy="no-proxy"를 선택하여, 우리는 타입캐스팅과 연관된 문제점들을 피할 수 있다. 하지만 우리는 빌드 시 바이트코드 수단을 필요로 할 것이고, 모든 연산들은 즉각적인 프락시 초기화로 귀결될 것이다.
만일 초기화 되지 않은 콜렉션이나 프락시가 Session 영역의 외부에서 접근될 경우에, 예를 들어 콜렉션을 소유하거나 프락시에 대한 참조를 가진 엔티티가 detached 상태에 있을 때, LazyInitializationException이 Hibernate에 의해 던져질 것이다.
때때로 우리는Session을 닫기 전에 프락시 또는 콜렉션이 초기화 됨을 확실히 할 필요가 있다. 물론 우리는 예를 들어 cat.getSex() 또는 cat.getKittens().size()를 호출하여 항상 초기화를 강제시킬 수 있다. 그러나 그것은 코드의 독자들에게는 혼동스럽고 일반적인 코드로 편의적이지 않다.
static 메소드들 Hibernate.initialize()와 Hibernate.isInitialized()는 lazy 초기화 된 콜렉션들이나 프락시들에 대해 작업하는 편리한 방법을 어플리케이션에 제공한다. Hibernate.initialize(cat)은 그것의 Session이 여전히 열려져 있는 한 프락시 cat의 초기화를 강제할 것이다. Hibernate.initialize( cat.getKittens())는 kittens의 콜렉션에 대해 유사한 효과를 갖는다.
또 다른 옵션은 모든 필요한 콜렉션들과 프락시들이 로드되기 전까지 Session을 열린 채로 유지하는 것이다. 몇몇 어플리케이션 아키텍처들, 특히 Hibernate를 사용하여 데이터에 접근하는 코드, 그리고 다른 어플리케이션 계층들이나 다른 물리적 프로세스들 내에서 그것을 사용하는 코드에서, 그것은 콜렉션이 초기화 될 때 Session이 열려져 있음을 확실히 하는 문제일 수 있다. 이 쟁점을 다루는 두 가지 기본 방법들이 존재한다:
웹 기반 어플리케이션에서, 서블릿 필터는 뷰 렌더링이 완료되는, 사용자 요청의 바로 끝에서만 Session을 닫는데 사용될 수 있다(Open Session in View 패턴). 물론 이것은 당신의 어플리케이션 인프라스트럭처의 예외상황 처리의 정정에 관한 무거운 요구를 부과한다. 뷰 렌더링 동안에 하나의 예외상황이 발생할때에도 사용자에게 반환되기 전에 Session이 닫혀지고 트랜잭션이 종료되는 것은 지극히 중요하다. 이 "Open Session in View" 패턴에 관한 예제들은 Hibernate 위키를 보라.
별도의 비지니스 티어를 가진 어플리케이션에서, 비지니스 로직은 반환 전에 웹 티어에 필요한 모든 콜렉션들을 "준비"해야 한다. 이것은 비지니스 티어가 모든 데이터를 로드시키고 이미 초기화된 모든 데이터를 특정 쓰임새에 필요한 프리젠테이션/웹 티어로 반환해야 함을 의미한다. 대개 어플리케이션은 웹 티어에 필요하게 될 각각의 콜렉션에 대해 Hibernate.initialize()를 호출하거나(이 호출은 세션이 닫히기 전에 발생해야 한다) 또는 FETCH 절을 갖거나 또는 Criteria 내에 FetchMode.JOIN을 가진 Hibernate 질의를 사용하여 콜렉션을 열심히 검색한다. 이것은 대개 당신이 Session Facade 대신 Command 패턴을 채택할 경우에 더 쉽다.
당신은 또한 초기화 되지 않은 콜렉션들(또는 다른 프락시들)에 접근하기 전에 merge() 또는 lock()으로 앞서 로드된 객체를 새로운 Sessionn에 첨부할 수도 있다. 아니다. Hibernate는 이것을 자동적으로 행하지 않고, 확실히 자동적으로 행하지 않을 것이다. 왜냐하면 그것은 특별한 목적을 위한 트랜잭션 의미를 도입할 것이기 때문이다!
때때로 당신은 거대한 콜렉션을 초기화 시키는 것을 원하지 않지만, 여전히 (그것의 사이즈와 같은) 그것에 대한 어떤 정보 또는 데이터의 부분집합을 필요로 한다.
당신은 그것을 초기화 시키지 않고서 콜렉션의 사이즈를 얻는데 콜렉션 필터를 사용할 수 있다:
( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()
createFilter() 메소드는 또한 전체 콜렉션을 초기화 시킬 필요 없이 콜렉션의 부분집합들을 효율적으로 검색하는데 사용된다:
s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();
Hibernate는 배치 페칭을 효율적으로 사용할 수 있다. 즉 하나의 프락시가 액세스 될 경우에 Hibernate는 몇몇 초기화 되지 않은 프락시들을 로드시킬 수 있다(또는 콜렉션들). batch 페칭은 lazy select 페칭 방도에 대한 최적화이다. 당신이 batch 페칭을 튜닝시킬 수 있는 두 가지 방법들이 존재한다: 클래스 레벨에서 그리고 콜렉션 레벨에서.
클래스들/엔티티들에 대한 batch 페칭은 이해하기가 더 쉽다. 당신이 실행 시에 다음 상황에 처한다고 상상하라: 당신은 하나의 Session 속에 로드된 25개의 Cat 인스턴스들을 갖고 있고, 각각의 Cat은 그것의 소유자 즉, Person에 대한 참조를 갖고 있다. Person 클래스는 프락시 lazy="true"로서 매핑된다. 만일 당신이 이제 모든 cat들을 통해 반복하고 각각의 cat에 대해 getOwner()를 호출할 경우, Hibernate는 프락시된 소유자들을 검색하기 위해 25개의 SELECT 문장들을 디폴트로 실행시킬 것이다. 당신은 Person 매핑에서 batch-size를 지정함으로써 이 동작을 튜닝시킬 수 있다:
<class name="Person" batch-size="10">...</class>
Hibernate는 이제 세 개의 질의들 만을 실행시킬 것이고, 그 패턴은 10,10, 5 이다.
당신은 또한 콜렉션들에 대해 batch 페칭을 이용 가능하게 할 수도 있다. 예를 들어, 만일 각각의 Person이 Cat들을 가진 lazy 콜렉션을 갖고, 10개의 person들이 Sesssion 내에 현재 로드되어 있을 경우, 모든 person들에 대한 반복은 10개의 SELECT들을 생성시킬 것이고, getCats()에 대한 매번의 호출에 대해 하나의 SELECT를 생성시킬 것이다. 만일 당신이 Person 매핑에서 cats 콜렉션에 대해 batch 페칭을 사용가능하게 할 경우, Hibernate는 콜렉션들을 미리-페치 시킬 수 있다:
<class name="Person"> <set name="cats" batch-size="3"> ... </set> </class>
batch-size 8로서, Hibernate는 4개의 SELECT들에서 3, 3, 3, 1 개의 콜렉션들을 로드시킬 것이다. 다시 그 속성의 값은 특정 Session 내에서 초기화 되지 않은 콜렉션들의 예상되는 개수에 의존한다.
만일 당신이 항목들의 포개진 트리를 가질 경우, 예를 들어 전형적인 bill-of-materials 패턴인 경우, (비록 내포된 set 또는 실체화된 경로(materialized path)가 주로-읽기-트리들에 대해 더 좋은 옵션일 수 있을지라도) 콜렉션들에 대한 batch 페칭이 특히 유용하다.
만일 한 개의 lazy 콜렉션이나 단일 값 프락시가 페치되어야 한다면, Hibernate는 하나의 subselect 내에서 원래의 질의를 다시 실행하여 그것들 모두를 로드시킨다. 이것은 조각난 로딩 없이 batch 페칭과 동일한 방식으로 동작한다.
Hibernate3은 개별적인 프로퍼티들에 대한 lazy 페칭을 지원한다. 이 최적화 기술은 또한 fetch groups 으로 알려져 있다. 이것이 대개 마케팅 특징임을 노트하길 바란다. 왜냐하면 실제로 행 읽기를 최적화 시키는 것이 컬럼 읽기에 대한 최적화 보다 훨씬 더 중요하기 때문이다. 하지만 리거시 테이블들이 수백 개의 컬럼들을 갖고 데이터 모형이 개선될 수 없을 때, 오직 클래스의 몇몇 프로퍼티들을 로드시키는 것 만이 유용할 수도 있다.
lazy 프로퍼티 로딩을 이용가능하게 하려면, 당신의 특정 property 매핑들에 대해 lazy 속성을 설정하라:
<class name="Document"> <id name="id"> <generator class="native"/> </id> <property name="name" not-null="true" length="50"/> <property name="summary" not-null="true" length="200" lazy="true"/> <property name="text" not-null="true" length="2000" lazy="true"/> </class>
Lazy property 로딩은 빌드 시 바이트코드 수단을 필요로 한다! 만일 당신의 영속 클래스들이 개선되지 않을 경우, Hibernate는 조용하게 lazy 프로퍼티 설정들을 무시하고 즉각적인 페칭으로 후퇴할 것이다.
bytecode 수단으로, 다음 Ant 태스크를 사용하라:
<target name="instrument" depends="compile"> <taskdef name="instrument" classname="org.hibernate.tool.instrument.InstrumentTask"> <classpath path="${jar.path}"/> <classpath path="${classes.dir}"/> <classpath refid="lib.class.path"/> </taskdef> <instrument verbose="true"> <fileset dir="${testclasses.dir}/org/hibernate/auction/model"> <include name="*.class"/> </fileset> </instrument> </target>
불필요한 컬럼 읽기를 피하는 다른 (더 좋은?) 방법은 적어도 읽기 전용 트랜잭션의 경우에 HQL 질의 또는 Criteria 질의의 투사(projection) 특징들을 사용하는 것이다. 이것은 빌드 시 바이트코드 처리에 대한 필요성을 피하게 해주고 확실히 선호되는 해결책이다.
당신은 HQL에서 fetch all properties를 사용하여 프로퍼티들에 대한 통상의 eager 페칭을 강제시킬 수 있다.
Hibernate Session은 영속 데이터에 대한 트랜잭션 레벨 캐시이다. class-by-class와 collection-by-collection 기반 위에 클러스터 또는 JVM-레벨(SessionFactory-레벨) 캐시를 구성하는 것이 가능하다. 당신은 클러스터링 된 캐시 속에 플러그인 할 수도 있다. 주의하라. 캐시들은 (비록 그것들이 캐시된 데이터를 정기적으로 만료되도록 구성되어 있을지라도) 또 다른 어플리케이션에 의해 영속 저장소에 대해 행해진 변경들을 결코 알지 못한다.
디폴트로, Hibernate는 JVM-레벨의 캐싱에 EHCache를 사용한다. (JCS 지원은 이제 진부하게 되었고 Hibernate의 장래 버전에서 제거될 것이다.) 당신은 hibernate.cache.provider_class 프로퍼티를 사용하여 org.hibernate.cache.CacheProvider를 구현하는 클래스의 이름을 지정함으로써 다른 구현을 선택할 수도 있다. You have the option to tell Hibernate which caching implementation to use by specifying the name of a class that implements org.hibernate.cache.CacheProvider using the property hibernate.cache.provider_class. Hibernate comes bundled with a number of built-in integrations with open-source cache providers (listed below); additionally, you could implement your own and plug it in as outlined above. Note that versions prior to 3.2 defaulted to use EhCache as the default cache provider; that is no longer the case as of 3.2. 당신은 hibernate.cache.provider_class 프로퍼티를 사용하여 org.hibernate.cache.CacheProvider를 구현하는 클래스의 이름을 지정함으로써 어느 캐싱 구현을 사용할 것인지를 Hibernate에게 알려주는 옵션을 갖는다. Hibernate는 (아래에 열거된) 오픈-소스 프로바이더들을 가진 많은 빌드되어 있는 통합들을 번들로 갖고 있다; 추가적으로 당신은 위에서 언급했듯이 그것에 당신 자신의 것을 구현할 수 있고 그것에 플러그 시킬 수 있다. 3.2 이번 버전들은 디플트 캐시 프로바이더로서 EhCache를 사용하도록 디포릍로 내장되어 있음을 노트하라; 버전 3.2의 경우에 그것은 더이상 디폴트 내장이 아니다.
표 19.1. 캐시 프로바이더들
캐시 | 프로바이더 클래스 | 타입 | 클러스터 안전 | 질의 캐시 지원 |
---|---|---|---|---|
Hashtable (제품 용도로 고안되어 있지 않음) | org.hibernate.cache.HashtableCacheProvider | memory | yes | |
EHCache | org.hibernate.cache.EhCacheProvider | memory, disk | yes | |
OSCache | org.hibernate.cache.OSCacheProvider | memory, disk | yes | |
SwarmCache | org.hibernate.cache.SwarmCacheProvider | clustered (ip multicast) | yes (clustered invalidation) | |
JBoss TreeCache | org.hibernate.cache.TreeCacheProvider | clustered (ip multicast), transactional | yes (replication) | yes (clock sync req.) |
클래스 또는 콜렉션 매핑의 <cache> 요소는 다음 형식을 갖는다:
<cache usage="transactional|read-write|nonstrict-read-write|read-only" (1) region="RegionName" (2) include="all|non-lazy" (3) />
(1) | usage(필수) 캐싱 방도를 지정한다: transactional, read-write, nonstrict-read-write 또는 read-only |
(2) | region (옵션, 디폴트는 class 또는 콜렉션 role 이름) second level 캐시 영역의 이름을 지정한다 |
(3) | include (옵션, 디폴트는 all) non-lazy는 lazy="true"로 매핑된 엔티티의 프로퍼티들을 지정하며 속성-레벨 lazy 페칭이 이용 가능할 때 키시될 수 없다 |
다른 방법으로 (선호적으로?), 당신은 hibernate.cfg.xml 내에 <class-cache>와 <collection-cache> 요소들을 지정할 수도 있다.
usage 속성은 캐시 동시성 방도를 지정한다.
당신의 어플리케이션이 영속 클래스의 인스턴스들을 읽어들일 필요가 있지만 결코 변경할 필요가 없을 경우에 read-only 캐시가 사용될 수 있다. 이것은 가장 간단한 최상의 퍼포먼스를 위한 방도이다. 그것은 클러스터 내 사용에는 완벽하게 안전하다.
<class name="eg.Immutable" mutable="false"> <cache usage="read-only"/> .... </class>
어플리케이션이 데이터를 업데이트 할 필요가 있을 경우, read-write 캐시가 적절하다. 만일 직렬화 가능한(serializable) 트랜잭션 격리 레벨이 필요한 경우에는 이 캐시 방도가 결코 사용되지 말아야 한다. 만일 캐시가 JTA 환경에서 사용될 경우, 당신은 JTA TransactionManager를 얻는 방도를 명명하는 hibernate.transaction.manager_lookup_class 프로퍼티를 지정해야 한다. 다른 환경들에서, 당신은Session.close() 또는 Session.disconnect()가 호출될 때 트랜잭션이 완료되는 것을 확실히 해야 한다. 만일 당신이 클러스터 내에 이 방도를 사용하고자 원할 경우, 당신은 기본 캐시 구현이 잠금을 지원하도록 하는 것을 확실히 해야 한다. 미리 만들어진 캐시 프로바이더들은 그렇게 행하지 않는다.
<class name="eg.Cat" .... > <cache usage="read-write"/> .... <set name="kittens" ... > <cache usage="read-write"/> .... </set> </class>
만일 어플리케이션이 오직 데이터를 자주 업데이트할 필요가 있고(예를 들어, 만일 두 개의 트랜잭션들이 동시에 동일한 항목을 업데이트 하려고 시도하는 정말 있음직하지 않은 경우) 그리고 엄격한 트랜잭션 격리가 필요하지 않은 경우, nonstrict-read-write 캐시가 적절할 수 있다. 만일 그 캐시가 JTA 환경에서 사용될 경우, 당신은 hibernate.transaction.manager_lookup_class를 지정해야 한다. 다른 환경들에서, 당신은 Session.close() 또는 Session.disconnect()가 호출될 때 트랜잭션이 완료되도록 확실히 해야 한다.
transactional 캐시 방도는 JBoss TreeCache와 같은 전체 트랜잭션적인 캐시 프로바이더들에 대한 지원을 제공한다. 그런 캐시는 오직 JTA 환경 내에서 사용될 수 있고 당신은 hibernate.transaction.manager_lookup_class를 지정해야 한다.
캐시 프로바이더들 중 어느 것도 모든 캐시 동시성 방도들을 지원하지 않는다. 다음 테이블은 어느 프로바이더들이 어느 동시성 방도들과 호환되는지를 보여준다.
당신이 객체를 save(), update() 또는 saveOrUpdate()에 전달할 때마다 그리고 당신이 load(), get(), list(), iterate() 또는 scroll()을 사용하여 객체를 검색할 때마다, 그 객체는 Session의 내부 캐시에 추가된다.
flush()가 차후적으로 호출될 때, 그 객체의 상태는 데이터베이스와 동기화 될 것이다. 만일 당신이 이 동기화가 발생되는 것을 원하지 않거나 만일 당신이 대량의 객체들을 처리 중이고 메모리를 효율적으로 관리할 필요가 있을 경우, evict() 메소드는 first-level 캐시로부터 그 객체와 그것의 콜렉션들을 제거하는데 사용될 수 있다.
ScrollableResult cats = sess.createQuery("from Cat as cat").scroll(); //a huge result set while ( cats.next() ) { Cat cat = (Cat) cats.get(0); doSomethingWithACat(cat); sess.evict(cat); }
Session은 또한 인스턴스가 세션 캐시에 속하는지 여부를 결정하는데 contains() 메소드를 제공한다.
세션 캐시로부터 모든 객체들을 완전하게 퇴거시키기 위해, Session.clear()를 호출하라.
second-level 캐시의 경우, 하나의 인스턴스, 전체 클래스, 콜렉션 인스턴스 또는 전체 콜렉션 role의 캐시된 상태를 퇴거시키는 SessionFactory 상에 정의된 메소드들이 존재한다.
sessionFactory.evict(Cat.class, catId); //evict a particular Cat sessionFactory.evict(Cat.class); //evict all Cats sessionFactory.evictCollection("Cat.kittens", catId); //evict a particular collection of kittens sessionFactory.evictCollection("Cat.kittens"); //evict all kitten collections
CacheMode는 특정 세션이 second-level 캐시와 어떻게 상호작용하는지를 제어한다
CacheMode.NORMAL - second-level 캐시로부터 아이템들을 읽어들이고 second-level 캐시로 아이템들을 기록한다
CacheMode.GET - second-level 캐시로부터 아이템들을 읽어들이지만, 데이터를 업데이트할 때를 제외하면 second-level 캐시로 기록하지 않는다
CacheMode.PUT - 아이템들을 second-level 캐시에 기록하지만, second-level 캐시로부터 읽어들이지 않는다
CacheMode.REFRESH - 아이템들을 second-level 캐시로기록하지만, second-level 캐시로부터 읽어들이지 않고, 데이터베이스로부터 읽어들인 모든 아이템들에 대한 second-level 캐시의 갱신을 강제시켜, hibernate.cache.use_minimal_puts의 효과를 무시한다
second-level 캐시 또는 질의 캐시 영역의 내용물을 브라우징하려면 Statistics API를 사용하라:
Map cacheEntries = sessionFactory.getStatistics() .getSecondLevelCacheStatistics(regionName) .getEntries();
당신은 통계를 이용 가능하게 하고, 선택적으로 Hibernate로 하여금 캐시 엔트리들을 보다 인간에게 이해가능한 형식으로 유지시키도록 강제시키는 것이 필요할 것이다:
hibernate.generate_statistics true hibernate.cache.use_structured_entries true
질의 결과 셋들이 또한 캐시될 수도 있다. 이것은 동일한 파라미터들을 가지고 자주 실행되는 질의들에만 유용하다. 질의 캐시를 사용하기 위해 당신은 먼저 그것을 이용 가능하도록 해야 한다:
hibernate.cache.use_query_cache true
이 설정은 두 개의 새로운 캐시 영역들 - 캐시된 질의 결과 셋들을 보관하는 것 (org.hibernate.cache.StandardQueryCache), 질의 가능한 테이블들에 대한 가장 최신 업데이트들에 대한 timestamp들을 보관하는 다른 것 (org.hibernate.cache.UpdateTimestampsCache)-의 생성을 강제한다 . 질의 캐시는 결과 셋 내에 실제 엔티티들의 상태를 캐시시키지 않음을 노트하라; 그것은 오직 식별자 값들과 값 타입의 결과들 만을 캐시시킨다. 따라서 질의 캐시는 항상 second-level 캐시와 함께 사용되어야 한다.
대부분의 질의들은 캐싱으로부터 이점이 없기에, 디폴트로 질의들은 캐시되지 않는다. 캐싱을 이용 가능하도록 하려면, Query.setCacheable(true)를 호출하라. 이 호출은 기존 캐시 결과들을 찾는 것을 질의에게 허용해주거나 질의가 실행될 때 그것의 결과들을 캐시에 추가하는 것을 허용해준다.
만일 당신이 질의 캐시 만료 정책들에 대한 세밀한 제어를 필요로 할 경우, 당신은 Query.setCacheRegion()을 호출함으로써 특별한 질의에 대해 명명되니 캐시 영역을 지정할 수도 있다.
List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger") .setEntity("blogger", blogger) .setMaxResults(15) .setCacheable(true) .setCacheRegion("frontpages") .list();
만일 질의가 그것의 질의 캐시 영역의 갱신을 강제시켜야 하는 경우에, 당신은 Query.setCacheMode(CacheMode.REFRESH)를 호출해야 한다. 이것은 기본 데이터가 별도의 프로세스를 통해 업데이트되었고(예를 들면, Hibernate를 통해 변경되지 않았고) 특정 질의 결과 셋들을 선택적으로 갱신하는 것을 어플리케이션에게 허용해주는 경우들에서 특별히 유용하다. 이것은 SessionFactory.evictQueries()를 통해 질의 캐시 영역을 퇴거시키는 보다 효과적인 대안이다.
우리는이미 콜렉션들에 관해 얘기하는데 꽤 많은 시간을 소요했다. 이 절에서 우리는 콜렉션들이 실행 시에 어떻게 행위하는지에 관한 한 쌍의 쟁점들을 조명할 것이다.
Hibernate는 세 가지 기본적인 종류의 콜렉션들을 정의한다:
값들을 가진 콜렉션들
one to many 연관들
many to many 연관들
이 분류는 여러 가지 테이블과 foreign key 관계들을 구별짓지만 우리가 관계형 모형에 대해 알 필요가 있는 모든 것을 우리에게 말해주지 않는다. 관계형 구조와 퍼포먼스 특징들을 완전하게 이해하기 위해, 우리는 또한 콜렉션 행들을 업데이트하거나 삭제하기 위해 Hibernate에 의해 사용되는 프라이머리 키의 구조를 검토해야 한다. 이것은 다음 분류를 제안한다:
인덱싱 된 콜렉션들
set들
bag들
모든 인덱싱된 콜렉션들(map들, list들, array들)은 <key>와 <index> 컬럼들로 이루어진 프라이머리 키를 갖는다. 이 경우에 콜렉션 업데이트들은 대개 극히 효율적이다 - Hibernate가 그것을 업데이트나 삭제를 시도할 때 프라이머리 키는 효율적으로 인덱싱될 수 있고 특정 행은 효율적으로 위치지워질 수 있다.
Set들은 <key>와 요소 컬럼들로 구성된 프라이머리 키를 갖는다. 이것은 몇몇 유형의 콜렉션 요소, 특히 composite 요소들 또는 대형 텍스트 또는 바이너리 필드들에 대해 덜 효율적일 수 있다; 데이터베이스는 복잡한 프라이머리 키를 효율적으로 인덱싱하는 것이 불가능할 수도 있다. 반면에 one to many 또는 many to many 연관들의 경우, 특히 합성 식별자들의 경우에는 효율적일 수 있을 것 같다.(부수-노트: 만일 당신이 당신을 위한 <set>의 프라이머리 키를 실제로 생성시키기 위해 SchemaExport를 원한다면 당신은 모든 컬럼들을 not-null="true"로 선언해야 한다.)
<idbag> 매핑들은 대용 키를 정의하여서, 그것들은 항상 업데이트에 매우 효율적이다. 사실, 그것들은 최상의 경우이다.
Bag들은 가장 나쁜 경우이다. 왜냐하면 하나의 bag은 중복 요소 값들을 허용하고 인덱스 컬럼을 갖지 않기 때문에, 프라이머리 키가 정의될 수 없다. Hibernate는 중복 행들 사이를 구분 짓는 방법을 갖고 있지 않다. Hibernate는 그것이 변경될 때마다 (한 개의 DELETE로) 콜렉션을 완전하게 제거하고 다시 생성시킴으로써 이 문제를 해결한다. 이것은 매우 비효율적이다.
one-to-many 연관의 경우, "프라이머리 키"는 데이터베이스 테이블의 물리적인 프라이머리 키가 아닐 수도 있지만- 이 경우에서도 위의 분류는 여전히 유용하다. (그것은 여전히 Hibernate가 콜렉션의 개별 행들을 어떻게 "위치지우는"지를 반영한다.)
위의 논의에서, 인덱싱된 콜렉션들과 (대개) set들이 요소들을 추가하고, 제거하고 업데이트함에 있어 가장 효율적인 오퍼레이션을 허용해준다.
아마 인덱싱 된 콜렉션들이 many to many 연관들을 위한 또는 값들을 가진 콜렉션들을 위한 set들에 대해 갖고 있는 하나 이상의 장점들이 존재한다. Set의 구조 때문에, Hibernate는 요소가 "변경"될 때 행을 UPDATE 하지 않는다. Set에 대한 변경들은 항상 (개별 행들에 대한) INSERT와 DELETE를 통해 동작한다. 다시 이 검토는 one to many 연관들에 적용되지 않는다.
배열들이 lazy 될 수 없음을 관찰 한 후에, 우리는 list들, map들, 그리고 idbag들이 단독이 아닌 set들을 가진 가장 퍼포먼스가 좋은(non-inverse) 콜렉션 타입들임을 결론 지을 것이다. Set들은 Hibernate 어플리케이션들에서 가장 공통된 종류의 콜렉션이 될 것이라 예상된다. 이것은 "set" 의미가 관계형 모형에서 가장 고유한 것이기 때문이다.
하지만, 잘 설계된 Hibernate 도메인 모형들에서, 우리는 대개 대부분의 콜렉션들이 사실 inverse="true"를 가진 one-to-many 연관들임을 보게 된다. 이들 연관들의 경우, 업데이트는 연관의 many-to-one 엔드에 의해 처리되고, 따라서 콜렉션 업데이트 퍼포먼스에 대한 검토들은 단순히 적용되지 않는다.
단지 당신이 영원히 bag들을 버리기 전에, bag들(과 또한 list들)이 set들보다 훨씬 더 성능이 좋은 특별한 경우들이 존재한다. inverse="true"를 가진 콜렉션들(예를 들어, 표준 양방향 one-to-many 관계 특질)의 경우, 우리는 bag 요소들을 초기화(페치) 시킬 필요 없이 bag 또는 list에 요소들을 추가시킬 수 있다! 이것은 Collection.add() 또는 Collection.addAll()이 (Set과는 달리) 항상 bag 또는 List에 대해 true를 반환해야하기 때문이다. 이것은 훨씬 다음 공통적인 코드를 더 빠르게 만들 수 있다.
Parent p = (Parent) sess.load(Parent.class, id); Child c = new Child(); c.setParent(p); p.getChildren().add(c); //no need to fetch the collection! sess.flush();
종종 콜렉션 요소들을 하나씩 삭제하는 것은 극히 비효율적일 수 있다! Hibernate는 완전하게 바보가 아니어서, 그것은 새로운 공백의 콜렉션의 경우(예를 들어 당신이 list.clear()를 호출했을 경우)에 그것을 행하지 않을 것임을 알고 있다. 이 경우에, Hibernate는 하나의 DELETE 명령을 내릴 것이고 우리는 모두 행했다!
우리가 길이 20인 하나의 콜렉션에 한 개의 요소를 추가하고 그런 다음 두 개의 요소들을 제거한다고 가정하자. Hibernate는 (콜렉션이 bag가 아닌 한) 한 개의 INSERT 문장과 두 개의 DELETE 문장을 명령 내릴 것이다. 이것은 확실히 마음에 든다.
하지만, 우리가 두 개의 요소들을 남겨둔채 18 개의 요소들을 제거하고 나서 세 개의 새로운 요소들을 추가한다고 가정하자. 두 가지 가능한 처리 방법들이 존재한다.
하나씩 열 여덟 개의 행들을 삭제한 다음에 세 개의 행들을 삽입시킨다
(한 개의 SQL DELETE로)전체 콜렉션을 삭제하고 모든 다섯개의 현재 요소들을 (하나씩) insert 시킨다
Hibernate는 두 번째 옵션이 아마 이 경우에 더 빠르다는 점을 알 만큼 충분히 영리하지 않다.(그리고 Hibernate가 그렇게 영리해지는 것을 희망 하는 것은 가능하지 않을 것이다; 그런 특징은 데이터베이스 트리거들 등을 혼동스럽게 할 수도 있다.)
다행히, 당신은 원래의 콜렉션을 폐기시키고(예를 들어 참조 해제하고) 모든 현재 요소들을 가진 새로이 초기화된 콜렉션을 반환함으로써 아무때든지 이 특징을 강제시킬 수 있다. 이것은 시간이 흐름에 따라 매우 유용하고 강력해질 수 있다.
물론 단 한번의 삭제(one-shot-delete)는 inverse="true"로 매핑된 콜렉션들에 적용되지 않는다.
최적화는 퍼포먼스 관련 숫자들에 대한 모니터링과 접근 없이는 많이 사용되지 않는다. Hibernate는 그것의 내부적인 오퍼레이션들에 대한 전체 영역의 특징들을 제공한다. Hibernate에서 Statistics는 SessionFactory에 대해 이용 가능하다.
당신은 두 가지 방법들로 SessionFactory metrics에 접근할 수 있다. 당신의 첫 번째 옵션은 sessionFactory.getStatistics()를 호출하고 당신 스스로 Statistics를 읽거나 디스플레이 하는 것이다.
만일 당신이 StatisticsService MBean을 이용 가능하도록 할 경우 Hibernate는 또한 metrics를 발표하는데 JMX를 사용할 수 있다. 당신은 모든 당신의SessionFactory에 대해 한 개의 MBean 또는 팩토리 당 한 개를 이용 가능하게 할 수 있다. 최소한의 구성 예제들은 다음 코드를 보라:
// MBean service registration for a specific SessionFactory Hashtable tb = new Hashtable(); tb.put("type", "statistics"); tb.put("sessionFactory", "myFinancialApp"); ObjectName on = new ObjectName("hibernate", tb); // MBean object name StatisticsService stats = new StatisticsService(); // MBean implementation stats.setSessionFactory(sessionFactory); // Bind the stats to a SessionFactory server.registerMBean(stats, on); // Register the Mbean on the server
// MBean service registration for all SessionFactory's Hashtable tb = new Hashtable(); tb.put("type", "statistics"); tb.put("sessionFactory", "all"); ObjectName on = new ObjectName("hibernate", tb); // MBean object name StatisticsService stats = new StatisticsService(); // MBean implementation server.registerMBean(stats, on); // Register the MBean on the server
TODO: 이것은 의미가 없다: 첫번째 경우에, 우리는 직접 MBean을 검색하고 사용한다. 두 번째 경우에 우리는 JNDI 이름을 사용하기 전에 세션 팩토리가 보관하고 있는 JNDI 이름을 부여해야 한다. hibernateStatsBean.setSessionFactoryJNDIName("my/JNDI/Name")을 사용하라.
당신은 SessionFactory에 대한 모니터링을 (비)활성화 시킬 수 있다
구성 시 : hibernate.generate_statistics, 디폴트는 false
실행 시 : sf.getStatistics().setStatisticsEnabled(true) 또는 hibernateStatsBean.setStatisticsEnabled(true)
Statistics(통계량들)은 clear() 메소드를 사용하여 프로그래밍 방식으로 재설정 될 수 있다. 요약은 logSummary() 메소드를 사용하여 logger(info 레벨)에게 전송될 수 있다.
Hibernate는 매우 기본적인 것에서부터 어떤 시나리오들에만 관련된 전문 정보에 이르는 많은 metrics를 제공한다. 모든 이용 가능한 카운터들은 Statistics interface API에서 3개의 카테고리로 설명되어 있다:
열려진 세션들의 개수, 검색된 JDBC 커넥션들의 개수 등과 같은 일반적인 Session 사용에 관련된 metrics.
전체적으로 엔티티들, 콜렉션들, 질의들, 그리고 캐시들에 관련된 metrics(전역 metrics로 알려져 있음),
특정한 엔티티, 콜렉션, 질의 또는 캐시 영역에 관련된 상세 metrics.
예를 들어 당신은 엔티티, 콜렉션, 질의들의 캐시 성공율 및 실패율, put(역자 주, 캐시 시도, putt) 비율, 콜렉션들과 질의들, 그리고 평균 질의 요구 시간 등을 찾을 수 있다. 수 밀리초들가 자바에서 근사치에 종속됨을 의식하라. Hibernate는 JVM 정밀도에 묶여 있고, 몇몇 플랫폼들에서 이것은 심지어 약 10초가 될 수도 있다.
간단한 getter들은 (예를 들어 특정 엔티티, 콜렉션, 캐시 영역에 묶이지 않은) 전역 metrics에 접근하는데 사용된다. 당신은 그것(특정 엔티티, 콜렉션, 또는 캐시 영역)의 이름을 통해, 그리고 질의들에 대한 그것의 HQL 또는 SQL 표현을 통해 특정 엔티티, 콜렉션, 또는 캐시 영역의 metrics에 접근할수 있다. 추가 정보는 Statistics, EntityStatistics, CollectionStatistics, SecondLevelCacheStatistics, 그리고 QueryStatistics를 참조하라. 다음 코드는 간단한 예제를 보여준다:
Statistics stats = HibernateUtil.sessionFactory.getStatistics(); double queryCacheHitCount = stats.getQueryCacheHitCount(); double queryCacheMissCount = stats.getQueryCacheMissCount(); double queryCacheHitRatio = queryCacheHitCount / (queryCacheHitCount + queryCacheMissCount); log.info("Query Hit ratio:" + queryCacheHitRatio); EntityStatistics entityStats = stats.getEntityStatistics( Cat.class.getName() ); long changes = entityStats.getInsertCount() + entityStats.getUpdateCount() + entityStats.getDeleteCount(); log.info(Cat.class.getName() + " changed " + changes + "times" );
모든 엔티티들, 콜렉션들, 콜렉션들,질의들 그리고 영역 캐시들에 대해 작업하기 위해, 당신은 다음 메소드들로서 엔티티들, 콜렉션들, 질의들, 그리고 영역 캐시들에 대한 이름들의 목록을 검색할 수 있다: getQueries(), getEntityNames(), getCollectionRoleNames(), 그리고 getSecondLevelCacheRegionNames().