잊지 않겠습니다.

gradle이 major update를 할 때마다 querydsl Q-class들의 생성방법이 계속해서 바뀌고 있습니다. 이제는 좀 더 자연스러운 방법으로 처리가 되네요. 더 이상 추가 task를 만들 필요없이 지원이 가능합니다.

변경된 build.gradle입니다. lombok querydsl을 모두 적용한 상태입니다.

dependencies {
    annotationProcessor(
            "com.querydsl:querydsl-apt:${rootProject.ext.querydslVersion}:jpa",
            "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final",
            "javax.annotation:javax.annotation-api:1.3.2",
            "org.projectlombok:lombok"
    )
}

sourceSets {
    main.java.srcDirs += [ generated ]
}

tasks.withType(JavaCompile) {
    options.annotationProcessorGeneratedSourcesDirectory = file(generated)
}

clean.doLast {
    file(generated).deleteDir()
}

간단한 설명입니다. annotationProcessor는 compile시의 annotationProcessor를 gradle에서 자동으로 추가해줍니다. 이때 사용되는 각각의 jar를 추가해주면 됩니다. 기존의 generateQueryDsl을 전혀 할 필요가 없습니다. 다음 JavaCompile환경에 생성될 Q-class들의 위치를 지정해주고 그 위치를 sourceSets에 추가합니다. 마지막으로 clean으로 생성된 Q-class들을 제거해주는 코드를 넣어줍니다.

gradle에서 annotationProcessor를 지원함으로서 더욱 편하게 QueryDsl을 지원할 수 있게 되었습니다.

Posted by Y2K
,

queryDSL이 4.x대로 변경이 되면서 package명에 큰 변화가 생겼습니다. 기존 com.mysema.querydsl 에서 com.querydsl로 package명이 바뀌어서 간략화 되었지요.

기존 querydsl 처리 부분에 대한 build.gradle을 변경한다면 다음과 같습니다.


dependencies {
compile("org.springframework.boot:spring-boot-starter-web") {
exclude module: "spring-boot-starter-tomcat"
}
compile 'com.graphql-java:graphql-java:2.1.0'
compile("org.springframework.boot:spring-boot-starter-jetty")
compile("org.springframework.boot:spring-boot-starter-actuator")
testCompile("junit:junit")
testCompile("org.springframework.boot:spring-boot-starter-test")
compile 'com.querydsl:querydsl-apt:4.1.4'
compile 'com.querydsl:querydsl-jpa:4.1.4'
compile 'org.springframework.data:spring-data-jpa:1.10.4.RELEASE'
compile group: 'org.hibernate', name: 'hibernate-core', version: '5.2.3.Final'
compile "org.hibernate:hibernate-entitymanager:5.2.3.Final"
compile 'com.h2database:h2:1.4.187'
compile group: 'org.aspectj', name: 'aspectjrt', version: '1.8.9'
compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.8.9'
compileOnly "org.projectlombok:lombok:1.16.12"
}


sourceSets {
main {
java {
srcDirs 'src/main/java', 'src/main/generated'
}
}
}

task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
file(new File(projectDir, "/src/main/generated")).deleteDir()
file(new File(projectDir, "/src/main/generated")).mkdirs()
source = sourceSets.main.java
classpath = configurations.compile + configurations.compileOnly
options.compilerArgs = [
"-proc:only",
"-processor", "com.querydsl.apt.jpa.JPAAnnotationProcessor"
]
destinationDir = file('src/main/generated')
}

compileJava {
dependsOn generateQueryDSL
}

clean.doLast {
file(new File(projectDir, "/src/main/generated")).deleteDir()
}


기존의 generateQueryDSL task를 유지할 필요 없이, java compile option으로 QClass 생성을 넣어주는것으로 script를 간략화시키는것이 가능합니다. 또한 Q class가 존재할 때, 기존 Q Class를 지워주지 않으면 compile 시에 에러를 발생하게 되기 때문에 지워주는 작업역시 doFirst에 같이 실행하고 있습니다.

기존의 build.gradle보다 간략해보여서 전 개인적으로 맘에 드네요.

Posted by Y2K
,

Spring에서 xml이나 annotation을 이용한 component-scan에 의해서 등록되는 Bean중에서 @PersistenceContext의 동작원리는 다음과 같습니다.

ComponentScanBeanDefinitionParser

ComponentScanBeanDefinitionParser.registerComponents를 통해 Bean을 등록시켜준다. annotationConfig가 enable된 상태에서 context의 Bean들을 scan시켜줍니다.

