[하이버네이트 Criteria] 목록 사이즈 구하기

난 편의상 이런 메서드들을 만들어 놓고 쓴다.
    private Session getSession() {
        return sessionFactory.getCurrentSession();
    }
    private Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }
이 두개가 있다고 했을 때.. Criteria API를 이용해서 목록 사이즈를 구하는 코드를 작성해보자.
    public int totalSize() {
        return getCriteriaOf(Code.class).list().size();
    }
캬.. 얼마나 명시적인가. 하지만 이러면 안된다. ㅠ.ㅠ.. 난 하이버가 좀 더 똑똑해져서 저렇게 짜더라도 알아서 count 쿼리를 만들어 주면 좋겠지만 그러지 않는다. 전부다 select 해온다. 로그를 보면..
Hibernate: select this_.id as id0_0_, this_.code as code0_0_, this_.descr as descr0_0_, this_.name as name0_0_ from Code this_
이렇다. 필요한건 사이즈 뿐인데 Code 테이블에 있는걸 전부다 가져온 셈이다. 크헉.. 따라서 목록 사이즈를 구할 때는 이런 쿼리를 쓰면 안된다.. 절대로; 그럼 어떻게 할까나..
    public int totalSize() {
        return (Integer)getCriteriaOf(Code.class)
            .setProjection(Projections.count(“id”))
            .uniqueResult();
    }
이렇게 하면된다. Projections를 이용하면 count 말고도 max, min등을 사용할 수 있다. 코드가 좀 길어지고 Projection API에 적응해야 한다는 단점이 있지만 쿼리는 깔끔해진다.
Hibernate: select count(this_.id) as y0_ from Code this_
딱 내가 원하던 쿼리다. 하이버네이트를 쓸땐 이렇게 Criteria나 HQL이 생성해주는 SQL도 일일히 확인하는것이 좋다. 사실 일일히 확인하는 작업을 DBA가 해주면 좋겠지만 하이버네이트랑 친한 DBA가 있을 때의 이야기이고 그렇지 않다면 본인이 해야겠다. 아니면 하이버네이트를 마스터해서 어떤 쿼리가 생성되는지 달달달 꽤고 있다면 시간을 좀 단축 시킬 수 있을 것 같기도 하다.
하이버네이트를 마스터 하기 위해서는 조마간 나올 하이버네이트 번역서가 필수라는….앗..  광고를 하려고 시작한 글은 아니었는데;; ㅋㅋㅋ
ps1: Criteria를 사용해서 좀 더 간결한 코딩으로 가져오는 방법이 있으면 알고 싶다.
ps2: 반드시 Criteria를 써야 한다. 나중에 저 쿼리에 검색 조건이 임의로 추가될텐데 그럴때 Criteria가 빛을 발하기 때문이다. 난 동적쿼리를 SQL이나 HQL로 짜지 못하겠다. 짜증난다.

하이버네이트 Criteria 검색 쿼리 골치

select this_.id as y0_ from Post this_ where this_.board_id=?

select this_.id as id1_0_, this_.author as author1_0_, this_.board_id as board7_1_0_, this_.contents as contents1_0_, this_.created as created1_0_, this_.title as title1_0_, this_.updated as updated1_0_
from Post this_
where this_.id in (?, ?, ?, ?, ?, ?) and  lcase(this_.title) like ? or lcase(this_.contents) like ?

===============================================

select this_.id as id1_0_, this_.author as author1_0_, this_.board_id as board7_1_0_, this_.contents as contents1_0_, this_.created as created1_0_, this_.title as title1_0_, this_.updated as updated1_0_
from Post this_
where this_.board_id=? and (lcase(this_.title) like ? or lcase(this_.contents) like ?)

================================================

select this_.id as id1_0_, this_.author as author1_0_, this_.board_id as board7_1_0_, this_.contents as contents1_0_, this_.created as created1_0_, this_.title as title1_0_, this_.updated as updated1_0_
from Post this_
where this_.id in
(select this0__.id as y0_ from Post this0__ where this0__.board_id=?)
and (lcase(this_.title) like ? or lcase(this_.contents) like ?)

=================================================

셋 다 똑같네.. 아.. 이런 ㅠ.ㅠ OTL… 뭐야 ㅠ.ㅠ 어떻게 짜야되지. 흠…

(lcase(this_.title) like ? or lcase(this_.contents) like ?)

이 부분이 잘못 됐나? title에 ?가 있거나 contents에 ?가 있는 것들 검색하려는데 분명 테스트 데이터에는 두 개가 나와야 되는데 계속 한 개만 나오네. 흠… 모르겠네… 모르겠어..

