잊지 않겠습니다.

'2013/09/10'에 해당되는 글 4건

  1. 2013.09.10 14. iBatis (myBatis)
  2. 2013.09.10 13. queryDSL & Spring Data JPA 1
  3. 2013.09.10 12. queryDSL & Spring Data JPA 1
  4. 2013.09.10 11. Spring과 Hibernate의 연결

14. iBatis (myBatis)

Java 2013. 9. 10. 10:27

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



ibatis (mybatis)

지금까지 직접 JDBC를 이용한 DB접속 방법과 Hibernate를 이용한 DB 접속 방법을 알아봤습니다.
DB라는 관계형 데이터를 java라는 객체 지향의 언어에 mapping을 시키는 과정은 참 여러가지 기술적인 요인들과 방법들을 계속해서 내보이고 있습니다.
그 중에서 SQL을 직접 사용하는 가장 low level의 JDBC, 그리고 SQL을 최대한 사용하지 않고 객체만으로 표현하는 Hibernate는 그 대표적인 기술이라고 할 수 있습니다. 그런데, 국내에서는 다른 기술을 더 많이 쓰고 있는 것이 사실입니다. 

iBatis(아래 부터는 myBatis)가 바로 그 기술입니다. 다른 것보다는 이 기술은 직접 sql을 사용한다는 큰 차이를 가지고 있습니다. 그렇지만, 기존에 우리가 만들었던 것과 같은 entity-dao-service layer를 충실히 지킬 수 있으며 기존의 SP와 같은 legacy sql query를 직접적으로 사용하고, code로서 관리한다는 점이 가장 큰 차이입니다.

iBatis는 JDBC를 이용한 반복적인 코드를 획기적으로 줄이는 것을 목표로 가지고 있으며, 개발자들에게 게을러질 수 있는 권리를 보장하고 있습니다. 

ibatis의 구조

Hibernate와 비슷하게 SqlSessionFactory, SqlSession이라는 두개의 객체를 가지고 있습니다. 기본적으로 mybatis.cfg.xml 파일을 이용해서 DB에 대한 연결 설정과 각각의 mapper를 구성하는 설정으로 구성되어 있습니다.

ibatis에서 DB에 대한 연결을 구성하는 방법은 다음과 같습니다. 

1. SqlSessionFactory 구성 
2. SqlSessionFactory를 통한 SqlSession을 구성
3. SqlSession을 통해 mapper를 구성

여기서 새로운 개념들이 조금 나오게 되는데요. Hibernate에서 많은 개념들을 차용해온것들이 나오게 됩니다. 

먼저, SqlSessionFactory는 이름 그대로 SessionFactory입니다. 

일단 SqlSession을 통해서 얻어지는 mapper는 기본적으로 dao 객체들입니다.

 
백문이 불여 일타. 한번 지금까지 만들어진 코드를 구성해보도록 합시다. 

기존 jdbcTemplate에서 사용한 Book, User, History 객체와 Dao에 대한 interface를 모두 카피해와서 새로운 프로젝트를 구성합니다.
myBatis는 maven central repository에 없습니다. 다음 repository 설정을 추가하고, dependency를 추가하도록 합니다. 이와 같이 maven central repository에서 지원하지 않는 library들은 자신만의 repository를 갖는 경우가 많습니다. 그리고 사내에서는 nexus라는 maven server를 설치해서, 사내 repository를 구성해서 사용하는 경우도 많습니다.


  <repositories>
    <repository>
      <id>mybatis-snapshot</id>
      <name>MyBatis Snapshot Repository</name>
      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    </repository>
  </repositories>

그 후, myBatis를 추가합니다.
   <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.1.1</version>
    </dependency>

   
myBatis에서 DB에 접근하기 위한 순서인 SqlSessionFactory를 먼저 구성해보도록 하겠습니다. 
SqlSessionFactory는 xml 파일로 설정하게 되며, 다음과 같이 설정할 수 있습니다. 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="bookstore">
    <environment id="bookstore">
      <transactionManager type="JDBC" />
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/bookstore" />
        <property name="username" value="root" />
        <property name="password" value="qwer12#$"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/userDao.mapper.xml" />
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/bookDao.mapper.xml" />
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/historyDao.mapper.xml"/>
  </mappers>
</configuration>

먼저 myBatis는 한개의 xml에 여러개의 connection 정보들을 담을 수 있습니다. 각각의 환경에 대한 id를 설정하고, 그 환경에 대한 기본 설정을 해주게 됩니다. DataSource에서 자주 보던 driver, url, username, password에 대한 설정을 하게 되는 것을 알 수 있습니다. 그리고 mapper들을 설정해주게 됩니다. 이 mapper들은 xml로 구성되어 있으며, 1 개의 method에 1:1로 mapping되는 SQL query가 들어가 있습니다. 일반적으로 interface 이름 + mapper.xml 의 명명 규칙을 따르게 됩니다.

