Chapter10. 스프링과 JDBC를 사용하여 데이터베이스 사용하기

데이터 액세스

  • 데이터 액세스 프레임워크 초기화
  • 커넥션 오픈
  • 다양한 예외처리
  • 연결 종료


스프링 데이터 액세스

  • 다양한 기술들로 결합된 데이터 액세스 프레임워크 제공
    ex) JDBC, 하이버네이트, JPA(Java Persistence API), 퍼시스턴스 프레임워크, NoSQL 데이터베이스 등
  • 퍼시스턴스 코드에서 데이터 액세스를 제거
  • 하위 레벨(low-level) 데이터 액세스 작업시 스프링을 사용하여 데이터 관리를 수행

이번 장에서는 JDBC를 위한 스프링 지원 사항에 대해서 중점적으로 살펴본다.

10-1 스프링의 데이터 액세스 철학

저장소 (DAO, Data-Acess Object)

  • 퍼시스턴스 로직이 산란(scattering)되는 것을 피하기 위해서 데이터 액세스는 해당 태스크에 집중된 하나 이상의 컴포넌트로 이루어진다.
  • 애플리케이션이 저장소 내 특정 데이터 액세스 전략에 커플링되는 것을 막기 위해 인터페이스를 사용하여 기능을 외부로 제공한다.
  • [그림10.1] 서비스 객체는 자신의 데이터 액세스를 처리하지 않는다. 대신 데이터 액세스를 저장소에 위임한다. 저장소의 인터페이스는 서비스 객체에 느슨하게 커플링한다. (p341)


인터페이스와 스프링

  • 서비스 객체는 인터페이스를 통해서 저장소에 액세스 한다. (테스트가 용이해짐)
  • 데이터 액세스 계층은 퍼시스턴스 기술에 상관없이 액세스 한다.
  • 데이트 액세스 메소드만 인터페이스를 통해서 노출시킨다. (애플리케이션 나머지 부분에 최소한의 영향)
  • 스프링은 데이터 액세스 계층을 모든 퍼시스턴스 옵션을 가지는 일관적인 예외 계층 구조를 사용하여 애플리케이션의 나머지와 격리한다.

10-1-1 스프링의 데이터 액세스 예외 계층 구조

SQLExcetion은 무쓸모

  • 데이터베이스 액세스 동안 무엇인가 문제가 있다는 사실을 의미한다.
  • 하지만 무엇이 잘못되었고 어떻게 처리해야 하는지에 대한 설명은 거의 없다.
  • 대부분 catch 블록에서 복구가 불가능한 치명적인 상황이다.
  • 예외 발생 근본 원인에 대한 상세 정보를 획득하기 위해 예외가 가진 모든 프로퍼티를 낱낱이 살펴봐야 한다.
    • 하이버네이트는 다양한 예외를 제공하지만 하이버네이트에 특화되어 있다. (플랫폼 독립적인 예외로 다시 던져야 함)
    • JDBC의 예외 계층 구조는 너무 포괄적이다.


스프링의 독립적인 예외

  • 예외가 발생한 문제상황을 잘 설명하는 다양한 데이터 액세스 예외들 제공
    ex) [표10.1] JDBC 예외 vs 스프링 데이터 액세스 예외 (p344)
  • 퍼시스턴스 솔루션에 독립적인 일관성 있는 예외 (퍼시스턴스 기술을 데이터 액세스 계층 내에 캡슐화)


스프링의 비검사형(unchecked) 예외

  • 스프링 데이터 액세스 예외는 DataAccessException을 확장한 것이다.
  • DataAcessException은 비검사형 예외 이다.
  • 스프링이 던지는 모든 데이터 액세스 예외는 반드시 잡아서 처리할 필요가 없다. (예외를 잡을지 말지는 개발자가 결정)
  • 이러한 이점을 취하기 위해서는 반드시 스프링이 제공하는 데이터 액세스 템플릿을 이용해야 한다.


