DB연동 #9 JPA(Java Persistence API) 활용하기 3 Java/Spring

JPA의 생산성과 효율성에 대하여 가장 크게 부각되는 경우는 여러개의 복잡하게 foreign key로 연결된 테이블의 데이터 관리의 경우이다 
예를 들어 아래와 같은 ERD로 표현된 학년등급(초 | 중 |  고 | 대), 학교, 학생의 3가지 테이블이 있고 각 테이블은 Foreign Key로 연결되어 있는 경우를 생각해 보자 

학교는 학년등급에 대하여 Foreign Key로 연결되고, 학생은 소속학교에 대하여 Foreign Key로 연결된다 
이를 DDL로 표현하면 아래와 같다 

CREATE TABLE GRADE_TB
(
        grade int NOT NULL,
        descript varchar(10),
        PRIMARY KEY (grade),
        UNIQUE (grade)
);
CREATE TABLE SCHOOL_TB
(
        Id bigint NOT NULL AUTO_INCREMENT,
        Name varchar(10),
        Address varchar(20),
        Edu_grade int NOT NULL,
        PRIMARY KEY (Id),
        UNIQUE (Id),
        UNIQUE (Edu_grade)
);
ALTER TABLE SCHOOL_TB
        ADD FOREIGN KEY (Edu_grade)
        REFERENCES GRADE_TB (grade)
        ON UPDATE RESTRICT
        ON DELETE RESTRICT
;
CREATE TABLE STUDENT_TB
(
        Id varchar(8) NOT NULL,
        Name varchar(8),
        Age int,
        School_Id bigint NOT NULL,
        PRIMARY KEY (Id),
        UNIQUE (Id),
        UNIQUE (School_Id)
);
ALTER TABLE STUDENT_TB
        ADD FOREIGN KEY (School_Id)
        REFERENCES SCHOOL_TB (Id)
        ON UPDATE RESTRICT
        ON DELETE RESTRICT
;
이제 학생의 정보와 학생이 다니고 있는 학교명을 같이 검색하고자 하면 Join을 동반한 복잡한 쿼리문을 작성해야 한다 
STUDENT_TB 테이블내의 School_Id 컬럼은 SCHOOL_TB의 Id 컬럼과 Foreign Key로 연결되며 학교명은 SCHOOL_TB에 descript로 저장되기 때문이다 
아래는 쿼리문과 이에 대한 결과이다 
SQL > select student.Id, student.Name, student.Age, school.Name from STUDENT_TB student join SCHOOL_TB school on student.school_Id = school.Id;

이를 JPA에서는 어떻게 간단하게 해결하는지 알아보자  

우선 가장 상위 레벨에 있는 GRADE_TB와 맵핑되는 클래스를 아래와 같이 작성해 보자

@Entity
@Table(name="GRADE_TB")
public class GradeJpaVO implements Serializable {
          private static final long serialVersionUID = 1L;

          @Id
          private  int grade;                   
          private                 String descript;
          public GradeJpaVO() {
                   super();
          }
          public int getGrade() {
                   return grade;
          }
                :
                : 
          public void setDescript(String descript) {
                   this.descript = descript;
          }
          
          @Override
          public String toString(){
                   return "grade: " + grade + " | " + "descript: " descript;
          }
}

다음은 GRADE_TB의 grade 를 Foreign Key로 하는 SCHOOL_TB의 맵핑 클래스를 작성해 보자

@Entity
@Table(name="SCHOOL_TB")
public class SchoolJpaVO implements Serializable {
          
          private static final long serialVersionUID = 1L;
          
          @Id
          private                 long   Id;
          private                 String name;
          private                 String address;
          
          @ManyToOne
          @JoinColumn(name="Edu_grade" , insertable=false, updatable=false)
          GradeJpaVO gradeVO;

          public SchoolJpaVO() {
                   super();
          }
          public long getId() {
                   return Id;
          }
                                :
                                :
          public String getGrade() {
                   return gradeVO.getDescript();
          }
          