다음은 bookDao.mapper.xml 파일의 내용입니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xyzlast.mybatis.bookstore.dao.BookDao">
  <resultMap type="com.xyzlast.mybatis.bookstore.entities.Book" id="BookResult">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="author" column="author"/>
    <result property="publishDate" column="publishDate"/>
    <result property="rentUserId" column="rentUserId"/>
    <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
  </resultMap>

  <select id="get" parameterType="int" resultMap="BookResult">
    SELECT * FROM books WHERE id = #{bookId}
  </select>
  <select id="getAll" resultMap="BookResult">
    SELECT * FROM books
  </select>
  <select id="countAll" resultType="int">
    SELECT count(*) FROM books
  </select>
  <insert id="add" parameterType="com.xyzlast.mybatis.bookstore.entities.Book" useGeneratedKeys="true" keyColumn="id">
    INSERT INTO books(name, author, publishDate, comment, status, rentUserId)
    VALUES(#{name}, #{author}, #{publishDate}, #{comment}, #{status.value}, #{rentUserId})
  </insert>
  <delete id="delete" parameterType="int">
    DELETE FROM books WHERE id = #{bookId}
  </delete>
  <delete id="deleteAll">
    DELETE FROM books
  </delete>
  <update id="update" parameterType="com.xyzlast.mybatis.bookstore.entities.Book">
    UPDATE books SET
    name = #{name}, author = #{author}, publishDate=#{publishDate}, status=#{status.value}, rentUserId=#{rentUserId}
    WHERE id = #{id}
  </update>
  <select id="search" parameterType="String" resultMap="BookResult">
    SELECT * FROM books
    WHERE name like "%${value}%"
  </select>
</mapper>


먼저, 가장 주시해서 봐야지 되는 것은 mapper의 namespace입니다. namespace에는 interface에 대한 package명이 포함된 full name이 들어가야지 됩니다. 이 부분에 대한 설정이 잘못 되어 있으면 사용할 수 없습니다. 그 다음으로 봐야지 될 것은 resultMap입니다. return되는 query문의 결과가 어떤 DTO/VO 객체에 어떤 property에 mapping되는지에 대한 설정이 여기에 기록이 됩니다. 재미있는 것이 BookStatus enum값의 mapping입니다. org.apache.ibatis.type.EnumOrdinalTypehandler를 이용해서 enum값과 BookStatus를 mapping시킬 수 있습니다. 

BookDao interface와 mapping.xml 파일을 한번 비교해보도록 하겠습니다.




query를 각각 type에 맞추어 select/insert/delete/update로 나눠 등록을 하게 됩니다. 또한 query의 id는 각 method의 이름과 1:1로 mapping이 되게 됩니다. myBatis를 이용하는 경우에는 interface에 대한 객체를 따로 만들지 않기 때문에 코딩양이 줄어 들 수 있습니다. 또한, sql query를 project에서 관리하고 있기 때문에 query에 대한 관리 역시 용의한 장점을 가지고 있습니다. 

이제 SqlSessionFactory를 얻어내는 과정은 모두 완료되었습니다. SqlSessionFactory는 application에서 딱 1개만 존재하면 됩니다. 이제 이 SqlSessionFactory에서 SqlSession을 얻어오는 과정은 Hibernate에서 SessionFactory를 얻어내는 과정과 완전히 동일합니다. 다음은 BookDao의 테스트 코드의 일부입니다. 

public class BookDaoTest {
    private SqlSession session;
    private BookDao bookDao;
    private SqlSessionFactoryGenerator sqlSessionFactoryGenerator;

    @Before
    public void setUp() throws IOException {
        sqlSessionFactoryGenerator = new SqlSessionFactoryGenerator();
        sqlSessionFactoryGenerator.setXmlFilename("mybatis.xml");
        SqlSessionFactory factory = sqlSessionFactoryGenerator.getSqlSessionFactory();

        session = factory.openSession();
        bookDao = session.getMapper(BookDao.class);
        bookDao.deleteAll();
        session.commit();
        assertThat(bookDao.countAll(), is(0));
    }

    @After
    public void tearDown() {
        session.close();
    }

    @Test
    public void add() {
        int count = bookDao.countAll();
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.add(book);
            session.commit();
            count++;
            assertThat(bookDao.countAll(), is(count));
        }
    }

SqlSessionFactory를 통해, SqlSession을 얻어내고 update/delete/insert에 대한 commit을 직접 행하도록 코드를 작성했습니다. 또한 모든 method가 완료되면 Hibernate와 동일하게, 반드시 Session을 닫아줘야지만 됩니다. 

Hibernate는 객체에 대한 xml 설정 또는 annotation 설정을 하는데 반하여, myBatis는 action에 설정을 하는 것을 주목해주세요. 이 둘의 차이는 매우 큰 차이를 가지고 오게 됩니다. 그리고, 지금 객체를 entities package에서 얻어오게 되었지만, 실질적으로 이 객체는 DTO또는 VO 객체가 됩니다. DB에서 값을 가지고 오는 역활만을 담당하는 객체로 보고 개발을 진행하는 것이 좋습니다. 


Summary

myBatis를 이용한 DAO 객체에 대해서 알아봤습니다. 지금까지 구성한 서비스까지 한번 구현해보세요. 이번에는 Spring을 사용하지 않고 구현하는 것이 목표입니다. 다음 장에서는 Spring을 이용해서 더욱더 간단하게 myBatis의 설정을 구축하는 것을 보여드리도록 하겠습니다. 





Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


queryDSL과 Spring Data JPA 모두 DAO에 대한 접근 방법을 제어하고, 코드양을 줄일 수 있는 좋은 방법입니다. 그렇지만, Spring Data JPA의 경우에는 규격대로 되어 있는 select 구문, update 구문 이외에는 사용할 수 없는 단점을 가지고 있습니다. 이러한 단점을 극복하는 좋은 방법은 Spring Data JPA에 queryDSL을 결합해서 사용하는 것입니다. 이렇게 되면, Spring Data JPA와 queryDSL의 모든 장점을 사용할 수 있습니다. 

queryDSL과 Spring Data JPA를 연동해서 사용하기 위해서는 maven repository에 다음과 같은 설정을 해야지 됩니다. 

    <repository>
      <id>spring-snapshot</id>
      <name>Spring Maven SNAPSHOT Repository</name>
      <url>http://repo.springsource.org/libs-snapshot</url>
    </repository>

그리고 버젼에 유의해서 Spring Data JPA와 queryDSL을 설정해줘야지 됩니다. queryDSL이 2.x에서 3.x대로 넘어가면서 객체의 이름이 많이 변경이 되었기 때문에 상호간의 호환에 문제가 있습니다. queryDSL 버젼은 3.1로, Spring Data JPA의 버젼은 1.4.0.BUILD.SNAPSHOT으로 설정해줍니다. 지금 queryDSL을 지원하는 Spring Data JPA의 경우에는 beta version이지만, 곧 정식버젼이 나온다고 하니 잠시 기다릴수 있을 것 같습니다.

다음은 설정될 maven pom 파일의 properties입니다. 

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
    <hibernate.version>4.1.10.Final</hibernate.version>
    <hibernate.validator.version>4.3.1.Final</hibernate.validator.version>
    <spring.version>3.2.2.RELEASE</spring.version>
    <querydsl.version>3.1.0</querydsl.version>
    <spring.jpa.version>1.4.0.BUILD-SNAPSHOT</spring.jpa.version>
  </properties>

이렇게 설정을 모두 마치고 코드를 한번 다시 살펴보도록 하겠습니다.

QueryDslPredicateExecutor

select 구문의 핵심입니다. QueryDslPredicateExecute는 JpaRespository interface와 같이 사용할 수 있습니다. interface는 다음과 같은 method를 포함합니다. 

public interface QueryDslPredicateExecutor<T> {
    T findOne(Predicate predicate);
    Iterable<T> findAll(Predicate predicate);
    Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
    Page<T> findAll(Predicate predicate, Pageable pageable);
    long count(Predicate predicate);
}


Predicate는 queryDSL의 query입니다. 이 interface를 상속하는 경우 BookRepository의 코드는 다음과 같이 구성될 수 있습니다. 

@Repositorypublic interface BookDao extends JpaRepository<Book, Integer>, QueryDslPredicateExecutor<Book> {

}

다른 코드는 전혀 필요없습니다. 이제 query를 한번 구성해보도록 하겠습니다. 

search에 대한 코드를 구성해보도록 하겠습니다. Spring Data JPA는 like 구문을 지원하지만, 모든 전/후에 대한 Like만 지원되기 때문에, 기존의 코드로는 구성이 불가능했습니다. 
다음과 같은 코드로 구성이 가능합니다. 

    @Test
    public void testProdicate() {
        String bookName = "책이름";

        QBook qBook = QBook.book;
        Predicate predicate = qBook.name.like(bookName + "%").and(qBook.status.eq(BookStatus.CanRent));
        Iterable<Book> books = bookDao.findAll(predicate);

        for(Book book : books) {
            assertThat(book.getName().contains(bookName), is(true));
        }
    }

Type에 safe 하고, 간단한 query 구문으로 code를 구성하는 것이 가능합니다. findAll method가 queryDSL의 Predicate를 지원하기 때문에 이러한 코드를 구성할 수 있습니다.
findAll을 이용하는 경우, 이제 select에 대한 이슈는 거의 해소가 가능합니다. 그래도 아직 문제가 조금 더 남아있습니다. 그 부분은 바로 각 항목에 대한 max, min 값을 구하는 Predicate query와 다양한 update,delete를 하는 query들을 만들어주는 것이 아직은 불가능합니다. 이러한 문제를 해결하고, queryDSL의 모든 기능을 지원하기 위해서는 QueryDslRepositorySupport를 사용해야지 됩니다. 

QueryDslRepositorySupport

QueryDslPredicateExecutor의 경우에는 select를 이용해서 객체를 얻어올 때 주로 사용됩니다. 그렇지만, 이에 대한 다른 접근이 필요합니다. 객체에 대한 update, delete의 경우에는 좀더 다양하게 처리하는 것이 필요합니다. 

예를 들어 다음 query를 처리할 때, hibernate의 경우에는 HQL 또는 Native query를 이용해서 처리하는 방법밖에는 없습니다. 

update users set name = 'ykyoon' where name='abc';

delete from users where name='abc';

이러한 일괄 변경 및 업데이트, 또는 특정 값의 sum, min, max, average 값을 도출하기 위해서 사용하는 것이 QueryDslRepositorySupport입니다. QueryDslRepositorySupport의 code를 한번 알아보도록 하겠습니다. 

@Repository
public abstract class QueryDslRepositorySupport {

    private final PathBuilder<?> builder;

    private EntityManager entityManager;
    private Querydsl querydsl;

    public QueryDslRepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass);
        this.builder = new PathBuilderFactory().create(domainClass);
    }

    @PersistenceContext
    public void setEntityManager(EntityManager entityManager) {

        Assert.notNull(entityManager);
        this.querydsl = new Querydsl(entityManager, builder);
        this.entityManager = entityManager;
    }

    @PostConstruct
    public void validate() {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        Assert.notNull(querydsl, "Querydsl must not be null!");
    }

    protected EntityManager getEntityManager() {
        return entityManager;
    }

    protected JPQLQuery from(EntityPath<?>... paths) {
        return querydsl.createQuery(paths);
    }

    protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
        return new JPADeleteClause(entityManager, path);
    }

    protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
        return new JPAUpdateClause(entityManager, path);
    }

    @SuppressWarnings("unchecked")
    protected <T> PathBuilder<T> getBuilder() {
        return (PathBuilder<T>) builder;
    }

    protected Querydsl getQuerydsl() {
        return this.querydsl;
    }
}