참고: 역자 한마디 "스프링의 핵심 장점 중 하나는 검사형 예외를 비검사형 예외로 변환해 주는 것" (p345)

  • 검사형(checked) 예외
    • throws 된다고 선언된 메소드는 호출시 반드시 try-catch 블록으로 감싸야만 컴파일 가능
    • ex) java.sql.SQLException, java.io.IOException 등
  • 비검사형(unchecked) 예외
    • try-catch 블록을 사용하지 않아도 컴파일이 되는 예외 (런타임 예외, 실행시간 예외)
    • ex) java.lang.RuntimeException, java.lang.Error를 확장한 NullPointException, IllegalArgumentException 등
  • 스프링은 비검사형 예외를 강력히 지지 한다.
    • 역자 또한 자바 API를 포함한 많은 라이브러리가 검사형 예외를 남용한다고 생각한다.
    • 스프링은 많은 예외가 catch 블록 내에서 해결할 수 없는 문제 때문에 발생한다고 보고 있다.
    • catch 블록의 작성을 강제하는 대신 비검사형 예외를 사용하도록 촉진한다.
    • 여타 프레임워크가 가진 검사형 예외를 비검사형 예외로 변환해주는 프레임워크는 스프링이 거의 유일하다.

10-1-2 데이터 액세스 템플릿화

ex) 비행기 수하물 이동

템플릿 메소드(Template Method) 패턴

  • 어떤 절차의 골격을 정의
  • 어떤 절차의 구현 종속적인 부분을 특정 인터페이스에 위임
  • 인터페이스를 다르게 구현함으로써 위임된 부분에 특화된 부분을 정의
  • 스프링이 데이터 액세스에 적용한 패턴


스프링 데이터 액세스 절차

  • 고정된 단계와 가변적인 단계를 템플릿(tempalte)과 콜백(callback)이라는 두 가지의 별도의 클래스로 분리
    • 템플릿 클래스: 트랜잭션 제어, 자원 관리 및 예외 처리와 같은 데이터 액세스의 고정된 부분 담당
    • 콜백(callback) 클래스: 질의객체(statement) 생성, 파라미터 바인딩, 질의 결과 추출과 변환 등
  • 스프링의 데이터 액세스 탬플릿 클래스는 공통적인 데이터 액세스 의무에 대한 책임을 진다.
  • 애플리케이션에 특화된 작업을 처리하기 위해서 템플릿 클래스는 맞춤형 콜백 객체를 호출한다.
  • 데이터 액세스 로직에만 집중할 수 있다.

[표10.2] 스프링 데이터 액세스 템플릿 종류 (p348)

스프링은 퍼시스턴스 플랫폼에 따라 선택할 수 있는 다양한 템플릿을 제공한다.

템플릿 클래스 (org.springframework.*) 용도
jca.cci.core.CciTemplate JCA CCI 연결
jdbc.core.JdbcTemplate JDBC 연결
jdbc.core.namedparam.NamedParameterJdbcTemplate 명명된 파라미터(named parameter)가 지원되는 JDBC 연결
jdbc.core.simple.SimpleJdbcTemplate 자바5를 활용해서 더 쉬워진 JDBC 연결 (스프링3.1에선 없어짐)
orm.hibernate3.HibernateTemplate Hibernate 3.x 세션
orm.ibatis.SqlMapClientTemplate iBATIS SqlMap 클라이언트
orm.jdo.JdoTemplate JDO(Java Data Object) 구현체
orm.jpa.JpaTemplate JPA(Java Persistence API) 엔티티 관리자
  • 10장 JDBC: 가장 기본적인 방법
  • 11장 하이버네이트, JPA: POJO 기반 ORM(Object-Relational Mapping) 솔루션
  • 12장 NoSQL: 비스키마(schemaless) 데이터

10.2 데이터 소스 설정

사용자가 사용하는 스프링 지원 데이터 액세스가 어떤 형태라도 데이터 소스에 대한 레퍼런스 설정이 필요함

  • JDBC 드라이버를 통해 선언된 데이터 소스
  • JNDI에 등록된 데이터 소스
  • 커넥션 풀링(pooling)하는 데이터 소스
    • 상용 application에서는 커넥션 풀에서 커넥션을 획득하는 데이터 소스를 권장

10.2.1 JNDI 데이터 소스 이용

  • 스프링 애플리케이션은 대부분 JEE 애플리케이션 서버에 배포 되어서 사용됨.
  • 이런 서버들은 WebSphere나 JBoss 또는 Tomcat 과 같은 웹컨테이너 역할을 함