=====================
id | title       | contents
1  | keesun | toby
2  | toby      | toby
3  | toby      | keesun
=====================

이 상태에서 서브 쿼리나 in 뒤에 담겨있는 id는 전부 1, 2, 3이라고 치고… titke이 keesun이거나 contents가 keesun이 row를 갖다 달라는 거자나.

그럼 1번 row를 보면 title이 keesun이니까 맞자나. title이 keesun 이자나 contents는 아니고 그럼 일단 하나.
그리고 3번은 contents가 keesun이니까 맞고..

그럼 결과가 2개가 되야 하는데

결과는 1번만 가져오는… 이 상황은 ..;;; 어렵네~

하이버네이트 Criteria 다루기 – 중복일까 아닐까

    @Override
    public List<T> search(P params, OrderPage orderPage) {
        // total rowcount
        orderPage.setRowcount((Integer) (addRestrictions(
                getSession().createCriteria(this.persistentClass), params)
                .setProjection(Projections.rowCount()).uniqueResult()));

        // pages list
        Criteria c = addRestrictions(getSession().createCriteria(
                this.persistentClass), params);
        orderPage.applyPage(c);
        orderPage.applyOrder(c);

        return c.list();
    }

    /**
     * template method for search
     *
     * @param c
     * @param params
     * @return
     */
    protected Criteria addRestrictions(Criteria c, P params) {
        return c;
    }

위 코드에서 중복이 보이시나요? 안 보이신 다구요?

    @Override
   public List<T> search(P params, OrderPage orderPage) {
       // total rowcount
       orderPage.setRowcount((Integer) (addRestrictions(

               getSession().createCriteria(this.persistentClass), params)

               .setProjection(Projections.rowCount()).uniqueResult()));

       // pages list
       Criteria c = addRestrictions(getSession().createCriteria(

               this.persistentClass), params)
;
       orderPage.applyPage(c);
       orderPage.applyOrder(c);

       return c.list();
   }

어떤가요. 중복 이죠? 그러나..

Criteria c = addRestrictions(getSession().createCriteria(

               this.persistentClass), params)
;
orderPage.setRowcount((Integer) (c.setProjection(Projections.rowCount()).uniqueResult()));
orderPage.applyPage(c);
orderPage.applyOrder(c);

대강 이런 식으로 리팩터링 해보면 하이버네이트는 요상한 쿼리와 함께 에러를 뱉어냅니다.

전체 Row 갯수를 반환하는 Criteria(쿼리는 select count(*).. )이런식으로 시작)를 다시 Order와 Page 처리를 할 때 사용하면 이상한 쿼리(select count(*).. order by … 이게 이상한 이유는 order by에서 사용한 컬럼이 group by에 있어야 하는데 groupd by를 정의한 적이 없거니와, 사실 두 번째 쿼리는 count(*)가 없어야 하는데 앞에서 만들어둔 Criteria에 이어 붙인 꼴이 되어서 이상해졌습니다.)가 되버립니다.

중복처럼 보이지만 제거하면 코드가 깨지는… 요상한 경우. 이거 어떻게 처리하는게 좋을까요? 전 요리 조리 해보다가 그냥 뒀습니다.

Criteria에서 Join하기

참조 : http://www.hibernate.org/hib_docs/v3/api/org/hibernate/Criteria.html

Member(1) — (*)Messenger 의 관계에서 다음과 같이 데이타가 들어있습니다.

Member m1 — Messenger msg1(“seal”, MSN)
Member m1 — Messenger msg2(“seal2”, Skype)
Member m3 — Messenger msg3(“keesun”, MSN)

이 때 “MSN 메신저가 있는 모든 멤버”를 가져 오려면 join을 해야 합니다. Criteria를 사용해서 Join을 하는 방법은 두 가지가 있습니다.

1. createCriteria() 메소드 사용하기.

    public void testCriteriaJoin1(){
        insertDatas();
        final KMessengerType messengerType = KMessengerType.MSN;

        Criteria c = s.createCriteria(KMember.class)
                      .createCriteria(“messengers”)
                        .add(Restrictions.eq(“m_type“, messengerType));

        assertEquals(2, c.list().size());
    }

2. createAlias() 메소드 사용하기.

    public void testCriteriaJoin2(){
        insertDatas();
        final KMessengerType messengerType = KMessengerType.MSN;

        Criteria c = s.createCriteria(KMember.class)
                      .createAlias(“messengers”, “msg”)
                      .add(Restrictions.eq(“msg.m_type“, messengerType));

        assertEquals(2, c.list().size());
    }