protected void registerComponents(
        XmlReaderContext readerContext, Set<BeanDefinitionHolder> beanDefinitions, Element element) {

    Object source = readerContext.extractSource(element);
    CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), source);

    for (BeanDefinitionHolder beanDefHolder : beanDefinitions) {
        compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder));
    }

    // Register annotation config processors, if necessary.
    boolean annotationConfig = true;
    if (element.hasAttribute(ANNOTATION_CONFIG_ATTRIBUTE)) {
        annotationConfig = Boolean.valueOf(element.getAttribute(ANNOTATION_CONFIG_ATTRIBUTE));
    }
    if (annotationConfig) {
        Set<BeanDefinitionHolder> processorDefinitions =
                AnnotationConfigUtils.registerAnnotationConfigProcessors(readerContext.getRegistry(), source);
        for (BeanDefinitionHolder processorDefinition : processorDefinitions) {
            compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition));
        }
    }

    readerContext.fireComponentRegistered(compositeDef);
}

AnnotationConfigUtil

AnnotationConfigUtil을 통해 Annotation이 설정된 Bean들을 얻어오기 시작합니다. registerAnnotationConfigProcessors에 의해서 각각의 Type에 따라 다른 등록을 Process 처리하게 됩니다.

if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}

if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}

if (!registry.containsBeanDefinition(REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(RequiredAnnotationBeanPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}

// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.
if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}

// Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.
if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    RootBeanDefinition def = new RootBeanDefinition();
    try {
        ClassLoader cl = AnnotationConfigUtils.class.getClassLoader();
        def.setBeanClass(cl.loadClass(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME));
    }
    catch (ClassNotFoundException ex) {
        throw new IllegalStateException(
                "Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);
    }
    def.setSource(source);
    beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));
}

처리되는 Bean들은 다음과 같습니다.

  • CONFIGURATIONANNOTATIONPROCESSORBEANNAME : @Configuration이 적용된 객체들을 처리하는 Process 입니다.
  • AUTOWIREDANNOTATIONPROCESSORBEANNAME : Bean 객체들의 @Autowired가 적용된 객체들을 처리하는 Process 입니다.
  • REQUIREDANNOTATIONPROCESSORBEANNAME : @Required Annotation이 처리됩니다.
  • COMMONANNOTATIONPROCESSORBEANNAME : EJB, JNDI 지원 객체들의 Property들을 처리하는 Process 입니다.
  • PERSISTENCEANNOTATIONPROCESSORBEANNAME : JPA를 처리합니다. 실질적으로 JPA의 EntityManagerFactory 객체의 경우에는 PersistenceAnnotationBeanPostProcess 에서 처리하게 됩니다.
PersistenceAnnotationBeanPostProcess

이제 마지막 코드입니다. PersistenceAnnotationBeanPostProcess는 @PersistenceContext로 지정된 Property에 EntityManagerFactory에서 새로운 EntityManager를 생성하거나, Transaction에 의해 기존ㅇ에 생성된 EntityManager를 반환시켜서 사용하게 해줍니다. 이에 대한 코드는 다음과 같습니다.

먼저, EntityManagerFactory를 생성하고 주입하고 있습니다.

public PersistenceElement(Member member, PropertyDescriptor pd) {
    super(member, pd);
    AnnotatedElement ae = (AnnotatedElement) member;
    PersistenceContext pc = ae.getAnnotation(PersistenceContext.class);
    PersistenceUnit pu = ae.getAnnotation(PersistenceUnit.class);
    Class<?> resourceType = EntityManager.class;
    if (pc != null) {
        if (pu != null) {
            throw new IllegalStateException("Member may only be annotated with either " +
                    "@PersistenceContext or @PersistenceUnit, not both: " + member);
        }
        Properties properties = null;
        PersistenceProperty[] pps = pc.properties();
        if (!ObjectUtils.isEmpty(pps)) {
            properties = new Properties();
            for (PersistenceProperty pp : pps) {
                properties.setProperty(pp.name(), pp.value());
            }
        }
        this.unitName = pc.unitName();
        this.type = pc.type();
        this.synchronizedWithTransaction = (synchronizationTypeAttribute == null ||
                "SYNCHRONIZED".equals(ReflectionUtils.invokeMethod(synchronizationTypeAttribute, pc).toString()));
        this.properties = properties;
    }
    else {
        resourceType = EntityManagerFactory.class;
        this.unitName = pu.unitName();
    }
    checkResourceType(resourceType);
}

다음에, 각 Resource에 EntityManager를 주입합니다.