JNDI 설명 (Java Naming and Directory Interface)

  • server.xml 등 서버에 DataSource 에 대한 몇몇 설정이 필요함
  • 장점
    • 데이터 소스가 애플리케이션 외부에서 관리
    • 애플리케이션에서는 데이터소스에 접근할 시점에 단순히 데이터소스를 요청하기만 하면 됨
    • 서버에 의해 관리되는 데이터소스는 향상된 성능을 위해 풀링 기능을 갖기도 하고 시스템 관리자가 운영시 동적으로 교체 가능함

애플리케이션 설정 방식

  <jee:jndi-lookup id="datasource" jndi-name="/jdbc/SplitterDS" resource-ref="true" />
  @Bean
  public JndiObjectFactoryBean dataSource() {
    JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
    jndiObjectFB.setJndiName("jdbc/SplitterDS");
    jndiObjectFB.setResourceRef(true);
    jndiObjectFB.setProxyInterface(javax.sql.DataSource.class.class);

    return jndiObjectFB;
  }
  • 자바 설정을 사용한다면 JNDI에서 DataSource를 검색하기 위해서 JndiObjectFactoryBean을 사용함

10.2.2 풀링 기능이 있는 데이터 소스 사용하기

JNDI 를 사용하지 않을 경우, 풀링 기능이 있는 데이터 소스를 스프링에 직접 설정 가능함

  <bean id="datasource" class="org.apache.commons.dbcp.BasicDataSource"
    p:driverClassName="org.h2.Driver"
    p:url="jdbc:h2:tcp://localhost/~spitter"
    p:username="sa"
    p:password=""
    p:initailSize="5"
    p:maxActive="10" />
  @Bean
  public BasicDataSource dataSource() {
    BasicDataSource ds = new BasicDataSource();
    ds.setDriverClassName("org.h2.Driver");
    ds.setUrl("jdbc:h2:tcp://localhost/~spitter");
    ds.setUsername("sa");
    ds.setPassword("");
    ds.setInitailSize(5);
    ds.setMaxActive(10);
    return ds;
  }

BasicDataSource 의 풀 프로퍼티

풀 설정 프로퍼티 설명
intialSize 해당 풀이 시작될 때 생성할 커넥션 수
maxActive 해당 풀에서 동시에 제공할 수 있는 최대 커넥션 수. 0은 무제한을 나타낸다
maxIdle 해당 풀에서 동시에 휴면 상태로 유지될 수 있는 최대 커넥션 수. 0은 무제한을 나타낸다
maxOpenPreparedStatements 질의 객체(statement) 풀에서 동시에 제공할 수 있는 최대 PreparedStatement 의 수. 0은 무제한을 나타낸다ㅏ
maxWait 해당 풀에 커넥션을 요청했을 때(제공 받을 수 있는 커넥션이 없어서) 대기 가능한 최대 시간. 이 시간이 지나면 예외가 발송핸다. 1은 무한히 대기함을 의미한다.
minEvictableIdleTimeMillis 해당 풀에서 커넥션을 제거하기 전에 휴면 상태로 남아 있을 수 있는 시간
minIdle 해당 풀에서 커넥션이 휴면 상태로 유지될 수 있는 최소 커넥션 수
poolPreparedStatements PreparedStatement 의 풀링(pooling) 여부를 나타내는 boolean 값

[참고] Statement vs PreparedStatement

Statement

executeQuery() 나 executeUpdate() 를 실행하는 시점에 파라미터로 SQL문을 전달하는데, 이 때 전달되는 SQL 문은 완성된 형태로 한눈에 무슨 SQL 문인지 파악하기 쉽다. 하지만, 이 녀석은 SQL문을 수행하는 과정에서 매번 컴파일을 하기 때문에 성능상 이슈가 있다. ( 이 컴파일을 Parsing 한다고도 표현한다. )

String sql = "select * from users where _id=1";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery( sql );
PreparedStatement

미리 컴파일 되기 때문에 Statement 에 비해 성능상 이점이 있다.

String sql = "select * from users where _id=?";
PreparedStatement pstmt = conn.prepareStatement( sql );
pstmt.setInt( 1, 1 );
ResultSet rs = pstmt.executeQuery();

10.2.3 JDBC 드라이버 기반 데이터 소스

스프링에 설정할 수 있는 가장 단순한 데이터 소스는 JDBC 드라이버를 통해 정의 (스프링 내 org.springframework.jdbc.datasource 패키지 안에 존재)

