06. 6주차. 데이터 모델 디자인
데이터 모델 디자인
해당 샘플 프로젝트는 사용자 로컬에 hsqldb가 설치되어 있음을 가정하고 작성하였다. hsqldb는 이클립스의 플러그인으로 구동하거나 hsqldb 홈페이지에서 다운받아 구동할 수 있다.
HSQLDB
GORM?
GORM(Grails' object relational mapping)은 Grails에서 구현된 ORM을 애기한다. 실제 구현은 Hibernate를 사용하고 있기때문에 사용자가 직접적으로 SQL을 사용할 필요가 적어 직관적으로 비지니스 로직 구현에 집중할 수 있고 Grovy를 사용하지 때문에 자바에서 구현할때보다 개발량이 줄어든다.
GORM을 살펴보기전에 Hibernate 사전 학습 및 Orm Framework에 대해 이해를 하기 위해 간단한 단일 객체의 CRUD 샘플을 만들어 본 후 동일한 객체를 Grails & GORM으로 작성해보겠다.
Hibernate의 단일 객체 CRUD
해당하는 hibernate기반의 sample 프로젝트는 maven 기반으로 database는 hsqldb, 구동은 jetty로 하는 것으로 구현 및 테스트 되었다. 해당 프로젝트는 hibernate_cat.zip으로 압축하여 첨부파일로 올려져있다.
hibernate의 설정파일인 hibernate.cfg.xml의 내용은 아래와 같다.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- Database connection settings --> <!-- JDBC connection pool (use the built-in) --> <property name="connection.pool_size">10</property> <!-- SQL dialect --> <property name="dialect">org.hibernate.dialect.HSQLDialect</property> <!-- Enable Hibernate's automatic session context management --> <property name="current_session_context_class">thread</property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <!-- Drop and re-create the database schema on startup --> <property name="hbm2ddl.auto">create</property> <property name="connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="connection.url">jdbc:hsqldb:hsql://localhost/mydatabase</property> <property name="connection.username">sa</property> <property name="connection.password" /> <mapping class="com.fastsystem.sample.hibernate.model.Cat" /> </session-factory> </hibernate-configuration>
기본적으로 일반적인 Database에 접속하는 다른 프레임워크와 다른 부분이 많이 없지만 Hibernate같은 경우는
개발자가 정의한 도메인에 따라서 데이터베이스 정의를 수정 및 삭제 후 재생성할 수 있는 기능이 있는데
hbm2ddl.auto 프로퍼티에 어떤 값을 주는지에 따라서 개발자가 원하는 대로 동작시킬 수 있다.
create,create-drop,update,validate 등이 있는데 자세한 내용 설명은 링크로 대체한다.
하이버네이트의 hbm2ddl.auto에 update가 좋을까? validate가 좋을까?
구현하려는 엔티티를 ERD로 표현하면 아래 이미지와 같다.
위의 Cat 엔티티를 Java로 구현하고 hibernate 문법을 적용하면 아래와 같은 소스가 된다.
package com.fastsystem.sample.hibernate.model; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.PrimaryKeyJoinColumns; import javax.persistence.Table; import org.hibernate.annotations.GenericGenerator; @Entity @Table(name = "CAT") public class Cat { @Id @GeneratedValue(generator="UUIDGenerator") @GenericGenerator(name="UUIDGenerator", strategy="com.fastsystem.sample.hibernate.util.UUIDGenerator") @Column(name="CAT_ID", columnDefinition = "CHAR(36)", length = 36, nullable = false) private String id; @Column(name="NAME", columnDefinition = "VARCHAR(16)", length = 16) private String name; @Column(name="SEX", columnDefinition = "CHAR(1)", length = 1) private char sex; @Column(name="WEIGHT", columnDefinition = "float") private float weight; public Cat() { } public Cat(String catId) { id = catId; } public String getId() { return id; } /* private void setId(String id) { this.id = id; } */ public String getName() { return name; } public void setName(String name) { this.name = name; } public char getSex() { return sex; } public void setSex(char sex) { this.sex = sex; } public float getWeight() { return weight; } public void setWeight(float weight) { this.weight = weight; } }
hihernate의 객체와 데이터베이스와의 Mapping은 위에서 사용한 Annotation을 사용하거나 별도의 Mapping Xml을 정의하여 적용할 수 있다. 각각 방식에 대한 방법은 링크로 대체한다.
XML Mapping
Annotation Mapping
GORM의 경우 스크립트 언어 특성상 모델의 클래스명과 테이블명이 일치하거나 프로퍼티명과 컬럼명이 일치하는 경우 생략할수도 있다. 해당 경우는 GORM CRUD 샘플에서 살펴보자.
Cat 객체를 CRUD 테스트를 위해 Service, Util Class들을 아래와 같이 생성했다.
package com.fastsystem.sample.hibernate.service; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import org.hibernate.HibernateException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.Transaction; import com.fastsystem.sample.hibernate.model.Cat; import com.fastsystem.sample.hibernate.util.HibernateUtil; public class CatService { public CatService() { } public boolean createCat(String name, char sex, float weight) { boolean resultBoolean = true; Session session = null; Transaction transaction = null; try { session = HibernateUtil.currentSession(); transaction = session.beginTransaction(); Cat cat = new Cat(); cat.setName(name); cat.setSex(sex); cat.setWeight(weight); session.save(cat); transaction.commit(); return resultBoolean; } catch (HibernateException e) { resultBoolean = false; if(transaction != null) { transaction.rollback(); } return resultBoolean; } finally { HibernateUtil.closeSession(); } } public Queue<Cat> getCatList(){ Session session = null; Transaction transaction = null; Queue<Cat> cats = new LinkedList<Cat>(); try { session = HibernateUtil.currentSession(); transaction = session.beginTransaction(); @SuppressWarnings("unchecked") List<Cat> list = session.createQuery("from Cat").list(); for (Iterator<Cat> iterator = list.iterator(); iterator.hasNext() ; ) { cats.add(iterator.next()); } transaction.commit(); } catch (HibernateException e) { e.printStackTrace(); if(transaction != null) { transaction.rollback(); } } finally { HibernateUtil.closeSession(); } return cats; } public Cat getCat(String catId) { Session session = null; Transaction transaction = null; Cat cat = null; session = HibernateUtil.currentSession(); transaction = session.beginTransaction(); try { //cat = (Cat) session.createQuery(" from Cat where cat_id = :catId ").setString("catId", catId).list().get(0); cat = (Cat) session.get(Cat.class, catId); transaction.commit(); } catch (Exception e) { e.printStackTrace(); if(transaction != null) { transaction.rollback(); } } finally { HibernateUtil.closeSession(); } return cat; } public void modifyCat(Cat cat) { Session session = null; Transaction transaction = null; session = HibernateUtil.currentSession(); transaction = session.beginTransaction(); try { //session.createQuery(" update Cat set name = :name, sex = :sex, weight = :weight where cat_id = :catid ").setProperties(cat); //cat = (Cat) session.get(Cat.class, cat); session.update(cat); transaction.commit(); } catch (Exception e) { e.printStackTrace(); if(transaction != null) { transaction.rollback(); } } finally { HibernateUtil.closeSession(); } } public void removeCat(String catId){ Session session = null; Transaction transaction = null; session = HibernateUtil.currentSession(); transaction = session.beginTransaction(); try { //session.createQuery(" delete Cat where cat_id = :catid ").setString("catId", catId); session.delete(session.get(Cat.class, catId)); transaction.commit(); } catch (Exception e) { e.printStackTrace(); if(transaction != null) { transaction.rollback(); } } finally { HibernateUtil.closeSession(); } } }
hibernate의 경우 sql을 작성하지 않아도 hibernate에서 sql을 생성하여 실행되지만 개발자가 직접 sql을 컨트롤 하고싶은 경우를 위해 hql과 Criteria Api를 제공한다. 위 Service 소스에 주석으로 막혀 있는 부분이 hql을 사용하여 구현한 부분이다. 위 문법 이외의 hql이나 Criteria에 대한 내용은 링크로 대체한다.
Hibernate를 이용한 ORM 7 - HQL과 Criteria를 이용한 조회
package com.fastsystem.sample.hibernate.util; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cfg.Configuration; public class HibernateUtil { private static final SessionFactory SESSION_FACTORY; private static final ThreadLocal<Session> THREAD_LOCAL = new ThreadLocal(); static { try { Configuration cfg = new Configuration().configure("hibernate.cfg.xml"); StandardServiceRegistryBuilder sb = new StandardServiceRegistryBuilder(); sb.applySettings(cfg.getProperties()); StandardServiceRegistry standardServiceRegistry = sb.build(); SESSION_FACTORY = cfg.buildSessionFactory(standardServiceRegistry); } catch (Throwable th) { System.err.println("Enitial SessionFactory creation failed" + th); throw new ExceptionInInitializerError(th); } } public static SessionFactory getSessionFactory() { return SESSION_FACTORY; } public static Session currentSession() throws HibernateException { Session session = THREAD_LOCAL.get(); if (session == null) { session = SESSION_FACTORY.openSession(); THREAD_LOCAL.set(session); } return session; } public static void closeSession() throws HibernateException { Session session = THREAD_LOCAL.get(); THREAD_LOCAL.set(null); if(session != null) { session.close(); } } }
위의 클래스는 hibernate 접속을 담당하는 util 클래스이다.
package com.fastsystem.sample.hibernate.util; import java.io.Serializable; import java.util.UUID; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.id.IdentifierGenerator; public class UUIDGenerator implements IdentifierGenerator{ public Serializable generate(SessionImplementor arg0, Object arg1) throws HibernateException { return UUID.randomUUID().toString(); } }
해당하는 클래스는 Cat 인스턴스를 생성할때 Pk인 Id를 생성해주는 클래스다.
실제 수행되는 화면은 아래와 같다.
생성
/hibernate_cat/create.jsp
보기
/hibernate_cat/list.jsp
수정, 삭제
/hibernate_cat/view.jsp
상당히 간단한 model1방식으로 구현되어 있으며 첨부파일로 올려져있는 hibernate_cat을 꼭 내려 받아 실제 구동을 통해 테스트해보기 바란다.
GORM의 단일 객체 CRUD
GORM의 경우도 위의 Cat 엔티티에 대한 CRUD를 구현해보겠다.
grails의 도메인 정의는 'grails-app/domain/'아래 정의해야 한다.
Cat이라는 파일로 Cat 엔티티를 도메인으로 정의해보겠다.
package grails_cat class Cat { String id; String name; String sex; float weight; static constraints = { name blank: false, nullable:false, size:1..16 sex blank: true, nullable:false, size:1..1 weight blank: false, nullable:false } static mapping = { id generator: 'uuid' version false } }
위의 hibernate에서의 도메인 모델 정의와 비교할 떄 소스코드가 상당히 많이 줄어든 것을 확인할 수 있다.
grails의 컨트롤러 정의는 'grails-app/controllers/'아래 정의해야 한다.
package grails_cat class CatController { def scaffold = true //CRUD 화면을 자동으로 생성해준다. //def index() { } //index 화면이 정의되어 있다면 동적으로 생성되지 않아 해당 정의를 막아야 정상적으로 리스트가 출력된다. }
그리고 DataSource의 경우 hibernate와 마찬가지로 hsqldb를 사용하기 위해 grails-app/conf/DataSource.grovy 파일을
아래와 같이 수정한다.
dataSource { pooled = true driverClassName = "org.hsqldb.jdbcDriver" username = "sa" password = "" } hibernate { cache.use_second_level_cache = true cache.use_query_cache = false cache.region.factory_class = 'net.sf.ehcache.hibernate.EhCacheRegionFactory' // Hibernate 3 // cache.region.factory_class = 'org.hibernate.cache.ehcache.EhCacheRegionFactory' // Hibernate 4 } // environment specific settings environments { development { dataSource { dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', '' url = "jdbc:hsqldb:hsql://localhost/mydatabase;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" } } test { dataSource { dbCreate = "update" url = "jdbc:hsqldb:hsql://localhost/mydatabase:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" } } production { dataSource { dbCreate = "create" url = "jdbc:hsqldb:hsql://localhost/mydatabase:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE" properties { maxActive = -1 minEvictableIdleTimeMillis=1800000 timeBetweenEvictionRunsMillis=1800000 numTestsPerEvictionRun=3 testOnBorrow=true testWhileIdle=true testOnReturn=false validationQuery="SELECT 1" jdbcInterceptors="ConnectionState" } } } }
특별히 설명할 부분은 없지만 'dbCreate'의 경우 hibernate의 'hbm2ddl.auto'와 같은 옵션이라고 보면 된다.
해당 grails를 구동한 화면은 아래와 같다.
grails의 home
CatController를 선택한다.
CatController의 list
CatController의 create
CatController의 show
CatController의 list
단순히 도메인 모델만 설정했을 뿐인데 CRUD까지 모두 생성되었다. 하지만 GORM이 어디에 사용되었는지
확인을 Static scaffolding 기능을 사용하여 해당 소스를 생성해보겠다.
grails command에 아래와 같이 수행한다.
grails generate-all grails_cat.Cat
프로젝트명과 설정한 도메인에 따라서 변경될 수 있다.
해당 커멘드가 수행되면 아래와 같이 변경된 CatController를 확인할 수 있다.
package grails_cat import static org.springframework.http.HttpStatus.* import grails.transaction.Transactional @Transactional(readOnly = true) class CatController { static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"] def index(Integer max) { params.max = Math.min(max ?: 10, 100) respond Cat.list(params), model:[catInstanceCount: Cat.count()] //READ } def show(Cat catInstance) { respond catInstance } def create() { respond new Cat(params) } @Transactional def save(Cat catInstance) { if (catInstance == null) { notFound() return } if (catInstance.hasErrors()) { respond catInstance.errors, view:'create' return } catInstance.save flush:true //CREATE request.withFormat { form { flash.message = message(code: 'default.created.message', args: [message(code: 'catInstance.label', default: 'Cat'), catInstance.id]) redirect catInstance } '*' { respond catInstance, [status: CREATED] } } } def edit(Cat catInstance) { respond catInstance } @Transactional def update(Cat catInstance) { if (catInstance == null) { notFound() return } if (catInstance.hasErrors()) { respond catInstance.errors, view:'edit' return } catInstance.save flush:true //UPDATE request.withFormat { form { flash.message = message(code: 'default.updated.message', args: [message(code: 'Cat.label', default: 'Cat'), catInstance.id]) redirect catInstance } '*'{ respond catInstance, [status: OK] } } } @Transactional def delete(Cat catInstance) { if (catInstance == null) { notFound() return } catInstance.delete flush:true //DELETE request.withFormat { form { flash.message = message(code: 'default.deleted.message', args: [message(code: 'Cat.label', default: 'Cat'), catInstance.id]) redirect action:"index", method:"GET" } '*'{ render status: NO_CONTENT } } } protected void notFound() { request.withFormat { form { flash.message = message(code: 'default.not.found.message', args: [message(code: 'catInstance.label', default: 'Cat'), params.id]) redirect action: "index", method: "GET" } '*'{ render status: NOT_FOUND } } } }
해당 소스를 보면 index,create,update,delete안에 hibernate에서 사용하는 메소드는 'list,save,delete'등이 사용된 것을 확인할 수 있다.
그리고 당연한 얘기지만 GORM도 HSQL을 수행할 수 있다.
http://grails.org/doc/2.2.4/ref/Domain%20Classes/executeQuery.html
해당 샘플은 grails_cat.zip으로 압축하여 첨부하였다.
GORM 관련 Api
Domain Class Usage