abstract class 이기 때문에, 상속을 받아서 구현해야지 됩니다. 먼저, User Id 값중 max 값을 얻어오는 code를 한번 알아보도록 하겠습니다.

    public int sumUserIds() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.id.max());
    }


다음은 Update입니다. 

    public Long updateNameByName() {
        QUser qUser = QUser.user;
        update(qUser).where(qUser.name.eq("abcde")).set(qUser.name, "가나다").execute();
        return 0L;
    }


마지막으로 Delete입니다. 

    public void deleteByName(String name) {
        QUser qUser = QUser.user;
        delete(qUser).where(qUser.name.eq(name)).execute();
    }

매우 간단하게 구현하는 것이 가능합니다. 우리가 지금까지 만든 JpaRepository와 QueryDslPredictExecutor와 같이 사용하는 경우에는 거의 모든 query들을 처리하는 것이 가능하게 됩니다. 전체 코드는 다음과 같습니다. 

@Repository
public class CalDao extends QueryDslRepositorySupport {
    public CalDao() {
        super(User.class);
    }

    public int sumUserIds() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.id.max());
    }

    public Long updateNameByName() {
        QUser qUser = QUser.user;
        update(qUser).where(qUser.name.eq("abcde")).set(qUser.name, "가나다").execute();
        return 0L;
    }

    public void deleteByName(String name) {
        QUser qUser = QUser.user;
        delete(qUser).where(qUser.name.eq(name)).execute();
    }
}

queryDSL + Spring Data JPA에서의 Hibernate 이용

사용하다보면 Hibernate의 기능을 사용해야지 될 때가 발생할 수 있습니다. 이는 기존의 coding이 Hibernate 기준으로 된 경우도 해당될 수 있을 것이고, Hibernate의 다양한 Criteria를 이용해보고 싶은 생각도 생길 수 있습니다. 
먼저, 계속해서 이야기드리는 것은 JPA라는 규격자체가 Hibernate라는 구현체 위에서 동작하는 것이기 때문에 Hibernate의 Session을 얻어내는것도 가능합니다. 다음과 같은 코드로 Session을 얻어서 Hibernate와 동일하게 처리하는 것도 가능합니다. 

    public Session getSession() {
        return (Session) getEntityManager().getDelegate();
    }


Summary

지금까지 queryDSL과 Spring Data JPA를 이용한 Model 구성에 대해서 알아봤습니다. 약 2개월간 계속해서 장/단을 뽑아보면서 제일 우리에게 좋은 Model Framework조합이 무엇인가를 고민했던 결과입니다. 이러한 개발 방법은 다음과 같은 장점을 가지고 있습니다. 

1. type-safe 한 query를 만들 수 있습니다.
2. 객체지향적인 코드 구성이 가능합니다.
3. Hibernate Criteria의 단점인 일괄 update / delete 문의 처리가 가능합니다.
4. Repository의 코딩양을 줄일 수 있습니다.
5. eclipse의 intellisense의 지원을 받을 수 있습니다.

최종적으로 구성되는 package입니다. 이 구성으로 대부분의 Model에 관련된 Project가 구성이 될 예정입니다. 




# config : ApplicationConfiguration class가 위치할 package입니다.
# entities : entity 객체들이 위치할 package입니다.
# repositories : JpaRepository, QueryDslPredictExecutor를 상속받은 Repository interface가 위치할 package입니다.
# repositories.support : QueryDslRepositorySupport를 상속받은 객체 또는 repositories에서 사용할 Predict를 지원하는 객체들이 위치할 package입니다.
# services : BL에 관련된 서비스가 위치할 package입니다.
# utils : 데이터의 변환 등 다양한 경우에 사용되는 utility class가 위치하는 package입니다.

여기서 지금까지 이야기하던 Dao와 Repository에 대한 정의를 다시 할 필요성이 있습니다. 

이 둘에 대한 정의는 다음과 같습니다. 

"DAO는 데이터베이스에서 값을 꺼내와 도메인 오브젝트로 반환해주거나 적절한 값으로 반환해주는 계층을 일컫는다. Repository는 한 도메인 오브젝트에 대해서 객체의 값을 보증해주기 위해 도메인 내부에서 데이터베이스와 소통하는 객체을 일컫는다."

말이 어렵습니다. 조금 더 단순하게 말하면....

# DAO는 DB과 연결되어, Domain Object(=Entity)로 변환하는 것을 의미합니다.
# Repository는 DB와 연결되어, Domain Object(=Entity)로 변환합니다. 단, Domain 내부에서만 사용됩니다.

뒤의 조건이 하나가 더 붙어있으면 Repository이고, DB에 접속하는 객체를 어느곳에서나 사용가능해야지 되면 DAO입니다. Repository pattern이란, DB에 접근하는 영역을 Domain Layer, 즉 Model 영역에서만 사용하게 되는 것을 의미하고, 이것은 n-tier system에 적합한 책임영역의 분리가 되는 코드를 의미합니다. 
만약에 Controller Layer에서 DB에 접근하게 되고, 사용하게 된다면, 그것은 더 이상 Repository가 될 수 없습니다. 그렇지만, Model/Domain Layer에서만 접근하게 된다면 이것은 DAO가 아닌 Repository가 되게 됩니다. 너무나 유사한 개념이지만, 영역을 나누는 의미에서 사용하는 용어의 차이라고 생각하시면 될 것 같습니다. 
우리가 개발하는 것은 DAO가 아닌 Repository가 되게 됩니다. (이것은 저도 잘못 개발하고 있던 내용중 하나입니다.) 이 둘간의 영역 차이는 매우 자주 나오는 문제입니다. 만약에 서비스가 아닌 다른 영역(BL이 아닌 다른 영역)에서 DB에 접근하게 되는 객체를 만든다면.. 이것은 DAO를 만들어주게 되는 것이 맞습니다. 
이건 pattern입니다. 무엇이 옳고 그른 문제는 아닙니다. Layer에 의한 명백한 의미를 나누기 위해서 Repository로 사용하는 것이 좀 더 나을 것 같습니다. 

정리입니다. 
queryDSL + Spring Data JPA를 이용한 Domain Layer의 개발을 주로 하게 될 것입니다. 이 부분은 지금 Open Source 측에서도 밀고 있는 추세이기도 하고, 계속해서 발전이 되어가고 있는 분야이기도 합니다. 
이제 다음시간에는 만약에 우리가 외부 SI를 나가게 된다면, 주로 사용하고 있는 myBatis(iBatis)에 대해서 간략하게 알아보도록 하겠습니다. 


Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



queryDSL과 Spring Data JPA에 대한 소개를 하기 전에 먼저 JPA에 대한 설명을 하도록 하겠습니다.

JPA란?

Java Persistence API의 약자입니다. 
영속성 객체에 대한 Java의 접근 방법을 정의한 API로, 객체를 통한 Persistence 영역의 접근을 제공하고 있습니다. 

객체를 통한 Persistence 영역에 대한 접근을 하기 위해, 사용되는 것이 ORM Framework 들이고, 그중에서 가장 선두를 달리고 있는 것이 Hibernate입니다. 

Hibernate와 JPA간의 차이를 한번 알아보도록 하겠습니다. 