JDBC Driver 설명 Pooling 기능 제공
DriverManagerDataSource 애플리케이션이 커넥션 요청 시마다 새로운 커넥션을 반환 풀링 지원 안함
SimpleDriverDataSource OSGi 컨테이너와 같이 특정 환경에서 발생할 수 있는 클래스 로딩 문제를 극복하기 위해 사용하는 것을 제외하면 DriverManagerDataSource 와 동일 풀링 지원 안함
SingleConnectionDataSource 항상 동일 커넥션을 반환 오직 한 커넥션만을 풀링
  <bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
    p:driverClassName="org.h2.Driver"
    p:url="jdbc:h2:tcp://localhost/~spitter"
    p:username="sa"
    p:password="" />
  • 풀링 설정 프로퍼티가 존재 안함
  @Bean
  public BasicDataSource dataSource() {
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName("org.h2.Driver");
    ds.setUrl("jdbc:h2:tcp://localhost/~spitter");
    ds.setUsername("sa");
    ds.setPassword("");
    return ds;
  }

Guide

  • DriverManagerDataSource, SingleConnectionDataSource : 작은 규모의 애플리케이션과 개발 단꼐에서는 데이터 소스가 좋은 선택이 될 수 있지만,
  • 상용 애플리케이션에서는 심각한 성능 저하를 고려하여 커넥션 풀링이 있는 데이터 소스를 사용할 것을 강력하게 권장함

10.2.4 임베디드 데이터 소스 사용하기

  • 사용자가 사용할 추가 데이터 소스로 임베디드 데이터베이스가 있음.
  • 애플리케이션이 연결하는 독립 데이터베이스 서버 대신에 임베디드 데이터 베이스가 애플리케이션 중 하나로 동작
<jdbc:embedded-database> type 프로퍼티를 사용함

10.2.5 데이터 소스 선택을 위한 프로파일링하기

  • 개발, 사용 단계에서의 데이터 소스 선택이 필요한 상황이 있음
  • 런타임 시 스프링의 데이터 소스 선택을 위한 프로파일링이 필요
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
        <constructor-arg ref="dataSource"/>
    </bean>

    <beans profile="dev">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>
    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>
@Configuration
public class DataSourceConfiguration {

    @Profile("dev")
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Profile("production")
    @Bean
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

10.3 스프링과 JDBC

10.3.1 지저분한 JDBC 코드 해결

  • cf. jdbc example
//STEP 1. Import required packages
import java.sql.*;

public class JDBCExample {
   // JDBC driver name and database URL
   static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";  
   static final String DB_URL = "jdbc:mysql://localhost/STUDENTS";

   //  Database credentials
   static final String USER = "username";
   static final String PASS = "password";

   public static void main(String[] args) {
   Connection conn = null;
   Statement stmt = null;
   try{
      //STEP 2: Register JDBC driver
      Class.forName("com.mysql.jdbc.Driver");

      //STEP 3: Open a connection
      System.out.println("Connecting to a selected database...");
      conn = DriverManager.getConnection(DB_URL, USER, PASS);
      System.out.println("Connected database successfully...");

      //STEP 4: Execute a query
      System.out.println("Inserting records into the table...");
      stmt = conn.createStatement();

      String sql = "INSERT INTO Registration " +
                   "VALUES (100, 'Zara', 'Ali', 18)";
      stmt.executeUpdate(sql);
      sql = "INSERT INTO Registration " +
                   "VALUES (101, 'Mahnaz', 'Fatma', 25)";
      stmt.executeUpdate(sql);
      sql = "INSERT INTO Registration " +
                   "VALUES (102, 'Zaid', 'Khan', 30)";
      stmt.executeUpdate(sql);
      sql = "INSERT INTO Registration " +
                   "VALUES(103, 'Sumit', 'Mittal', 28)";
      stmt.executeUpdate(sql);
      System.out.println("Inserted records into the table...");

   }catch(SQLException se){
      //Handle errors for JDBC
      se.printStackTrace();
   }catch(Exception e){
      //Handle errors for Class.forName
      e.printStackTrace();
   }finally{
      //finally block used to close resources
      try{
         if(stmt!=null)
            conn.close();
      }catch(SQLException se){
      }// do nothing
      try{
         if(conn!=null)
            conn.close();
      }catch(SQLException se){
         se.printStackTrace();
      }//end finally try
   }//end try
   System.out.println("Goodbye!");
}//end main
}//end JDBCExample
package com.vaannila.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.sql.DataSource;

import com.vaannila.domain.Forum;

public class JDBCForumDAOImpl implements ForumDAO {

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void insertForum(Forum forum) {
        /**
         * Specify the statement
         */
        String query = "INSERT INTO FORUMS (FORUM_ID, FORUM_NAME, FORUM_DESC) VALUES (?,?,?)";
        /**
         * Define the connection and preparedStatement parameters
         */
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            /**
             * Open the connection
             */
            connection = dataSource.getConnection();
            /**
             * Prepare the statement
             */
            preparedStatement = connection.prepareStatement(query);
            /**
             * Bind the parameters to the PreparedStatement
             */
            preparedStatement.setInt(1, forum.getForumId());
            preparedStatement.setString(2, forum.getForumName());
            preparedStatement.setString(3, forum.getForumDesc());
            /**
             * Execute the statement
             */
            preparedStatement.execute();
        } catch (SQLException e) {
            /**
             * Handle any exception
             */
            e.printStackTrace();
        } finally {
            try {
                /**
                 * Close the preparedStatement
                 */
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                /**
                 * Close the connection
                 */
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                /**
                 * Handle any exception
                 */
                e.printStackTrace();
            }
        }

    }

}
  • 간단한 테이블 입력 하나에도 너무나 많은 Exception 코드가 들어 있다.
  • 실질적인 작업 수행은 단순한 몇개의 줄로만 이루어진다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:hsql://localhost"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>

    <bean id="forumDAO" class="com.vaannila.dao.ForumDAOImpl">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