@Override
protected Object getResourceToInject(Object target, String requestingBeanName) {
    // Resolves to EntityManagerFactory or EntityManager.
    if (this.type != null) {
        return (this.type == PersistenceContextType.EXTENDED ?
                resolveExtendedEntityManager(target, requestingBeanName) :
                resolveEntityManager(requestingBeanName));
    }
    else {
        // OK, so we need an EntityManagerFactory...
        return resolveEntityManagerFactory(requestingBeanName);
    }
}

최종적으로 얻어지는 EntityManager는 resolveEntityManager method를 통해서 주입되게 됩니다. EntityManager의 코드는 다음과 같습니다.

private EntityManager resolveEntityManager(String requestingBeanName) {
    // Obtain EntityManager reference from JNDI?
    EntityManager em = getPersistenceContext(this.unitName, false);
    if (em == null) {
        // No pre-built EntityManager found -> build one based on factory.
        // Obtain EntityManagerFactory from JNDI?
        EntityManagerFactory emf = getPersistenceUnit(this.unitName);
        if (emf == null) {
            // Need to search for EntityManagerFactory beans.
            emf = findEntityManagerFactory(this.unitName, requestingBeanName);
        }
        // Inject a shared transactional EntityManager proxy.
        if (emf instanceof EntityManagerFactoryInfo &&
                ((EntityManagerFactoryInfo) emf).getEntityManagerInterface() != null) {
            // Create EntityManager based on the info's vendor-specific type
            // (which might be more specific than the field's type).
            em = SharedEntityManagerCreator.createSharedEntityManager(
                    emf, this.properties, this.synchronizedWithTransaction);
        }
        else {
            // Create EntityManager based on the field's type.
            em = SharedEntityManagerCreator.createSharedEntityManager(
                    emf, this.properties, this.synchronizedWithTransaction, getResourceType());
        }
    }
    return em;
}

항시 SharedEntityManager를 이용해서 @Transactional annotation과 같이 사용할 수 있도록 동작하고 있는 것을 볼 수 있습니다. 코드를 보시면 아시겠지만, JPA를 사용하실 때는 반드시 @PersistenceContext annotation을 이용해야지 됩니다.

Posted by Y2K
,

기본적인 CRUD를 지원하는 GenericDAO를 구성하는 코드의 비교를 해볼려고 합니다.

일단 기술 배경으로는 Spring을 사용하고, Spring내에서의 Hibernate, JPA, 그리고 Hibernate를 이용한 queryDSL 코드간의 GenericDao를 비교해보고자 합니다.

기본적으로 Entity는 BaseEntity를 상속하는 것을 원칙으로 갖습니다. DB에서 생성되는 단일키를 사용하는 것으로 기본 구상을 잡고 들어가면 GenericDao의 interface는 다음과 같이 구성될 수 있습니다.

public interface EntityDao<T extends BaseEntity> {
    List<T> getAll();
    void deleteAll();
    T getById(int id);
    void add(T entity);
    void update(T entity);
    int countAll();
}

이제 이 interface를 구현한 AbstractDao class를 각 3개의 기술로 따로 구현해보고자 합니다.

Hibernate

Hibernate는 SessionFactory를 통해 생성되는 Session을 기반으로 구성합니다. Spring을 이용하기 때문에 SessionFactory.getCurrentSession() method를 이용해서 Session을 구해와서 처리합니다.

약간의 특징이라고 할 수 있는 것이 Criteria를 이용하는 코드의 경우, Class<?>를 반드시 각 EntityDao 생성 객체에서 넘겨주는 것이 필요합니다.

public abstract class AbstractSessionFactoryDao<T extends BaseEntity> implements EntityDao<T> {

    @Autowired
    protected SessionFactory sessionFactory;
    private final Class<?> clazz;

    protected AbstractSessionFactoryDao(Class<?> clazz) {
        this.clazz = clazz;
    }


    @Override
    public List<T> getAll() {
        Session session = sessionFactory.getCurrentSession();
        return session.createCriteria(clazz).list();
    }

    @Override
    public void deleteAll() {
        List<T> items = getAll();
        Session session = sessionFactory.getCurrentSession();
        for (T item : items) {
            session.delete(item);
        }
    }

    @Override
    public T getById(final int id) {
        Session session = sessionFactory.getCurrentSession();
        return (T)session.get(clazz, id);
    }

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

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

    @Override
    public int countAll() {
        Session session = sessionFactory.getCurrentSession();
        Long count = (Long) session.createCriteria(clazz)
                .setProjection(Projections.rowCount())
                .uniqueResult();

        if(count == null) {
            return 0;
        } else {
            return count.intValue();
        }
    }
}
JPA