          @Override
          public String toString() {
                   return "Id: " + Id + " | " + "name: " name + "grade:" + getGrade();
          }
}

위에서 눈여겨 봐야 할 부분이 아래와 같은 @ManyToOne 이라는 어노테이션이다 
그리고 그 밑에 @JoinColumn 어노테이션을 통하여 어떤 Foreign Key로 연결되는 지를 알려주고 그 아래에 GradeJpaVO를 선언하고 있다
GradeJpaVO는 GRADE_TB 라는 테이블과 맵핑되어 있다고 이미 정의되어 있으므로 추가적으로 어느 테이블의 Foreign Key와 연결된다고 지시해줄 필요가 없다 

         @ManyToOne
          @JoinColumn(name="Edu_grade" , insertable=false, updatable=false)
          GradeJpaVO gradeVO;

그리고 gradeVO 인스턴스를 통하여 그 클래스내에 정의된 함수를 아래와 같이 사용할 수 있다 
          public String getGrade() {
                   return gradeVO.getDescript();
          }

최종적으로 SCHOOL_TB의 Id를 Foreign Key로 하는 STUDENT_TB의 클래스를 살펴보면 위와 같은 구조임을 알 수있다 

@Entity
@Table(name="STUDENT_TB")
public class StudentJpaVO implements Serializable {
          
          private static final long serialVersionUID = 1L;
          
          @Id
          private                 String Id;
          private                 String Name;
          private                 int               Age;
          @ManyToOne
          @JoinColumn(name="School_Id" , insertable=false, updatable=false)
          SchoolJpaVO                   school;
          public StudentJpaVO() {
                   super();
          }
          public String getId() {
                   return Id;
          }
          public void setId(String id) {
                   Id = id;
          }
          
          public String getSchoolName() {
                   return school.getName();
          }
  
          @Override
          public String toString() {
                   return "Id:" + Id + "name: " + Name + "school:" + getSchoolName();
          }
}

myBatis에서는 아래와 같이 Join 쿼리문으로 통하여 결과데이터를 VO 객체에 맵핑해줘야 하지만 JPA는 아래와 같은 쿼리문 없이 클래스의 정의만으로 Foreign Key로 연결된 테이블의 정보를 모두 얻어올 수 있다 
Query > select student.Id, student.Name, student.Age, school.Name from STUDENT_TB student join SCHOOL_TB school on student.school_Id = school.Id;

DB 테이블간의 관계구조가 복잡하면 복잡할수록 JPA의 생산성과 효율성에 매료되어 감탄을 넘어 감동에 이른다 




DB연동 #8 JPA(Java Persistence API) 활용하기 2 Java/Spring

JPA 가 앞서 설명에서 보여준대로 SQL문의 작성이 필요없고 JAVA 객체와 테이블의 데이터가 맵핑되므로 편하기는 하지만 대가는 따르기 마련이다 

DBMS의 테이블과 이를 맵핑할 JAVA 클래스의 Property를 섬세하게 맞춰주지 않으면 어플리케이션 구동 시점부터 부터 오류가 발생한다 

아래는 필자가 경험한 오류이다 

DB의 테이블 스키마는 아래와 같다 


이에 대한 맵핑 클래스의 Property는 아래와 같다  


          @Id
          private String code = "";                    
          @Column
          private String name = "";                  
          
          @Column(name="open_status", columnDefinition = "TINYINT", length=1)
          private int open_status;          
          
          @Transient
          private String strOpenStatus = "";
          
          @Temporal(TemporalType.TIMESTAMP)
          private Date open_date = null;                                              
                        :
                        :
          public String getStrOpenDate() {
                   return  new SimpleDateFormat("yyyy-MM-dd", Locale.KOREA).format(this.open_date);
          }

            
여기서 눈여겨 봐야 할 부분은 세번째 Column의 open_status 이다 
MySql의 tinyint 형으로 정의해 놓았는데 이를 클래스의 Propety에서는 별도의 columnDefinition = "TINYINT" 로 정의해 주어야 한다