HibernateJPA
DB에 대한 접근(DataSource)SessionFactoryEntityManagerFactory
DB의 query executorSessionEntityManager
INSERT 대응 methodsave | saveOrUpdatemerge
UPDATE 대응 methodupdate | saveOrUpdatemerge
DELETE 대응 methoddeleteremove
query 적용 방법Criteria, HQL (Hibernate query Language), Native SQL
* Criteria : session.createCriteria
* HQL : session.createQuery
* Native SQL : session.createSQLQuery
Criteria, JPQL (java persistence query language), Native SQL
* Criteria의 문법은 Hibernate와 큰차이를 보이고 있음. JPA3.0에서는 기존의 Hibernate와 통일성을 주는 방향으로 변화가능성이 있다고 이야기함
* JPQL의 문법은 HQL과 완전 동일
* JPQL : em.createQuery
* Native SQL : em.createNativeQuery

접근 하는 방식은 거의 1:1로 동일합니다. 다만 query의 적용방법에서 차이를 보이게 되는데요. HQL이 JPA의 표준이 되면서, 기존 Hibernate에서 주로 사용되고 있던 Criteria보다 근간에는 JPQL 또는 HQL을 더 많이 사용하게 되는 추세입니다. 다음 JPA 버젼에서는 Criteria의 문법변경 이야기가 있으니, 그 때는 다시 역전되지 않을까.. 하는 생각을 가지고 있습니다. 

지금부터 소개할 queryDSL과 Spring Data JPA는 JPA를 기반으로 하는 기술입니다. 
소개드릴 queryDSL과 Spring JPA Data는 다음과 같은 장점이 있습니다. 

1. Hibernate와 같은 save, update, delete를 사용할 수 있습니다.
2. type-safe 한 query를 작성 가능합니다.
3. sql 작성자들과 query를 code로 옮기기가 용의합니다.
4. DAO logic의 코드양이 획기적으로 줄어들 수 있습니다.

이번 장에서는 Hibernate, JPA를 기반으로 하는 Model에 대한 library들을 좀 더 알아보도록 하겠습니다. 

queryDSL

.NET에서 linq가 나온 후, linq의 사상을 옮겨온 Framework입니다. type-safe한 query 작성 및 Criteria 보다 보기 편한 query를 만드는 것이 목적입니다.

queryDSL은 query에 대한 Q-Object를 이용해서 마치 sql query문과 비슷하게 query문을 작성할 수 있습니다. 매우 깔끔하게 보이기도 하고, .NET에서 linq를 하던 사람들에게 좀 더 익숙하기도 한 개발 방법입니다. queryDSL을 사용하면 다음과 같은 query 문을 작성해서 처리 가능합니다. 다음은 queryDSL을 이용한 select query입니다. 

List<Customer> result = query.from(customer)
    .where(customer.lastName.like("A%"), customer.active.eq(true))
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .list(customer);    

select * from customer where lastname like 'A%' and active = 1 order by lastname asc, firstname desc;

보시면 query문과 매우 비슷한 구조를 가지고 있습니다.

queryDSL과 나중에 소개드릴 Spring Data JPA는 java persisance area 표준에 따르고 있습니다. JPA라고 불리우는 이 표준은 Hibernate가 주축이 되어, Hibernate의 ORM적 구성이 Java의 표준이 되어버린 구성입니다. 지금 구성된 queryDSL은 Hibernate와 같은 JPA 상에서 구성되는 Library입니다. 따라서, Hibernate로 구성된 BookStore가 그대로 사용되어질 수 있습니다. 

개발 순서는 다음과 같습니다. 

1. Hibernate ORM 규칙에 따른 JPA annotation을 이용한 객체 정의
2. maven을 이용한 compile
3. Query Definition을 이용한 query 작성

먼저 1번 항목인 Hibernate ORM 규칙에 따른 JPA annotation을 모두 구성한 프로젝트에 다음 library들을 모두 추가합니다. (version에 유의해주세요!)
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
    <hibernate.version>4.1.10.Final</hibernate.version>
    <hibernate.validator.version>4.3.1.Final</hibernate.validator.version>
    <spring.version>3.2.2.RELEASE</spring.version>
    <querydsl.version>3.1.0</querydsl.version>
  </properties>

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>${hibernate.validator.version}</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-core</artifactId>
      <version>${querydsl.version}</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-apt</artifactId>
      <version>${querydsl.version}</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-jpa</artifactId>
      <version>${querydsl.version}</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-sql</artifactId>
      <version>${querydsl.version}</version>
    </dependency>

그리고 build > plugins 에 다음 plugin을 추가합니다. 

      <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>maven-apt-plugin</artifactId>
        <version>1.0.4</version>
        <executions>
          <execution>
            <goals>
              <goal>process</goal>
            </goals>
            <configuration>
              <outputDirectory>target/generated-sources/java</outputDirectory>
              <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
          </execution>
        </executions>
      </plugin>

그리고 command 창에서 mvn compile을 실행해서 code를 compile합니다. 
compile 후, target directory의 generated-sources를 봐보시길 바랍니다. 안에 보시면 Query 코드들이 만들어져 있습니다. 


이제 target/java 에서 우클릭해서, Use Source Code를 선택해주세요.


이제 queryDSL을 사용할 준비를 모두 마쳤습니다. 

기존 코드인 countAll과 search를 한번 비교해보도록 하겠습니다. 

Hibernate Criteria
    @Override
    public int countAll() {
        Session session = sessionFactory.getCurrentSession();
        Long count = (Long) session.createCriteria(Book.class)
                      .setProjection(Projections.rowCount()).uniqueResult();
        if(count == null) {
            return 0;
        }
        return count.intValue();
    }

    @Override
    public List<Book> search(String name) {
        Session session = sessionFactory.getCurrentSession();
        @SuppressWarnings("unchecked")
        List<Book> books = session.createCriteria(Book.class)
                                  .add(Restrictions.like("name", name, MatchMode.ANYWHERE))
                                  .list();
        return books;
    }

queryDSL
    @Override
    public int countAll() {
        HibernateQuery query = new HibernateQuery(sessionFactory.getCurrentSession());
        QBook qBook = QBook.book;
        Long count = query.from(qBook).uniqueResult(qBook.count());
        return count.intValue();
    }

    @Override
    public List<Book> search(String name) {
        HibernateQuery query = new HibernateQuery(sessionFactory.getCurrentSession());
        QBook qBook = QBook.book;
        return query.from(qBook).where(qBook.name.like("%" + name + "%")).list(qBook);
    }

마치 직접 query를 사용하고 있는 것과 비슷한 문법을 보여줍니다. 이 방법은 Hibernate의 Criteria를 사용할 때 발생할 수 있는 property의 직접 타이핑에 의한 에러를 방지할 수 있으며, 개발자들이 보다더 쉽게 query를 작성할 수 있도록 도와주게 됩니다. 
기존의 CUD는 Session의 save, update, delete를 기존과 같이 사용하고, Read method만 queryDSL을 사용해서 개발의 속도를 향상하고, 오타를 방지할 수 있습니다. 

다음은 queryDSL의 예시입니다. (queryDSL homepage에서 보다 많은 예시를 볼 수 있습니다.)

JOIN
QCat cat = QCat.cat;
QCat mate = new QCat("mate");
QCate kitten = new QCat("kitten");
query.from(cat)
    .innerJoin(cat.mate, mate)
    .leftJoin(cat.kittens, kitten)
    .list(cat);
from Cat as cat
    inner join cat.mate as mate
    left outer join cat.kittens as kitten