jpa를 이용하는 경우, @PersistenceContext 를 이용해서 EntityManager를 등록해서 사용해주면 됩니다. Spring은 @PersistenceContext를 확인하면, 등록된 EntityManagerFactoryBean은 EntityManager를 사용할때마다 EntityManagerFactory에서 얻어와서 사용하게 됩니다. 이는 Spring에서 핸들링시켜줍니다. 이에 대한 코드 분석은 다음 Post에서 한번 소개해보도록 하겠습니다.

JPA를 이용한 GenericDao class의 코드는 다음과 같습니다.

public abstract class AbstractJpaDao<T extends BaseEntity> implements EntityDao<T> {

    protected AbstractJpaDao(String entityName) {
        this.entityName = entityName;
    }

    @PersistenceContext
    protected EntityManager em;

    @Autowired
    protected JpaExecutor executor;
    protected final String entityName;

    @Override
    public List<T> getAll() {
        return em.createQuery("from " + entityName).getResultList();
    }

    @Override
    public void deleteAll() {
        System.out.println(em);
        em.createQuery("delete from " + entityName).executeUpdate();
    }

    @Override
    public T getById(final int id) {
        Query query = em.createQuery("from " + entityName + " where id = :id");
        query.setParameter("id", id);
        return (T) query.getSingleResult();
    }

    @Override
    public void add(final T entity) {
        em.merge(entity);
    }

    @Override
    public void update(final T entity) {
        em.merge(entity);
    }

    @Override
    public int countAll() {
        return em.createQuery("from " + entityName).getResultList().size();
    }
}
Hibernate기반의 queryDSL

GenericDAO class를 만들기 위해서 QueryDsl에서 가장 문제가 되는 것은 QClass들의 존재입니다. Generator에서 생성되는 Q Class들은 EntityPathBase class에서 상속받는 객체들입니다. 따라서 다른 GenericDAO class 들과는 다르게 2개의 Generic Parameter를 받아야지 됩니다.

다음은 queryDSL을 이용한 GenericDAO code입니다.

public abstract class AbstractQueryDslDao<T extends BaseEntity, Q extends EntityPathBase<T>> implements EntityDao<T> {

    @Autowired
    protected SessionFactory sessionFactory;
    protected final Q q;
    protected final Class<?> clazz;

    protected AbstractQueryDslDao(Class<?> clazz, Q q) {
        this.clazz = clazz;
        this.q = q;
    }

    protected HibernateQuery getSelectQuery() {
        return new HibernateQuery(sessionFactory.getCurrentSession()).from(q);
    }

    protected HibernateDeleteClause getDeleteQuery() {
        return new HibernateDeleteClause(sessionFactory.getCurrentSession(), q);
    }

    protected HibernateUpdateClause getUpdateQuery() {
        return new HibernateUpdateClause(sessionFactory.getCurrentSession(), q);
    }


    @Override
    public List<T> getAll() {
        HibernateQuery query = getSelectQuery();
        return query.list(q);
    }

    @Override
    public void deleteAll() {
        getDeleteQuery().execute();
    }

    @Override
    public T getById(int id) {
        return (T)sessionFactory.getCurrentSession().get(clazz, id);
    }

    @Override
    public void add(T entity) {
        sessionFactory.getCurrentSession().saveOrUpdate(entity);
    }

    @Override
    public void update(T entity) {
        sessionFactory.getCurrentSession().saveOrUpdate(entity);
    }

    @Override
    public int countAll() {
        Long count = getSelectQuery().count();
        return count.intValue();
    }
}

개인적으로는 JPA에서 사용하는 JPQL의 문법이 조금 맘에 안듭니다. 거의 SQL query문을 객체를 이용해서 사용한다는 느낌만 들고 있기 때문에, 오타 등의 문제에서 자유롭지가 않습니다. 그런 면에서는 queryDSL을 이용하는 것이 가장 좋은 것 같지만 queryDSL의 경우에는 처음 초기 설정이 조금 까다롭습니다. 같이 일하는 팀원중 이런 설정을 잘 잡는 사람이 있으면 괜찮지만…

다음에는 글 내용중 언급한것처럼 @PersistenceContext Autowiring에 대해서 코드 분석을 해볼까합니다. ^^ 아무도 안오는 Blog지만요. ^^

Posted by Y2K
,

java에서 가장 대표적인 ORM인 Hibernate와 Hibernate 기반의 JPA연결 방법에 대해서 알아보도록 하겠습니다.