두번째로 눈여겨 봐야 할 부분은 @Temporal 어노테이션이다
테이블의 open_date는 MySQL의 datetime 타입인데 이를 @Temporal 어노테이션을 통하여 Java의 java.util.Date 타입으로 맵핑하도록 하였다 
사용법은  @Temporal(TemporalType.TIMESTAMP) 이다

날짜의 경우 본 예제에서는 DB 테이블에서는 datetime 타입에 대해 JAVA 클래스에서 java.util.Date 타입으로 맵핑했다  그리고 웹페이지 출력을 위하여 아래와 같이 별도 함수를 통하여 String으로 변환하여 전달하는 함수를 작성하여 해결했다 

          public String getStrOpenDate() {
                   return  new SimpleDateFormat("yyyy-MM-dd", Locale.KOREA).format(this.open_date);
          }

마지막으로 눈여겨 봐야 할 부분은 @Transient 어노테이션이다 
클래스의 일부 Property를 테이블의 Column과 맵핑할 필요가 없는 경우에 사용한다 
Property에 대하여 아무런 어노테이션이 없으면 JPA는 무조건 테이블의 Column과 맵핑을 시도하며 오류를 나타내다 

위와같이 설정하고 구동으로 해도 아래와 같은 오류를 만나게 된다

Hibernate.tool.schema.spi.SchemaManagementException : wrong column type encountered ....   found [bit (Types#BIT)], but expecting[tiny (Types#INTEGER)] 

이를 해결하기 위해서는 DB접속 URL에서의 옵션설정에서 tinyint에 대하여 아래와 같이 지정해 주어야 한다 



편한 만큼 신경써줘야 하는 부분이 테이블의 각 Column과 클래스의 Property라는 점을 다시한번 상기하며 맵핑관련하여 몇가지 알아두어야 할 사항을 아래와 같이 정리하였다

 < DB와 Java 자료형 대입표 >


< 어노테이션 설정 내용 >

@Entity: JPA에서 테이블에 매핑할 클래스에 붙임. 해당 클래스는 엔티티라 부른다.
기본생성자(default constructor)는 필수
final class, enum, interface, inner class에 사용 불가. final 필드 사용 불가
-> runtime시에 javassist에 의해 Entity의 서브클래스 생성하기 때문. 클래스 상속 불가하면 안됨
@Table: 엔티티와 매핑할 테이블명을 지정
@Id: 기본키(primary Key) 매핑
@Column: 테이블의 컬럼명 매핑. 지정안하는 경우 객체 필드명과 동일하게 지정
@Enumerated: 자바의 enum 타입을 사용하는 경우 지정
@Temporal: 날짜 타입(Data, Calendar) 매핑시 사용. (DATE, TIME, TIMESTAMP)
@Lob: CLOB, BLOB 타입과 매핑시
@Transient: 테이블 컬럼에 매핑되지 않는 필드에 지정.
@Access: JPA가 엔티티 데이터에 접근하는 방식 지정. FILED가 기본값. 필드(FILED) or Getter(PROPERTY). 




DB연동 #7 JPA(Java Persistence API) 활용하기 1 Java/Spring

지금까지 myBatis의 ORM 기술을 익혔다면 이제는 JPA의 ORM 기술을 익혀보자 

순서는 아래와 같다 

1. pom.xml 에 dependency 추가하기

                <dependency>
                        <groupId>org.hibernate</groupId>
                        <artifactId>hibernate-entitymanager</artifactId>
                        <version>5.1.0.Final</version>
                </dependency>

                <dependency>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-orm</artifactId>
                        <version>${org.springframework-version}</version>
                </dependency>

추가적으로 spring.framework-version을 아래와 같이 수정하였다 

- 수정 전 - 
<org.springframework-version>4.2.4.RELEASE</org.springframework-version>
 
- 수정 후  - 
<org.springframework-version>5.0.4.RELEASE</org.springframework-version>

안그러면 아래와 같은 에러를 나중에 만나게 된다

Error creating bean with name 'entityManagerFactory' defined in class path resource [context_xml/DBConfig.xml]
nested exception is java.lang.NoClassDefFoundError: org/springframework/context/index/CandidateComponentsIndexLoader 

Spring Bean Config XML인 DBConfig.xml 파일내에 entityManagerFactory에 대한 Bean 생성이 안된다는 얘기인데 왜그럴까 찾아보니 아래 사이트에 답이 있었다 




2. Project > Properties > Project facets 설정 
  JPA 부분을 체크

  Apply 버튼으로 적용하고 나면 자동으로 src/main/resources/META-INF  아래에 persistence.xml 파일이 아래와 같이 자동으로 생성된다


자동으로 생성된 persistence.xml 파일을 열어보면 아래와 같다
여기에서 persistence-unit의 name에 대한 대입내용은 프로젝트 명이다 

<?xml version="1.0" encoding="UTF-8"?>
    <persistence-unit name="DBConnect">
    </persistence-unit>
</persistence>
3. Spring Bean Config XML  에  JPA관련 설정내용 추가하기 

        <import resource="DbcpSource.xml" />
        <import resource="C3p0Source.xml" />
                    :
                    :    
        <bean id="jpaVendorAdapter"
                class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        </bean>
        <bean id="entityManagerFactory"
                class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
                        <property name="dataSource" ref="mysqlC3P0Source" />
                        <property name="jpaVendorAdapter" ref="jpaVendorAdapter" />
        </bean>

    myBatis에서는 sqlSessionFactory 클래스의 Bean이 필요했다면 JPA는 entityManagerFactory가 필요하다  

    위에서 이전에 작성했던 C3P0Source.xml 의 내용을 상기하면 Multi Connection으로 다수의 Connection을 사용하는 구조이며 이를 기반으로 하는 JPA 설정 내용이다   

4. VO 객체 만들기


위 메뉴를 통하여 AdminJpaVO.java  클래스 생성
이렇게 VO 클래스를 작성하면 자동적으로 src/main/resources/META-INF 의 디렉토리에 있는 persistence.xml 파일에 아래와 같이 AdminJpaVO클래스가 자동적으로 추가된것을 볼 수 있다
그리고 여기에 <properties> 태그를 통하여 Hibernate 관련 property 설정내용을 추가하도록 한다 

  <persistence-unit name="DBConnect">
                <class>com.dbconn.VO.AdminJpaVO</class>
                <properties>
                        <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
                        <property name="hibernate.show_sql"  value="true" />
                        <property name="hibernate.format_sql"  value="true" />
                        <property name="hibernate.use_sql_comments" value="false"  />
                        <property name="hibernate.id.new_generator_mapping" value="true"  /> 
                        <property name="hibernate.hbm2ddl.auto"  value="validate"/>
                </properties>
        </persistence-unit>


이제 AdminJpaVO.java 소스를 아래와 같이 DB의 Field 구조에 맞게 Property 및 각각의 Property에 대한 getXXXX와 setXXXX 함수를 작성한다 
@Table 어노테이션은 맵핑할 DBMS의 테이블명이다   이렇게 설정하면 명시한 테이블과 동일한 구조의 VO 클래스로써 자동으로 테이블의 데이터를 맵핑해준다 
아래의 @Id 는 Unique Key를 의미하며, @Column 어너테이션은 AdminJpaVO클래스 내에 있는 멤버 Property의 String 타입의 경우 테이블의 Field Type을 디폴트로 Varchar로 인식하지만, 현재 테이블의 userId는 Char 타입이므로 별도의 Column 타입 지정을 위해 columnDefinition으로 "char"을 지정하였다 

@Entity
@Table(name="ADMIN_TB")
public class AdminJpaVO implements Serializable {
          
          private static final long serialVersionUID = 1L;
          
          @Id
          @Column(columnDefinition = "char")
          private String userId;
          private String password;
          private String name;
          private String email;
          public AdminJpaVO() {
                   super();
          }
          public String getUserId() {
                   return userId;
          }
          public void setUserId(String userId) {
                   this.userId = userId;
          }
          public String getPassword() {
                   return password;
          }
          public void setPassword(String password) {
                   this.password = password;
          }
          public String getName() {
                   return name;
          }
          public void setName(String name) {
                   this.name = name;
          }
          public String getEmail() {
                   return email;
          }
          public void setEmail(String email) {
                   this.email = email;
          }
          
          @Override
          public String toString(){
                   return userId + " | " + password + " |  " + name + " | " + email + " | ";
          }
  
4.  DAO 클래스 만들기 
    이제 테이블에 맵핑할 VO 클래스를 만들었으니 DBMS에 요청할 DAO 클래스를 작성하도록 한다 

        @Repository("adminJpaDAO")
        public class AdminJpaDAO {
                   @PersistenceContext
                   private                 EntityManager        em;
                   
                   public void insertAdmin(AdminJpaVO admin) {
                             em.persist( admin );
                   }
                   
                   public void updateAdmin( AdminJpaVO admin) {
                             em.merge( admin );
                   }
                   
                   public void deleteAdmin( AdminJpaVO admin ) {
                             em.remove( em.find( AdminJpaVO.class , admin.getUserId()) );
                   }
                   
                   public AdminJpaVO getAdmin( AdminJpaVO admin ) {
                             AdminJpaVO dbAdmin = em.find( AdminJpaVO.class , admin.getUserId() );
                             return dbAdmin;
                   }
                   
                   public List<AdminJpaVO> getAdminList( ){
                             // List<AdminJpaVO> list = em.createQuery("from AdminJpaVO").getResultList();
                             List<AdminJpaVO> list = em.createQuery( "select b from AdminJpaVO b" ).getResultList();
                             return list;
                   }
                   
      }

@Repository 어노테이션을 통하여 Spring의 Bean 관리자를 통하여 본 클래스의 인스턴스를 생성하도록 한다 
@PersistenceContext 어노테이션은 Spring Bean Config XML을 통하여 지정한 entityManagerFactory(<bean id="entityManagerFactory")의 인스턴스를 자동으로 멤버변수 EntityManager em에 대입하도록 해준다 

맨 마지막의 getAdminList 함수에서는 createQuery 함수 사용의 예를 보여준다
"from AdminJpaVO"  또는 "select b from AdminJpaVO b" 을 잘 살펴보면 우리가 흔히 아는 쿼리문으로 from 뒤에 테이블명이 있어야 할 것 같지만 테이블에 맵핑된 클래스명이 있음을 알 수 있다
이는 테이블에 맵핑된 클래스를 통하여 쿼리문을 구성한다는 의미다 


5.  JUnit Test로 테스트 해보기

이제 JUnit Test 로 아래와 같이 테스트 해본다 

public class AdminJpaDAOTest {
          
          AdminJpaDAO adminDao = null;
          
          @Before
          public void init(){
                   AbstractApplicationContext ctx =
                                      new GenericXmlApplicationContext("classpath:context_xml/DBConfig.xml" );
                   
                   Assert.assertNotNull( ctx );
                   
                   adminDao =          ctx.getBean("adminJpaDAO", com.dbconn.DAO.AdminJpaDAO.class);
                   Assert.assertNotNull( adminDao );
          }
          
          @Test
          public void test() {
                   AdminJpaVO admin = new AdminJpaVO();
                   admin.setUserId("david");
                   
                   AdminJpaVO dbAdmin = adminDao.getAdmin(admin);
                   System.out.println( dbAdmin );
          }
        @Test
          public void test2() {
                   List<AdminJpaVO> list = adminDao.getAdminList();
                   
                   int nLen = list.size();
                   for( int nIdx = 0; nIdx < nLen; nIdx ++ ){
                             AdminJpaVO admin = list.get(nIdx);
                             System.out.println( admin );
                   }
          }
}

AdminJpaDAO가 제공하는 함수를 실행하는 지점 다음에 Break point를 설정하고 MySQL에서 Connection 수를 확인해보면 아래와 같이 2에서 7로 늘어난 것을 알수 있다  
이는 c3p0의 connection 5개가 DBMS에 접속중임을 보여준다 






1 2 3 4 5 6 7 8 9 10 다음