Order
QCustomer customer = QCustomer.customer;
query.from(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .list(customer);
from Customer as customer
    order by customer.lastName asc, customer.firstName desc

Group By
query.from(customer)
    .groupBy(customer.lastName)
    .list(customer.lastName);
select customer.lastName
    from Customer as customer
    group by customer.lastName

매우 많은 query 문이 존재하고, sql query로 대부분이 작성 가능합니다. 꼭 한번 사용해보시길 바랍니다. 

하나 더 말씀드린다면, 이 부분은 지금 저희 개발에서 Domain의 main 기술이 될 것입니다. Hibernate를 바로 사용하는 것에 대한 부담을 해결할 수 있습니다. 그리고 query를 만드는 개발 패턴을 좀 더 효율적으로 구성할 수 있다는 장점을 가지고 있습니다. 



Spring Data JPA

Spring Data JPA는 ORM Framework입니다. JPA 기반의 repository를 아주 빨리, 쉽게 개발할 수 있는 방법을 제시하고 있습니다. 지금까지 보던 모든 기술들(JdbcTemplate, Hibernate, myBatis)에서 repository는 객체만 바뀔뿐, 코드의 중복은 계속해서 나타나게 됩니다. 특히 우리가 CRUD라고 부르는 영역의 CUD 코드의 경우에는 거의 대부분이 중복이 되는 경우가 많습니다. 이러한 문제점을 착안하여 Spring Data - JPA project가 시작되게 되었습니다. 

Spring Data JPA는 단독으로 동작하는 ORM Framework가 아닙니다. JPA ORM engine을 기반으로 동작하는 ORM Framework입니다. 여기에서 JPA ORM engine이 될 수 있는 표준적인 JSR 220 을 만족하는 ORM engine은 다음과 같습니다. 

# Hibernate
# Google App Engine for JAVA
# TopLink

Google App Engine이 RDBMS를 완벽하게 지원하지 못하고, TopLink의 경우에는 Oracle에서 판매하는 상용제품이며 Oracle DB에만 특화가 되어있는 ORM입니다. 그렇다면, 실질적으로 지금 사용할 수 있는 선택의 폭은 Hibernate 밖에는 존재하지 않습니다. 지금 상태로는 Spring Data JPA는 Hibernate를 기반으로 동작하는 ORM Framework이다. 라고 생각해주시면 좋을 것 같습니다. 

Spring Data JPA를 사용하기 위해서는 다음을 추가합니다. (Hibernate 프로젝트 생성에서 시작합니다.)

    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-jpa</artifactId>
      <version>1.3.0.RELEASE</version>
    </dependency>

Spring Data JPA는 Spring 중 다음 jar 들과 연관관계를 갖습니다. 

spring-core, spring-beans, spring-context, spring-orm, spring-aop, spring-tx

이제 JPA의 기반이 되는 Hibernate를 추가해주도록 하겠습니다. 

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>${hibernate.version}</version>
    </dependency>

마지막으로 Spring Framework를 모두다 추가하도록 하겠습니다. 

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>${spring.version}</version>
    </dependency>

이제 pom.xml 설정이 모두 마쳐졌습니다. 

조금 장황할정도로 pom.xml 에 설정이 많이 들어가게 됩니다. 

Spring JPA Data는 Dao/Repository의 반복적인 코드가 될 수 있는 CUD를 자동화된 코드로 지원합니다. Spring JPA Data에서 제공하는 interface는 다음과 같습니다.

# Repository
# CrudRepository
# JpaRepository

이 중에서 우리가 사용할 녀석은 다른 interface를 모두 상속한 JpaRepository<T, C>입니다. JapRepository의 선언은 전에 Hibernate의 GenericDao의 선언과 거의 동일합니다. 단 두번째 인자가 PK에 대한 객체 Type을 넣어주는것만 다릅니다. Book, User, History 모두 int를 사용하고 있기 때문에, Integer를 두번째 Type에 넣어주면 됩니다. 
코드는 Hibernate에서 작성된 annotation이 모두 적용된 entity 객체들을 모두 project에 copy를 합니다. 먼저 단순하게 BookDao interface를 다음과 같이 작성합니다.

public interface BookDao extends JpaRepository<Book, Integer> {

}

interface 구현이 아닌 interface 상속을 통해서 구현이 되어 있는 것을 주의해주세요. JpaRepository<T,C> 의 T에는 Target이 되는 Class가 들어가고, C에는 Target Class의 @Id property의 객체값이 들어갑니다. (int인 경우, Integer)

자. 이제 지금까지 만들었던 BookDaoImpl의 코딩은 모두 끝났습니다. interface에 대한 코딩 작업이 전혀 필요하지 않습니다. 그 이유는 기본적으로 전에 보셨던 GenericDao에 대한 확장과 비슷한 방법입니다. Spring Data JPA는 JpaRepository, Repository, CrudRepository를 상속받은 interface를 모두 찾아내서, 동적인 객체를 생성합니다. 우리가 따로 코드를 만들어줄 필요가 전혀 없는것이지요. GenericDao의 결정판입니다. ^^

이제 테스트 코드를 작성해보도록 하겠습니다. 기존의 테스트 코드에서 약간의 변경만 있으면 됩니다. countAll()을 count()로, add, update를 모두 save로 변경만 시켜주면 테스트 코드가 모두 완성됩니다. 

@SuppressWarnings("unused")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@TransactionConfiguration(transactionManager="transactionManager", defaultRollback=false)
@Transactional
public class BookDaoTest {
    @Autowired
    private BookDao bookDao;
    @Autowired
    private HistoryDao historyDao;
    @Autowired
    private UserDao userDao;
    private static Logger logger = LoggerFactory.getLogger(BookDaoTest.class);

    @Before
    public void setUp() {
        logger.trace("==== setUp started");
        assertThat(bookDao, is(not(nullValue())));
        historyDao.deleteAll();
        userDao.deleteAll();
        bookDao.deleteAll();
        assertThat(bookDao.count(), is(0L));
        logger.trace("=== setUp ended");
    }

    @Test
    public void saveAndCount() {
        long currentCount = bookDao.count();
        long index = 0L;
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.save(book);
            index++;
            assertThat(bookDao.count(), is(currentCount + index));
        }
    }

마지막으로 applicationContext.xml을 구성합니다. applicationContext는 지금까지 보던것과 약간 많이 다릅니다. 구성된 applicationContext.xml 입니다. 

  <context:property-placeholder location="spring.properties" />
  <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
    <property name="driverClass" value="${connect.driver}" />
    <property name="jdbcUrl" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
    <property name="idleConnectionTestPeriodInMinutes" value="60" />
    <property name="idleMaxAgeInMinutes" value="240" />
    <property name="maxConnectionsPerPartition" value="30" />
    <property name="minConnectionsPerPartition" value="10" />
    <property name="partitionCount" value="3" />
    <property name="acquireIncrement" value="5" />
    <property name="statementsCacheSize" value="100" />
    <property name="releaseHelperThreads" value="3" />
  </bean>
  <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="dataSource" ref="dataSource" />
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  </bean>
  <tx:annotation-driven transaction-manager="transactionManager" />
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="packagesToScan" value="xyzlast.bookstore.jpa.bookstore01.entities"/>
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter" ref="hibernateVendor"/>
    <property name="jpaPropertyMap" ref="jpaPropertyMap"/>
  </bean>  
  <util:map id="jpaPropertyMap">
    <entry key="hibernate.dialect" value="${connect.dialect}" />
  </util:map>  
  <bean id="hibernateVendor" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
    <property name="showSql" value="true"/>
  </bean>
  <jpa:repositories base-package="xyzlast.bookstore.jpa.bookstore01.repository" transaction-manager-ref="transactionManager" />

@Configuration을 통해서, ApplicationConfiguration이 어떻게 구성이 되어 있는지 한번 알아보도록 하겠습니다.

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages= {"com.xyzlast.bookstore.dao"})
@PropertySource("classpath:spring.properties")
@ComponentScan(basePackages = { "com.xyzlast.bookstore.dao", "com.xyzlast.bookstore.services", "com.xyzlast.bookstore.utils" })
public class HibernateBookStoreConfiguration {
    @Autowired
    private Environment env;

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configHolder = new PropertySourcesPlaceholderConfigurer();
        return configHolder;
    }

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();
        dataSource.setUsername(env.getProperty("connect.username"));
        dataSource.setPassword(env.getProperty("connect.password"));
        dataSource.setDriverClass(env.getProperty("connect.driver"));
        dataSource.setJdbcUrl(env.getProperty("connect.url"));
        dataSource.setMaxConnectionsPerPartition(20);
        dataSource.setMinConnectionsPerPartition(3);
        return dataSource;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactory.setPackagesToScan("com.xyzlast.bookstore.entities");
        entityManagerFactory.setDataSource(dataSource());
        entityManagerFactory.setJpaVendorAdapter(hibernateJpaVendorAdapter());
        entityManagerFactory.setJpaProperties(japProperties());
        return entityManagerFactory;
    }

    @Bean
    public Properties japProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
        return properties;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }

    @Bean
    public HibernateJpaVendorAdapter hibernateJpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter =  new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(true);
        return hibernateJpaVendorAdapter;
    }

    // Hibernate를 이용하는 경우, 반드시 HibernateExceptionTranslator가 Bean에 등록되어야지 된다.
    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }
}

