By zmzizi

Hibernate concepts for Beginners-2 #

- 데이터 핸들링과 HQL 의 사용

여기에서는 Hibernate 를 사용하여 데이터 핸들링의 기본인 Create, Retrieve, Update, Delete 를 하는 방법들에 대해 다룰 것입니다. 또한, 트랜잭션 처리 및 HQL(Hibernate Query Language)의 사용에 대해 소개할 것입니다.

데이터 CRUD#

1. hibernate에서는 : Hibernate 에서의 데이터 CRUD 는 모두 net.sf.hibernate.Session 인터페이스를 경유하여 이루어진다.
2. 데이터 입력 : Hibernate 는 항상 POJO 를 매개로 데이터를 입력한다. 예를 들면 net.sf.hibernate.Session 인터페이스에 정의된 save 관련 메써드는 모두 POJO object 를 넘겨받아 입력한다.

Serializable save(Object object);
=> object 를 입력하고 입력된 object 의 id 값(Serializable)을 반환한다.

void save(Object object, Serializable id);
=> object 를 주어진 id값으로 저장한다.(id generator 가 assigned 일 경우 사용될 것이다.

void saveOrUpdate(Object object);
=> POJO 의 id 속성 중 unsaved-value 의 설정에 따라 save 혹은 update 를 선택하여 데이터를 조작한다.

Object saveOrUpdateCopy(Object object);
=> 주어진 object 의 상태값을 동일한 persistent instance(이후 저장객체)에 copy 를 한 이후, 이 저장객체를 반환한다. 만일 저장객체가 없다면 save 하고 새로운 저장객체를 반환한다.

Object saveOrUpdateCopy(Object object, Serializable id);
=> 상동(上同)

3. 데이터 수정 : 수정 역시 항상 POJO 를 매개로 이루어지며, native SQL 에서 지원하는 대량수정(ex:UPDATE tableName SET columnName = value WHERE columnName2 = value2)은 지원하지 않는다. 따라서, 동일한 조건으로 많은 데이터를 update 하기 위해서는 해당 조건에 맞는 데이터를 가져온 뒤에, Loop 를 돌면서 가져온 저장객체의 값을 수정하여 update 한다. 예를 들면 net.sf.hibernate.Session 인터페이스에 정의된 update 관련 메써드

void update(Object object);
=> 주어진 object 의 id 에 해당되는 저장객체의 상태값을 수정한다.

void update(Object object, Serializable id);
=> 주어진 id 에 해당되는 저장객체의 상태값을 수정한다.

4-1. 데이터 삭제 : 삭제의 경우 POJO 를 매개로 하는 방법과 query 로 삭제하는 방법 두 가지를 지원한다. 예를 들면 net.sf.hibernate.Session 인터페이스에 정의된 delete 관련 메써드

void delete(Object object);
=> 주어진 object 의 id 값에 일치하는 저장객체를 삭제한다.

int delete(String query);
=> 주어진 select HQL query 의 검색범위에 있는 데이터를 삭제한다.

int delete(String query, Object[] values, Type[] types);
=> 주어진 select HQL query 와 파라미터 값 및 타입의 검색범위에 있는 데이터를 삭제한다.

int delete(String query, Object value, Type type);
=> 주어진 select HQL query 와 파라미터 값 및 타입의 검색범위에 있는 데이터를 삭제한다.

4-2. oneshot delete : (까페글 352번 참조) 특정 부모 클래스에 속한 자식 클래스를 삭제하고자 할 때,

// 하이버네이트 세션을 얻는다.
Session session = super.getSession();

// 상위 객체인 유저 그룹을 가져온다.
Group group = session.get(Group.class, groupId);

// 상위 객체와 자식 객체간의 관계를 끊는다.
group.getUser().clear();

// 실제로 데이터베이스에서 물리적으로 자식 객체를 삭제한다. 즉 특정 조건의 객체들 삭제하는 것이다. 
session.delete("SELECT user FROM User user WHERE user.group.id = " + group.getId());
와 같은 방법으로 일괄삭제한다.

4-3. 만일, 해당 POJO 가 1:1 혹은 1:n 등으로 연관된 POJO 가 있고, 해당 POJO 들이 cascade 옵션이 all 혹은 delete 라면 연관관계에 있는 클래스들 역시 삭제된다. 즉, 위의 oneshot delete 의 예시에서 GROUP 이 삭제될 때 USER 까지 동시에 삭제하고자 한다면, USER 클래스의 GROUP property 의 cascade="delete" 로, GROUP 클래스에서 USER 클래스를 담는 collection property 의 cascade="all" 로 설정했다면, session.delete(groupObject); 를 호출하면, 해당 groupObject 에 속한 모든 USER 저장객체들도 삭제된다.
5. 데이터 인출

5-1. 하나의 데이터 인출 : net.sf.hibernate.Session 인터페이스에서 하나의 데이터를 인출하는 방법은 get(), load() 의 2가지로 분류된다. (물론, 대량 데이터를 인출해서 그 중 하나만을 가져올 수도 있지만, 그것은 예외적인 경우이다.) load() 에 Object object 파라미터를 받는 오버로드된 메써드가 있다는 점을 제외하면 이 두개의 메써드는 거의 비슷하지만, 둘의 결정적인 차이점은 get() 은 데이터가 없을 때 null 을 반환하지만, load() 는 Exception 을 발생시킨다는 점이다. 따라서, load() 는 데이터가 반드시 있으며 그 데이터를 반드시 가져와야 할 때 사용되는 무결성을 보장해주며, 저장객체의 존재유무를 판단하거나 데이터가 있을지 없을지 불확실한 경우에 쓰는 것은 보류되어야 한다.

net.sf.hibernate.Session 인터페이스에 정의된 delete 관련 메써드

// 넘겨진 id 에 해당되는, clazz 타입의 저장객체를 반환한다.
Object get(Class clazz, Serializable id);

// 넘겨진 id 에 해당되는, lockMode 가 설정된, clazz 타입의 저장객체를 반환한다.
Object get(Class clazz, Serializable id, LockMode lockMode);

// 넘겨진 id 에 해당되는, clazz 타입의 저장객체를 반환한다.
Object load(Class theClass, Serializable id);

// 넘겨진 id 에 해당되는, lockMode 가 설정된, clazz 타입의 저장객체를 반환한다.
Object load(Class theClass, Serializable id, LockMode lockMode);

// 넘겨진 id 에 해당되는 저장객체의 상태값을, 넘겨진 빈 object 객체에 담는다.
void load(Object object, Serializable id);

5-2. 대량 데이터 인출 : 대량 데이터 인출은 Session 인터페이스에서 직접 데이터를 가져오는 방법과 Session 에서 Query 인스턴스를 가져온 다음 Query 인스턴스로부터 데이터를 가져오는 방법, creteria 를 생성하는 방법의 3가지로 나뉜다.

- Session 인터페이스에서 직접 데이터를 가져오는 방법은 find() 메써드와 iterate() 메써드가 있으며, 이 둘은 반환타입이 List/iterator 의 차이만 있을 뿐, 파라미터 및 동작은 거의 같다.

- Query 인스턴스를 만드는 방법은 createQuery() 와 createSQLQuery() 의 두 종류가 있으며, createQuery() 는 HQL 로부터 Query 인스턴스를 생성하며, createSQLQuery() 는 native SQL(엄밀하게는 Hibernate 가 인식할 수 있도록 약간의 수정이 가해진 native SQL 이다.) 로부터 Query 인스턴스를 생성한다.

- Query 인스턴스로부터 데이터를 가져오는 방법은 list(), iterate(), uniqueResult() 가 있으며, 각각 list, iterate, object 를 반환한다. Query 인터페이스에는 생성시의 쿼리에 넘겨져야 할 각종 파라미터들을 세팅하는 setter 메써드가 있으며, 특기할만한 메써드는 setFirstResult(int firstResult), setMaxResults(int maxResults) 가 있다.

- 페이징 처리를 위해서는 setFirstResult(int firstResult) 에 시작할 rownum 을, setMaxResults(int maxResults) 에 페이지당 가져올 데이터수를 넣으면 된다. 예를 들어, 한페이지당 10개씩 데이터를 보여준다고 할 때, 3페이지의 데이터는 setFirstResult((3-1) * 10), setMaxResults(10) 이 된다. 이것은 MySQL 페이징시 사용되는 limit startRownum, maxResults 의 방법과 동일한 것이다.(이때, startRownum 은 0부터 시작된다는 사실을 잊지 말도록!)

- 만약, 하나의 데이터를 인출하고자 하지만, id 값이 아닌 조건으로 인출하고자 할 경우, find(), iterate() 로 가져온 컬렉션 데이터에서 get(0) 하거나 next() 로 데이터를 반환할 수 있다. 또한, 위와 같은 방식으로 Query 인스턴스를 가져온 뒤에, query.setMaxResults(1).uniqueResult() 를 하게 되면 데이터 하나만을 가져올 수 있다. 단, 이때 자신이 원하는 데이터를 가져오기 위해서는 Query 를 생성할 HQL 에서 order by 를 통해 자신이 원하는 데이터가 제일 앞에 오도록 정렬해야만 한다.

- Criteria 를 만드는 방법은 createCriteria(Class persistentClass) 메써드를 사용하며, 특정 POJO 클래스 타입의 데이터를 가져오기 위한 Criteria 인스턴스를 생성한다. (Criteria 를 만든 이후 사용법은 대충 눈대중으로 본 것 이상은 없는 관계로 사용해 보신 분들의 보충설명이 있기를 바랍니다.)

트랜잭션 처리#

트랜잭션의 처리를 위해 Hibernate 에서 제공하는 방법은 2가지가 있다.

하나는 Session 인터페이스의 flush 를 사용하는 것이고, 다른 하나는 Transaction 인터페이스를 사용하는 것이다.

1. flush() 의 사용

1-1 Session 인터페이스에는 flush() 와 관련되어 FlushMode flushMode getFlushMode(), void setFlushMode(FlushMode flushMode), void flush() 의 3가지 메써드가 있다. 내용은 메써드명에 분명하게 드러나 있으며, 사용은 다음과 같다.

Session session = 세션을 가져온다.
session.setFlushMode(FlushMode.NEVER);

try {
.. 처리 로직

session.flush();
}catch(HibernateException he) {
    //exception 처리
}finally {\\
    session.close();\\

1-2. FlushMode 클래스

FlushMode 클래스는 FlushMode 의 상태를 나타내는 3개의 static 변수를 가지고 있다.

- static FlushMode AUTO : 신선하지 못한 데이터를 반환하지 않도록 쿼리가 실행되기 전에 자동으로 flush 한다.
- static FlushMode COMMIT : Transaction.commit() 가 호출되면 해당 Session 에 담긴 정보가 flush 된다.
- static FlushMode NEVER : Session.flush() 가 호출되면 해당 Session 에 담긴 정보가 flush 된다.

2. Transaction 인터페이스의 사용

Transaction 인터페이스를 사용하는 방법은 일반적인 JDBC 의 트랜잭션 처리와 매우 유사하다.

Session session = 세션을 가져온다.
Transaction tx = session.beginTransaction();

try {
    // 처리 로직
    tx.commit();\\
}catch(HibernateException he) {
    //exception 처리
    tx.rollback();
}finally {
    session.close();
}

HQL 의 사용#

1. HQL 은 객체를 통한 접근이다. - 이 말은 간단한 테이블끼리의 연관관계들은 객체접근으로 바로 가능하다는 것이다. Tips(1) 에서 예로 들었던, Woman 과 Clothes 의 관계를 보자. Clothes POJO 는 자신의 소유주인 Woman 을 가진다.(물론, 가지지 않을 수도 있다.) 이때, 소유주의 나이가 30살 이상인 옷들을 가져오려고 할 경우, native SQL 로는 다음과 같이 표현가능하다.

① SELECT * FROM CLOTHES c INNER JOIN WOMAN w ON c.womanid=w.womanid WHERE w.age >= 30;
② SELECT * FROM CLOTHES c WHERE c.womanid IN (SELECT womanid FROM WOMAN w WHERE w.age >= 30);

하지만, HQL 을 사용하여 객체관계로 표현하면 다음과 같이 간략해진다.

FROM Clothes c WHERE c.woman.age >= 30;

즉, Clothes 타입 POJO 의 인스턴스 c 의 Woman 타입 property woman의 int 타입 property 인 age 를 . 으로 접근 가능하다는 것이다. 우리에게 이와 같은 접근방식은 EL(Expression Language : jsp 2.0 스펙에서 공식적인 표현형으로 도입) 에서 이미 익숙하다. 이때, 역의 관계로 접근하고자 할 때는 어떨까? 즉, Clothes 가 아닌 Woman 의 입장에서 데이터를 가져오고 싶을 때의 경우를 말한다. Woman 의 입장에서는 Clothes 는 collection 이기 때문에, 객체 접근이 불가능하다. 하지만, 이런 경우에는 위의 ①, ② 의 native SQL 을 HQL 로 전환하는 것으로 문제는 해결된다.

① SELECT count(*FROM CLOTHES c JOIN c.woman w WHERE w.age >= 30;
② SELECT count(*FROM CLOTHES c WHERE c.womanId IN (SELECT w.womanId FROM Woman w WHERE w.age >= 30);
위의 ①의 경우, Join 조건을 기술하지 않아도 되는데, 그것은 이미 CLOTHES c JOIN c.woman w 의 c.woman 이라는 표현에서 두 POJO 간의 Join 조건이 들어있기 때문이다.

2. HQL 은 초(超) Vendor 적인 OR mapping Tool 이다. 이것은 어떤 특정한 DBMS Vendor 의 기능을 별도로 지원하지는 않는다는 의미이다. 예를 들어, max(), min() 등의 펑션은 어떤 DBMS 에서도 동일하게 지원한다. 즉, HQL 에서도 지원한다. 그러나, now() 라는 펑션은 MySQL 에는 존재하지만 Oracle 에는 존재하지 않는 펑션이다. 따라서, now() 라는 펑션은 HQL 에서 지원하지 않는다. 비슷하게, rownum, Hint 와 같이 Oracle 특화 기능들 역시 HQL 에서는 지원하지 않는다. 하지만, base DBMS 에 따라 지원을 할 수도 안할 수도 있는 기능들 역시 존재하는데, 예를 들어 sub query 의 경우 HQL 자체는 지원을 하지만 기반 DBMS 가 MySQL 4.0 이하 버전처럼 sub query 를 지원하지 않으면 사용할 수 없는 문제들이 그것이다.(당연한 말인가? -_ -a)

3. HQL 로 작성된 쿼리 역시 최종적으로는 기반한 DBMS 의 SQL 로 변환되어 실행된다. 이것은 굉장히 중요한 문제인데, HQL 로 쿼리를 작성할 때에도 항상 최종 변환된 SQL 의 형태를 고려해야만 한다는 의미이다. 즉, 변환된 SQL 에 대한 Tunning 을 Hibernate 가 자체적으로 제공해주지 않으며, 오로지 개발자가 HQL 을 작성할 시에 그것을 고려해야만 한다. 예를 들어, HQL 이 SQL 로 변환될 때에는 항상 HQL 이 쓰여진 순서에 맞춰 대칭되는 SQL로 변환된다는 점, 쿼리문은 항상 앞부터 실행되며, 앞의 index 부터 타게 된다는 점 등이 그것이다. 이런 부분에 대한 세밀한 고려없이 막연히 HQL 은 객체접근이라는 생각만 가지고 있으면 극한의 퍼포먼스를 보게 될 것이다.(안 좋은 의미로... -0-/)

4. count() 를 할 경우, 반드시 count(*) 를 사용하라. 내가 경험했던 가장 황당한 Hibernate 사용기는 바로 count() 와 관련한 것이다. 나만의 문제인지, Hibernate 의 버그인지는 확인해보지 않았지만, 최소한 나의 경우에는 count() 는 굉장한 이슈였다. 일반적으로 우리가 native SQL 을 사용할 때, select count(*) from 혹은 select count(pk) from 이라고 많이 사용한다. 그리고, count(pk) 라고 하는 것이 조금 더 퍼포먼스에 유리하다고도 배웠던 것으로 기억한다. Hibernate 에서 count() 를 사용하는 것은 Session.find("select count(*) from Woman").get(0) 과 같이 보통 find(), iterator() 를 사용해 가져오게 된다. (uniqueResult()로도 가져올 수 있지만, 일반적으로 많이 사용되지는 않으니 이 설명에서는 배제하도록 하자.) 일반적으로 native SQL 에서 count() 를 할 경우 int 값이 반환된다. 그러니, HQL 을 사용해서 find() 했을 경우, list 의 제일 첫번째 데이터는 당연히 Integer 형일 것이라고 가정한다. 일반적으로는 그렇다. 그렇지만, 그렇지 않은 경우가 있다! select count(*) from 의 경우에는 위의 가정은 맞아 떨어진다. 하지만, select count(pk) from 식으로 count() 안에 특정 컬럼명이 들어갈 경우, Hibernate 에서는 이것을 해당 컬럼에 대한 데이터를 모두 가져오라는 것으로 인식한다. 즉, select woman from Woman woman 을 실행시키는 것과 같다. 내 경험에 비추어 봤을 때, count(*) 가 1초가 걸렸다면, count(pk) 는 5분이 넘게 걸렸다.(사실 5분쯤 지났을 때 alt+tab 을 눌러대다 컴퓨터가 다운됐다... -0-;;;) 이것은 10만건의 데이터를 일일히 Bean 에 매핑하고 Bean 을 List 에 담아서 반환하다 보니 생기는 어쩔 수 없는 시스템 부하 때문이었다. 그러면, 이렇게 넘겨받은 list의 size() 를 호출해서 반환할 것인가? =ㅅ= 결론은, 절대 count(columnName) 을 사용해서는 안된며, count 시에는 반드시 count(*) 를 호출해주는 습관을 들여야 할 것이다. 물론, 이것은 HQL 작성시에만 적용되는 규칙이며, native SQL 을 작성할 경우는 논외이다.

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-5) was last changed on 15-Sep-2006 12:10 by 211.183.67.167