먼저, 모든 연결 방법은 Hibernate를 기반으로 구성되어 있습니다. 이는 Hibernate의 사상이 JPA의 기본이 되어버린 것이 큽니다. 따라서, Spring을 이용한 ORM의 접근방법이라고 하는 것이 대부분 Hibernate에 대한 연결이 되는 것이 일반적입니다.

Hibernate

Hibernate를 사용하기 위해서는 Configuration에 다음 2가지가 등록되어야지 됩니다.

  1. LocalSessionFactoryBean : SessionFactory에 대한 Factory Bean입니다. SessionFactory를 생성하는 객체를 등록시켜줍니다. 이는 Spring에서 사용할 DataSource와 Entity가 위치한 Package들에 대한 검색을 모두 포함하게 됩니다.
  2. HibernateTransactionManager : PlatformTransactionManager를 구현한 Hibernate용 TransactionManager를 등록해야지 됩니다. 이는 Spring에서 @EnableTransactionManager와 같이 사용되어 @Transactional annotation을 사용할 수 있게 됩니다.
LocalSessionFactory

SessionFactory를 생성하는 FactoryBean입니다. hibernate.cfg.xml 에서 설정되는 내용이 여기에 들어가게 됩니다.

  • Hibernate의 property를 등록합니다.
  • DataSource를 등록시킵니다.
  • Entity가 위치한 Package를 지정합니다.
    @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"));
        properties.setProperty("hibernate.format_sql", env.getProperty("hibernate.format_sql"));

        LocalSessionFactoryBean localSessionFactoryBean = new LocalSessionFactoryBean();
        localSessionFactoryBean.setDataSource(dataSource());
        localSessionFactoryBean.setHibernateProperties(properties);

        localSessionFactoryBean.setPackagesToScan(new String[] { "me.xyzlast.bh.entities" });
        return localSessionFactoryBean;
    }
HibernateTransactionManager

Transaction을 사용하기 위한 PlatformTransactionManager interface의 구현체를 등록합니다.

  • SessionFactory와 같이 사용될 DataSource를 등록합니다.
  • SessionFactoryBean에서 생성되는 SessionFactory를 등록시켜줍니다.
    @Bean
    public PlatformTransactionManager transactionManager() {
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setSessionFactory(sessionFactory().getObject());
        transactionManager.setHibernateManagedSession(false);
        return transactionManager;
    }

JPA

Hibernate 기반의 JPA를 등록하기 위해서는 Hibernate-JPA support bean이 필요합니다. 이에 대한 maven repository의 gradle 설정은 다음과 같습니다.

    compile 'org.hibernate:hibernate-entitymanager:4.3.1.Final'

Spring에서 Hibernate기반의 JPA를 사용하기 위해서는 다음 Bean들이 필요합니다.

  1. LocalContainerEntityManagerFactoryBean : SessionFactoryBean과 동일한 역활을 맡습니다. SessionFactoryBean과 동일하게 DataSource와 Hibernate Property, Entity가 위치한 package를 지정합니다. 또한 Hibernate 기반으로 동작하는 것을 지정해하는 JpaVendor를 설정해줘야지 됩니다.
  2. HibernateJpaVendorAdapter : Hibernate vendor과 JPA간의 Adapter를 설정합니다. 간단히 showSql 정도의 Property만을 설정하면 됩니다. 매우 단순한 code입니다.
  3. JpaTransactionManager : DataSource와 EntityManagerFactoryBean에서 생성되는 EntityManagerFactory를 지정하는 Bean입니다.
LocalContainerEntityManagerFactoryBean

Hibernate에서 SessionFactoryBean과 동일한 역활을 담당하는 FactoryBean입니다. EntityManagerFactory를 생성하는 FactoryBean형태입니다. @Configuration 선언은 다음과 같습니다.

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setJpaVendorAdapter(hibernateJpaVendorAdapter());
        entityManagerFactoryBean.setDataSource(dataSource());
        entityManagerFactoryBean.setPackagesToScan("me.xyzlast.bh.entities");
        // NOTE : Properties를 이용해서 Hibernate property를 설정합니다.
        // entityManagerFactoryBean.setJpaProperties();
        return entityManagerFactoryBean;
    }
HibernateJpaVendorAdapter

JPA 규약과 Hibernate간의 Adapter 입니다. 특별히 설정할 내용은 존재하지 않고, showSQL 정도의 설정만이 존재합니다.

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

JPA를 지원하는 TransactionManager를 등록합니다. DataSource와 EntityManagerFactory를 등록시켜주면 됩니다.

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


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 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



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

그 부분은 각 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
,