설정을 보시면, 이제 기존에 가지고 있던 Hibernate 관련 SessionFactory가 모두 제거가 된 것을 알 수 있습니다. JPA는 java에서 persistance layer에 접근하는 방식을 표준화 한것입니다. 이에 대한 구현체로 가장 유명한 것이 Hibernate가 되기 때문에, Hibernate를 이용한 JPA를 이용하기 때문에, JPA로 Hibernate를 한번 더 싸는 형태로 구성이 되게 됩니다. 

# jpaPropertyMap : jpa의 추가 속성들을 집어넣는 Map입니다.
# hibernateVendor : ORM vendor를 지정하는 객체입니다.
# entityManagerFactory : entityManagerFactory는 Hibernate에서의 SessionFactory와 동일한 역활을 하는 객체입니다. entityManagerFactory를 구성합니다. 속성으로는 Entity로 등록할 객체가 모여있는 package를 지정하고 dataSource, ORM vendor, property를 지정합니다.
# transactionManager : transactionManager를 구성합니다.
# jpa:repositories : repository(dao) 인터페이스가 위치한 package를 지정하고, TransactionManager를 지정합니다.

이제 테스트 코드를 돌려보면 정상적으로 돌아가는 것을 알 수 있습니다. 그리고 BookDao interface의 객체를 System.out.println으로 찍어보면 다음과 같습니다. (HistoryDao 코드만 제외!. HistoryDao의 findByUser, findByBook method는 아직 구현되지 않았습니다.)

org.springframework.data.jpa.repository.support.SimpleJpaRepository@74279e1e

SimpleJpaRepository라는 객체를 Spring Data JPA가 만들어서 @Autowired 해서 사용하는 방식으로 변경하게 됩니다. 이 객체는 CRUD에서 CUD는 이미 지원하고 있고, R의 경우에는 select * from books 정도의 전체를 얻어오는 query를 지원하고 있습니다. 만들어진 interface에 where 조건을 붙인 query를 어떻게 사용하는지 알아보도록 하겠습니다. 


findByXXXX method의 선언

HistoryDao의 findByBook, findByUser의 경우에는 History의 book, user property를 참고해서 where 절을 만들게 됩니다. 이러한 작업 역시 중복 작업이 될 수 있기때문에, 이러한 method에 대해서 Spring Data JPA는 다음과 같은 방법을 이용합니다.

findBy{PropertyName} 을 이용하면, 자동으로 where 조건을 만족하는 구문을 만들어주게 됩니다. 그래서 구현되는 interface는 다음과 같습니다. 

@Repository
public interface HistoryDao extends JpaRepository<History, Integer> {
    List<History> findByUser(User user);
    List<History> findByBook(Book book);
}


이렇게 선언을 해주면, Spring JPA Data가 자동으로 모든 method의 구현코드를 만들어줍니다. GenericDao의 결정판이라고 이야기드린 이유를 아시겠나요? ^^

KEYWORDSAMPLEJPQL SNIPPET
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
BetweenfindByStartDateBetween… where x.startDate between 1? and ?2
LessThanfindByAgeLessThan… where x.age < ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> age)… where x.age not in ?1

Spring Data JPA를 이용한 Sort, Paging


findByXXX method의 경우에는 Sort와 Paging이 확장이 가능합니다. 

기본적으로 Spring Data JPA의 경우에는 method 이름을 이용한 Sort를 지원하고 있습니다. 때에 따라서, Dynamic한 Sort를 넣어줘야지 되는 경우가 종종 존재할 수 있습니다. 이러한 경우, method의 가장 마지막 parameter에 Sort 객체를 넣어주면 다양한 Sort를 지원할 수 있습니다. 기본적인 사용은 다음과 같습니다. 조금 아쉬운것이, Sort의 경우에는 OrderBy의 Property를 문자열로 넣어줘야지 되는 단점을 가지고 있습니다. 약간 아쉬운 점이라고 할 수 있습니다.

public interface BookDao extends JpaRepository<Book, Integer> {
    List<Book> findByNameLike(String name);
    List<Book> findByNameLike(String name, Sort sort);
}

    @Test
    public void findByNameLinkeUsingSort() {
        String bookName = "book";
        String bookNameQuery = "%" + bookName + "%";
        Sort sort = new Sort(Sort.Direction.DESC, "status");
        List<Book> books = bookDao.findByNameLike(bookName, sort);
        for(Book book : books) {
            assertThat(book.getName().contains(bookNameQuery), is(true));
        }
    }

다음은 Paging입니다. 기본적으로 Paging은 Sort의 사용법과 동일하게 parameter의 마지막에 Pageable 항목을  추가해서 Page의 index, size, sort를 모두 지원 가능합니다. Paging을 기본적으로 제공하는 것은 findAll method입니다. findAll에서 Paging을 지원하는 예제 코드는 다음과 같습니다. 

    @Test
    public void findAllUsingPaging() {
        Sort sort = new Sort(Sort.Direction.DESC, "status");
        PageRequest pageRequest = new PageRequest(0, 10, sort);
        Page<Book> books = bookDao.findAll(pageRequest);

        assertThat(books.getNumberOfElements() <= 10, is(true));
        assertThat(books.getNumber(), is(0));
    }


Summary

queryDSL은 type-safe 한 query를 작성할 수 있도록 도움을 줍니다. 개발에 있어서 가장 큰 문제가 될 수 있는 오타에 의한 에러를 막아줄수 있는 확실한 방법이고, query문에 유사한 포멧을 가지게 되기 때문에 SQL 개발자에게도 유용한 개발 방법이 될 수 있습니다. 그리고 Spring Data JPA는 DAO에 대한 코드양을 줄이고, 직관적인 포멧의 코딩을 가능하게 합니다. Spring이나 지금 신생으로 크고 있는 Open Source이기도 하고, 개발에 있어서 많은 도움을 받을 수 있습니다. 

이제 다음장에서는 queryDSL과 Spring JPA Data간의 결합을 시도해볼 예정입니다. 지금까지 구현된 코드를 한번 타이핑을 해보시고 만들어보시길 바랍니다. 감사합니다.






Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



저번 과제를 진행하시면, 무언가 코드의 중복이 일어나는것 같은데... 하는 느낌을 가지실 수 있을겁니다. 

그 부분은 각 code의 get, add, delete, countAll, deleteAll code의 부분이 계속되는 코드 패턴의 반복으로 나타나게 됩니다. 이 부분에 대한 코드 수정을 한번 해보도록 하겠습니다. 

간단히 각 dao의 add method의 차이를 한번 알아보도록 하겠습니다. 

void add(User user);
void add(Book book);
void add(History history);

method의 형태가 매우 유사합니다. 세부 코드를 보시면 더 비슷합니다.

public void add(final User user) {
    executor.execute(new HibernateAction() {
         session.save(user);
    });
}

이 내부의 코드 중에서 변화하는 부분과 변화하지 않는 부분을 나누면, 객체의 Type 이외에는 변화하지 않는 것을 알수 있습니다. 이렇게 Type만이 다르고, 코드가 동일한 경우에는 Generic을 이용해서 중복 코드를 제거할 수 있습니다. 

이제 상속을 통해서 코드의 중복을 한번 제거해보도록 하겠습니다. 기존 interface의 중복이 될 수 있는 get, add, delete, countAll, deleteAll에 대한 interface를 GenericDao interface로 따로 구성하도록 합니다. 이제 다른 Interface들은 GenericDao interface를 상속받아 구성될 것입니다.