10.3.2 JDBC 템플릿과 놀아보자

  • 스프링의 JDBC 프레임워크는 자원 관리와 예외 처리를 처리함으로서 개발자는 오직 데이터 처리 부분 구현에 집중 가능

  • 세개의 선택 가능한 JDBC 템플릿 클래스를 제공

    • JdbcTemplate : 스프링의 가장 기본적인 JdbcTemplate 으로, 색인된 파라미터(indexed parameter) 기반 의 쿼리를 통해서 데이터베이스에 쉽게 액세스하는 기능을 제공
    • NamedParameterJdbcTemplate : 명명된 파라미터(named parameter)로 바인딩하여 쿼리 실행
    • SimpleJdbcTemplate : 스프링 버전 3.1에서 삭제
// JDBC를 사용하는 DAO 클래스...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
  this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActors(Actor exampleActor) {

  // 이름있는 파라미터들이 어떻게 위의 'Actor' 클래스의 프로퍼티에서 일치되는 것을 찾는지 주의깊게 봐라.
  String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";

  SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);

  return this.namedParameterJdbcTemplate.queryForInt(sql, namedParameters);
}
package com.vaannila.dao;

import java.sql.ResultSet;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import com.vaannila.domain.Forum;

public class ForumDAOImpl implements ForumDAO {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public Forum selectForum(int forumId) {
        /**
         * Specify the statement
         */
        String query = "SELECT * FROM FORUMS WHERE FORUM_ID=?";
        /**
         * Implement the RowMapper callback interface
         */
        return (Forum) jdbcTemplate.queryForObject(query, new Object[] { Integer.valueOf(forumId) }, 
                new RowMapper() {
                    public Object mapRow(ResultSet resultSet, int rowNum) throws SQLException {
                        return new Forum(resultSet.getInt("FORUM_ID"), resultSet.getString("FORUM_NAME"), 
                                resultSet.getString("FORUM_DESC"));
                    }
                });
    }

}
  • RowMapper를 통해 재사용성을 높일 수 있음
    • org.springframework.jdbc.core.RowMapper
              (Forum) jdbcTemplate.queryForObject(query, new Object[] { Integer.valueOf(forumId) }, 
                new ForumRowMapper());
package com.javarticles.spring.integration.jdbc;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

public class ForumRowMapper implements RowMapper<Forum> {

    public Article mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Forum(resultSet.getInt("FORUM_ID"), resultSet.getString("FORUM_NAME"), 
                                resultSet.getString("FORUM_DESC"));
    }
}
  • main method
package com.vaannila.dao;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.vaannila.domain.Forum;

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        ForumDAO forumDAO = (ForumDAO) context.getBean("forumDAO");
        Forum springForum = new Forum(1,"Spring Forum", "Discuss everything related to Spring");
        forumDAO.insertForum(springForum);        
        System.out.println(forumDAO.selectForum(1));

    }

}

results matching ""

    No results matching ""