여기까지만 봐서는 둘의 차이가 단순히 alias를 사용해야만 한다는 것 밖에 모르겠습니다.[footnote]’msg.’을 빼고 실행하면 Member에 m_type이란 컬럼이 없다면서  Hibernate.QueryException이 발생합니다.[/footnote]

“MSN 메신저를 가지고 이름이 keesun인 멤버”를 찾으려면 위 주건에 .add(Restriction.eq(“name”, “keesun”)만 추가하면 됩니다. 이것을 맨 끝에 추가 해보면 둘의 차이를 알 수 있습니다.

1. createCriteria() 메소드를 사용.

    public void testCriteriaJoin1(){
        insertDatas();
        final KMessengerType messengerType = KMessengerType.MSN;

        Criteria c = s.createCriteria(KMember.class)
                      .createCriteria(“messengers”)
                          .add(Restrictions.eq(“m_type”, messengerType))
                          .add(Restrictions.eq(“name”, “keesun”));

        assertEquals(1, c.list().size());
    }

=> Messenger에 name이라는 컬럼이 없다면서 HQE[footnote]Hibernate Query Exception[/footnote]가 발생합니다.

2. createAlias() 메소드를 사용.
   

public void testCriteriaJoin2(){
        insertDatas();
        final KMessengerType messengerType = KMessengerType.MSN;

        Criteria c = s.createCriteria(KMember.class)
                      .createAlias(“messengers”, “msg”)
                      .add(Restrictions.eq(“msg.m_type”, messengerType))
                      .add(Restrictions.eq(“name”, “keesun”));

        assertEquals(1, c.list().size());
    }

=> 제대로 동작합니다.

createCriteria(String) 메소드 뒤에 붙은 add() 메소드 들은 해당 String 타입에 대항하는 컬럼에 참조하게 되고 createAlias(String, String)메소드 뒤에 붙은 add() 메소드들은 여전히 기본(this)이 상위에 있는 createCriteria(Class)에 있는 Class 타입을 나타냅니다.

1을 제대로 동작하게 하려면 추가한 문장을 s.createCriteria(KMember.class) 요것 바로 다음으로 이동시키면 됩니다.

2에서 좀더 명확하게 나타내려면 .add(Restrictions.eq(“this.name“, “keesun”)); 이렇게 this를 추가해 주면 됩니다.

8.3. Criteria 공부하기

특정 Member의 검색을 할 때 이름만 입력할 수도 있고 이메일만 입력할 수도 있습니다. 입력 할 수 있는 곳이 여러 곳이면 둘 다 입력하거나 둘 다 입력하지 않을 수도 있습니다. 이럴 때 입력을 하느냐 안하느냐에 따라 쿼리가 달라지는데요. 이런것을 Dynamic Query라고 하는것 같습니다.

HQL을 이용해서 이러한 쿼리를 다음과 같이 작성할 수 있습니다.

    public void testDynamicQueryByHQL(){
        insertDatas();
        String name = “s”;
        String email = null;

        StringBuffer sb = new StringBuffer(“from k_Member m where 1=1”);
        if(!StringUtils.isEmpty(name)) sb.append(” and m.name like :name”);
        if(!StringUtils.isEmpty(email)) sb.append(” and m.email like :email”);
        q = s.createQuery(sb.toString());
        if(!StringUtils.isEmpty(name)) q.setParameter(“name”, “%” + name + “%”);
        if(!StringUtils.isEmpty(email)) q.setParameter(“email”, “%” + email + “%”);

        assertEquals(2, q.list().size());
    }

위와 같은 내용의 쿼리를 Criteria를 사용해서 작성하면 다음과 같이 간결해 집니다. 그리고 @Entity의 name속성에 지정해 준 값을 사용하는 것이 아니라 진짜 클래스 명을 사용해야 합니다.

    public void testDynamicQueryByCriteria(){
        insertDatas();
        String name = “s”;
        String email = “keesun@os.net”;

        Criteria c = s.createCriteria(KMember.class);
        if(!StringUtils.isEmpty(email)) c.add(Restrictions.eq(“email”, email));
        if(!StringUtils.isEmpty(name)) c.add(Restrictions.like(“name”, “%” + name + “%”));

        assertEquals(1, c.list().size());
    }

Criteria의 add 메소드는 인자로 Criterion을 넘겨 주어야 하는데요. Restrictions라는 팩토리를 사용해서 Criterion을 받아 오게 됩니다. Restrictions 클래스에는 쿼리에 덧붙일 수 있는 여러 메소드들이 있습니다.