public interface GenericDao<T> {
    List<T> findAll();
    int countAll();
    T findById(final int id);
    void update(final T t);
    void add(final T t);
    void delete(final T t);
    void deleteAll();
}


그리고, 이 interface를 받는 GenericDaoImpl을 구성해보도록 하겠습니다. 이제 모든 DaoImpl은 GenericDaoImpl을 상속받아 사용할 예정입니다.

public abstract class GenericDaoImpl<T> implements GenericDao<T> {
    private final Class<T> type;
    protected final HibernateSqlExecutor executor;

    public GenericDaoImpl(Class<T> type, HibernateSqlExecutor executor) {
        this.type = type;
        this.executor = executor;
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        return (List<T>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(type).list();
            }
        });
    }

    @Override
    public int countAll() {
        Long count = (Long) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(type).setProjection(Projections.rowCount()).uniqueResult();
            }
        });
        return count.intValue();
    }

보시면 생성자부분에 새로운 코드가 들어가게 됩니다. Class<T>가 바로 그것인데요. Class의 Type값을 생성자에 넣어서, Session의 대상이 되는 class를 지정하게 됩니다. 

이제 상속받은 BookDao interface와 BookDaoImpl을 살펴보도록 하겠습니다. 

public interface BookDao extends GenericDao<Book> {
    List<Book> search(String name);
}

public class BookDaoImpl extends GenericDaoImpl<Book> implements BookDao {

    public BookDaoImpl(HibernateSqlExecutor executor) {
        super(Book.class, executor);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> search(final String name) {
        return (List<Book>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(Book.class)
                .add(Restrictions.like("name", name, MatchMode.ANYWHERE))
                .list();
            }
        });
    }
}

상당히 코드가 재미있게 변경이 됩니다. 

이렇게 변경할 수 있는 것은 객체만으로 통신할 수 있기 때문입니다. 따로 sql query문과 같은 다른 문자열을 다루는 것이 전혀 없고, 객체만으로 RDBMS와 통신할 수 있는 장점이 가장 보이는 코드이기도 하지요. 

한가지 더 이야기드린다면, 나중에 나올 Spring JPA Data의 경우에는 이 GenericDao interface를 극단적으로 발전시킨 경우가 만들어집니다. 

꼭 한번 이런식으로 코드를 변경시켜보시길 바랍니다. 그리고 덤으로 Generic에 대한 개념을 꼭 알아두시길 바랍니다. (C++의 경우에는 Template을 찾아보시면 됩니다.)


Spring + Hibernate

지금까지 작성되었던 Hibernate를 이용한 dao, service layer를 spring을 이용하도록 수정해보도록 하겠습니다. 

먼저 조금 소개를 한다면, Spring과 Hibernate는 원래 사이가 좋은 편은 아니였습니다.
예전 Spring 1.x와 Hibernate 2.x 간에는 개발자간에 매우 심각한 대립이 존재를 했었고, 그로 인한 키보드 배틀이 엄청나게 있었지요.
가장 큰 이유는 바로 전까지 제가 만들었던 HibernateSqlExecutor와 같은 HibernateTemplate을 Spring에서 제공하고 있었습니다. Hibernate는 DB에 접근하는 방법을 Session을 바로 얻어서 사용하도록 되어 있는데, 이러한 Raw level 접근을 Spring을 사용함으로서 제약을 걸게 되어 있었던 것이 사실입니다. 

그렇지만, Hibernate 3.x대로 넘어가고, Spring이 2.x대로 넘어가면서 화해(?)를 하게 됩니다. Spring 측에서 HibernateTemplate을 사용하지 않고, Spring에서 Hibernate를 사용할 수 있도록 Spring Framework의 큰 부분을 변경했습니다. 그리고 그에 맞추어 Hibernate에서는 SessionFactory에서 getCurrentSession() method를 추가함으로서, Spring의 기본적인 Transaction 방법에 맞추어 Hibernate를 이용한 DB 접근을 가능하게 하였습니다. 

둘간의 관계는 Spring 자체는 application framework입니다. Hibernate는 ORM framework고요. Hibernate 자체가 Spring보다 범위가 작은 Framework라고도 할 수 있습니다. Java의 모든 application은 Spring을 사용할 수 있지만, DB를 사용하지 않는 Application은 Hibernate를 사용하지 않을테니까요. 그럼에도 불구하고, Java의 최고 양대 open source framework는 spring과 hibernate입니다. 둘은 open source가 세상을 얼마나 바꾸어 놓을 수 있는지 보여주었고, 상업적으로도 엄청난 성공을 거뒀습니다. spring은 지금 vmware에서 제공되고 있고, hibernate는 weblogic을 제공하는 JBOSS에서 제공되고 있습니다. 

잡설이 길었습니다. spring은 이러한 hibernate를 위한 library를 따로 분리해서 사용하고 있습니다. pom.xml에 다음 jar가 추가되어야지 됩니다. 

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>

Hibernate 까지 포함한 전체 jar의 구성은 다음과 같습니다. (BoneCP를 사용해서 ConnectionPool을 사용할 예정입니다.)

    <!-- Test 관련 jar -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.2.1.RELEASE</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.22</version>
    </dependency>
    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.9.Final</version>
    </dependency>

    <!-- Spring 관련 jar -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <!-- boneCP 관련 jar -->
    <dependency>
      <groupId>com.jolbox</groupId>
      <artifactId>bonecp</artifactId>
      <version>0.7.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>14.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.2</version>
    </dependency>

사용되는 jar들이 꽤나 많습니다. 이건 세발의 피입니다. ㅠㅠ 나중에 좀 사용하다보면 pom.xml 파일이 600 line이 넘어가는 경우가 허다하게 나옵니다. ㅠㅠ

이제 spring을 이용한 transaction 구성과 BookDao, UserDao, HistoryDao의 구현 객체를 수정하도록 하겠습니다. 이제 Transaction을 spring에서 관리할 예정이기 때문에 기존의 HibernateSqlExecutor를 제거할 예정입니다. 그리고 Transaction을 맺는것은 한개의 Session을 Dao Action에 계속해서 사용하는 형식이 됩니다. 
이 부분은 Hibernate에서 Transaction/Session에 대한 관리 부분과 연관이 됩니다. 기본적으로 Hibernate는 Session을 beginTransaction(), commit(), rollback()을 이용해서 Session의 변경사항을 db에 반영하도록 되어 있습니다. 그런데, Spring의 @Transactional을 이용하면 @Transactional의 시작시점에서 자동으로 session.beginTransaction()의 시작과, commit(), rollback()을 구성해주게 됩니다. 따라서 전체 코드는 다음과 같이 수정이 되게 됩니다. (GenericDaoImpl만 코드를 살펴보도록 하겠습니다.)

public abstract class GenericDaoImpl<T, K extends Serializable> implements GenericDao<T, K> {
    private final Class<T> type;
    protected SessionFactory sessionFactory;

    public GenericDaoImpl(Class<T> type) {
        this.type = type;
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        Session session = sessionFactory.getCurrentSession();
        return session.createCriteria(type).list();
    }

    @Override
    public int countAll() {
        Session session = sessionFactory.getCurrentSession();
        return ((Long) session.createCriteria(type).setProjection(Projections.rowCount()).uniqueResult()).intValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public T findById(final K id) {
        Session session = sessionFactory.getCurrentSession();
        return (T) session.get(type, id);
    }

    @Override
    public void update(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.update(t);
    }

    @Override
    public void add(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.save(t);
    }

    @Override
    public void delete(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.delete(t);
    }

    @Override
    public void deleteAll() {
        Session session = sessionFactory.getCurrentSession();
        @SuppressWarnings("unchecked")
        List<T> result = (List<T>) session.createCriteria(type).list();
        for(T r : result) {
            session.delete(r);
        }
    }
}


Spring에서 SessionFactory를 @Autowired해서 사용할 예정입니다. 이제 applicationContext.xml을 작성하도록 하겠습니다. 

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">  
  <context:property-placeholder location="classpath:spring.properties" />
  <!--   Repository scan -->
  <context:component-scan base-package="com.xyzlast.bookstore.dao"/>
    
  <!-- BoneCP connection Pool DataSource 설정 -->
  <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
    <property name="driverClass" value="${connect.driver}" />
    <property name="jdbcUrl" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
    <property name="idleConnectionTestPeriodInMinutes" value="60" />
    <property name="idleMaxAgeInMinutes" value="240" />
    <property name="maxConnectionsPerPartition" value="30" />
    <property name="minConnectionsPerPartition" value="10" />
    <property name="partitionCount" value="3" />
    <property name="acquireIncrement" value="5" />
    <property name="statementsCacheSize" value="100" />
    <property name="releaseHelperThreads" value="3" />
  </bean>
  <!--   Spring에서 제공하는 Hibernate4용 SessionFactory 생성 -->
  <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:hibernate.cfg.xml"/>
    <!--     entity가 위치한 package를 scan -->
    <property name="packagesToScan" value="com.xyzlast.bookstore.entities"/>
  </bean>
  <!--   Hibernate4용 TransactionManager 설정 -->
  <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
  </bean>
  <!--   TransactionManager 등록 -->
  <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

@Configuration을 이용한 code base configuration은 다음과 같이 구성됩니다. 

@Configuration
@EnableTransactionManagement
@PropertySource("classpath:spring.properties")
@ComponentScan(basePackages = { "com.xyzlast.bookstore.dao", "com.xyzlast.bookstore.services", "com.xyzlast.bookstore.utils" })
public class HibernateBookStoreConfiguration {
    @Autowired
    private Environment env;

    @Autowired
    private SessionFactory sessionFactory;

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configHolder = new PropertySourcesPlaceholderConfigurer();
        return configHolder;
    }

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();
        dataSource.setUsername(env.getProperty("connect.username"));
        dataSource.setPassword(env.getProperty("connect.password"));
        dataSource.setDriverClass(env.getProperty("connect.driver"));
        dataSource.setJdbcUrl(env.getProperty("connect.url"));
        dataSource.setMaxConnectionsPerPartition(20);
        dataSource.setMinConnectionsPerPartition(3);
        return dataSource;
    }

    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
        properties.setProperty("hibernate.show_sql", env.getProperty("hibernate.show_sql"));

        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(dataSource());
        sessionFactory.setHibernateProperties(properties);
        sessionFactory.setPackagesToScan("com.xyzlast.bookstore.entities");

        return sessionFactory;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setSessionFactory(sessionFactory);
        return transactionManager;
    }

    // Hibernate를 이용하는 경우, 반드시 HibernateExceptionTranslator가 Bean에 등록되어야지 된다.
    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }
}


하나 재미있는 코드 구성이 Configuration에서 나오게 됩니다. 먼저, sessionFactory입니다. SessionFactory를 반환하는 것이 아닌, LocalSessionFactoryBean을 반환하게 됩니다. 이는 Spring ApplicationContext에서 FactoryBean을 등록시켜, 그 FactoryBean을 통한 재 생성이 되게 됩니다. 마치 prototype scope로 SessionFactory를 생성해서 사용하는 것 처럼, SessionFactory를 사용할 때마다 새로 반환시키는 구성이 되게 됩니다. 마지막으로 HibernateExceptionTranslator 입니다. Hibernate에서 반환되는 Exception을 Spring JDBCTemplate과 유사한 Exception으로 변경시키는 Bean입니다. 이 Bean이 등록되지 않으면 Exception이 발생되게 됩니다. 

약간 설정이 어려운 감이 없지는 않습니다. 그렇지만 기본적으로 DataSource를 만들어주고, 만들어진 DataSource를 기반으로 SessionFactory를 등록한다. 라는 기본 개념은 JdbcTemplate을 사용할때와 별반 차이가 없습니다. 
그리고, Hibernate를 사용하는 경우에는 CUD에 대한 처리가 매우 단순해지는 코드적 장점을 가지고 있으며, 무엇보다 DB의 객체 자체를 Domain의 Model로 대응시켜서 사용가능하다는 큰 장점을 가지고 있습니다. 꼭 사용법을 익혀두시길 바랍니다. 


Hibernate Criteria Examples

Hibernate로 하는 query문은 Criteria라는 객체지향적 문법으로 만들어집니다. 이 문법의 장점은 다음과 같습니다. 

# DB에 종속적이지 않은 문법을 만들어냅니다. : Criteria로 만들어진 코드는 Dialect에 의해서 DB의 종류에 맞는 SQL로 변환되어 실행됩니다.
# BL 로직을 표현하기 쉽다. : SQL이라는 DB의 언어에서 객체지향적 java code로 만들어진 문법은 알아보기가 쉽습니다.

다음은 Criteria 문법으로 표현되는 SQL query 문에 대한 예시들입니다.

Entity Select query

select * from SM_WAITING where openDate = '20120510' and phone_id = '123456' and enteranced = 'N' and deleted = 'N' order by seq;

        List<Waiting> waitings = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.openDate", yyyymmdd))
                .add(Restrictions.eq("phoneNumber", phoneNumber))
                .add(Restrictions.eq("entranced", false))
                .add(Restrictions.eq("deleted", false))
                .addOrder(Order.asc("id.sequence")).list();


Count Row

select count(*) from SM_WAITING where shopId = 'sm00000001' and openDate = '20120101' and seq > 3 and enteranced = 'N' and deleted = 'N';

        Long remainTeamCount = (Long) session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", waiting.getId().getShop()))
                .add(Restrictions.eq("id.openDate", waiting.getId().getOpenDate()))
                .add(Restrictions.lt("id.sequence", waiting.getId().getSequence()))
                .add(Restrictions.eq("entranced", false))
                .add(Restrictions.eq("deleted", false))
                .setProjection(Projections.rowCount()).uniqueResult();


Get MAX/MIN/SUM value

select max(seq) from SM_WAITING where shopId = 'sm00000001' and openDate = '20120101'

        Integer maxSeq = (Integer) session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", shop))
                .add(Restrictions.eq("id.openDate", yyyymmdd))
                .setProjection(Projections.max("id.sequence"))
                .uniqueResult();


Paging query (oracle 기준)

select * from (
         select rownum as rnum, name, address from Waiting where phonenumber = '1011234550'
    ) where rnum between (:PageNo * (:PageNo-1)) and ((:PageNo * (:PageNo-1)) + :PageSize )

        List<Waiting> waitings = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("phoneNumber", phoneNumber))
                .setFirstResult(pageIndex * pageSize).setMaxResults(pageSize)
                .list();


SubQuery

SELECT *
  FROM PIZZA_ORDER
 WHERE EXISTS (SELECT 1
                 FROM PIZZA
                WHERE PIZZA.pizza_size_id = 1
                  AND PIZZA.pizza_order_id = PIZZA_ORDER.pizza_order_id)

Criteria criteria = Criteria.forClass(PizzaOrder.class,"pizzaOrder");
DetachedCriteria sizeCriteria = DetachedCriteria.forClass(Pizza.class,"pizza");
sizeCriteria.add("pizza_size_id",1);
sizeCriteria.add(Property.forName("pizza.pizza_order_id").eqProperty("pizzaOrder.pizza_order_id"));
criteria.add(Subqueries.exists(sizeCriteria.setProjection(Projections.property("pizza.id"))));
List<pizzaOrder> ordersWithOneSmallPizza = criteria.list();


JOIN

select * from smWait w, smShop s where s.shop_name = 'ykyoon'

        List<Waiting> waits = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", shop))
                .list();


몇몇 대표적인 query들을 뽑아봤습니다. where 조건은 add로 Restrictions로 처리하고, count와 같은 계산 절은 Projections 로 처리가 되는 것을 알 수 있습니다. 여러 query 들을 한번 만들어보시길 바랍니다. 많이 익숙해질 필요가 있습니다. 


Posted by Y2K
,