2020. 12. 8. 10:53ㆍ교육과정/KOSMO
키워드 : 방명록 답글 기능 만들기 / mybatis
****
※. DB 연동방식 : ① JDBC
② mybatis (ibatis)
③ jpa
0. 답글 달기의 구조 : 순서번호의 역순 정렬을 사용한다.
(1) 글 번호 순으로 정렬했을 경우
(2) 순서번호의 역순으로 정렬했을 경우
▶ 순서번호의 답글에 관한 숫자는 6자리 중 2자리씩 할당되므로,
999999 여섯자리 숫자 구조에서는 답글의 답글이 3번째까지 가능
▶ 한 글의 답글은 2자리 숫자가 99에서부터 역순으로 전개되므로 99개까지 가능
※ 답글은 현재는 사용하는 경우가 많지 않으나, 관리자가 답글을 달아주는 경우에 사용된다.
1. 게시글 보기(BoardView.jsp) 에서 "답변하기" 텍스트 클릭하면 답변 작성 페이지로 이동
(1) <a> 태그를 사용하여 하이퍼 링크를 생성
<a href="BoardReplyForm.jsp">답변하기</a>
(2) 기존 글이 부모글이 되므로, 부모글의 게시글번호(ID)를 파라미터로 가져가기
<a href="BoardReplyForm.jsp?parentId=<%= rec.getArticleId() %>">답변하기</a>
(3) 이전 화면(BoardView.jsp)에서 넘어오는 부모 게시글의 번호를
답변 작성 페이지(BoardReplyForm.jsp)에서 받아서 저장
<%
String parentId = request.getParameter("parentId");
%>
(4) "작성" 버튼을 클릭했을 때 다음화면으로 넘어갈 <form> 의 자식 요소들에 name을 부여
작성자 : <input type='text' name='writerName'><br/><br/>
제 목 : <input type='text' name='title'><br/><br/>
내 용 : <textarea name='content' rows='10' cols='40'></textarea><br/><br/>
패스워드(수정/삭제시 필요) :
<input type='password' name='password'><br/><br/>
(5) <form> 에 action 속성 부여하여 사용자 입력값을 "BoardReply.jsp"로 전송
<form name='frm' action='BoardReply.jsp' method='post'>
(6) 부모 게시글의 번호를 <form> 의 자식요소로 넣어 다음 화면(BoardReply.jsp)에 함께 전달
<input type='hidden' name='parentId' value='<%= parentId %>' />
2. 답변 작성 페이지(BoardReplyForm.jsp)에서 넘어온 입력값을
서비스 페이지(ReplyArticleService.java)로 전송
(1) 답변 작성 페이지(BoardReplyForm.jsp)에서 BoardReply.jsp로 넘어오는
파라미터 (부모 게시물의 글 번호)를 넘겨받아 저장
<%
String pId = request.getParameter("parentId");
%>
(2) 테스트를 위해 이전 화면에서 넘어온 데이터를 화면에 출력 (임시)
<%= pId %> / <%= rec.getTitle() %>
(3) ReplyArticleService.java의 reply( ) 메소드 호출
<%
BoardRec reRec = ReplyArticleService.getInstance().reply(pId, rec);
%>
3. 사용자 입력값을 DB에 등록하기
(1) ReplyArticleService.java의 reply( ) 메소드는 인자 2개를 받아 수행
public BoardRec reply( String pId, BoardRec rec ) throws BoardException {
(2) reply( ) 메소드에서는 부모게시글의 레코드를 바탕으로 답변글의 순서번호를 구한 뒤,
그룹번호는 부모의 그룹번호와 동일하게 부여한다.
등록일은 새로운 날짜가 들어가게 한다. (기작성된 내용으로 분석X 자유ㅋ)
BoardDao dao = BoardDao.getInstance();
BoardRec parent = dao.selectById(parentId); // 부모게시글의 레코드를 얻어옴
checkParent(parent, parentId); // 부모게시글을 체크
String maxSeqNum = parent.getSequenceNo();
String minSeqNum = getSearchMinSeqNum( parent );
String lastChildSeq = dao.selectLastSequenceNumber( maxSeqNum, minSeqNum );
String sequenceNumber = getSequenceNumber( parent,lastChildSeq);
rec.setGroupId(parent.getGroupId()); // 부모의 그룹번호와 동일하게 지정
rec.setSequenceNo(sequenceNumber); // 위에서 구한 답변글의 순서번호 지정
rec.setPostingDate( (new Date()).toString()); // 등록일
(3) BoardDao.java의 insert( ) 메소드를 호출하여 답글의 입력값을 DB에 등록
int articleId = dao.insert(rec);
rec.setArticleId(articleId);
return rec;
(결과) 답변이 부모글의 아래에 자리하게 된다.
< ReplyArticleService.java의 reply( ) 메소드 전체 소스 코드 >
public BoardRec reply( String pId, BoardRec rec ) throws BoardException{
int parentId = 0;
if( pId != null ) parentId = Integer.parseInt(pId);
BoardDao dao = BoardDao.getInstance();
// 부모게시글의 레코드를 얻어옴
BoardRec parent = dao.selectById(parentId);
// 부모게시글을 체크
checkParent(parent, parentId);
// 답변글에 순서번호 구하기
String maxSeqNum = parent.getSequenceNo();
String minSeqNum = getSearchMinSeqNum( parent );
String lastChildSeq = dao.selectLastSequenceNumber( maxSeqNum, minSeqNum );
String sequenceNumber = getSequenceNumber( parent,lastChildSeq);
rec.setGroupId(parent.getGroupId()); // 부모의 그룹번호와 동일하게 지정
rec.setSequenceNo(sequenceNumber); // 위에서 구한 답변글의 순서번호 지정
rec.setPostingDate( (new Date()).toString()); // 등록일
int articleId = dao.insert(rec);
rec.setArticleId(articleId);
return rec;
}
4. 목록에서 답변에 해당되는 글 제목 앞에 이미지 삽입하기
(1) BoardList.jsp의 글 제목이 자리하는 <td> 태그 안에 부모글이 있을 경우 (getLevel( ) != 0) 제목 앞에 이미지 출력
<td>
<a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>
<td>
<% if(rec.getLevel() != 0) { %>
<img alt='답글표시' src='imgs/board_re.gif'>
<% } %>
<a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>
(2) 부모글이 얼마나 존재하느냐에 따라 이미지 앞에 공백을 넣기 위해 for문 작성
<td>
<% if(rec.getLevel() != 0) { %>
<img alt='답글표시' src='imgs/board_re.gif'>
<% } %>
<a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>
<td>
<% for(int i=0; i<rec.getLevel(); i++) { %>
<% } %>
<% if(rec.getLevel() != 0) { %>
<img alt='답글표시' src='imgs/board_re.gif'>
<% } %>
<a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>
< BoardList.jsp 전체 소스 코드 >
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="board.model.*, board.service.*" %>
<%@ page import="java.util.List" %>
<% //웹브라우저가 게시글 목록을 캐싱할 경우 새로운 글이 추가되더라도 새글이 목록에 안 보일 수 있기 때문에 설정
response.setHeader("Pragma","No-cache"); // HTTP 1.0 version
response.setHeader("Cache-Control","no-cache"); // HTTP 1.1 version
response.setHeader("Cache-Control","no-store"); // 일부 파이어폭스 버스 관련
response.setDateHeader("Expires", 1L); // 현재 시간 이전으로 만료일을 지정함으로써 응답결과가 캐쉬되지 않도록 설정
%>
<%
String pNum = request.getParameter("page");
int pageNo = 1;
if (pNum != null)
pageNo = Integer.parseInt(pNum);
// Service에 getArticleList()함수를 호출하여 전체 메세지 레코드 검색
ListArticleService service = ListArticleService.getInstance();
int pageTotalCount = service.getTotalPage();
List <BoardRec> mList = service.getArticleList(pageNo);
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title> 게시글 목록 </title>
</head>
<BODY>
<h3> 게시판 목록 </h3>
<table border="1" bordercolor="darkblue">
<tr>
<td> 글번호 </td>
<td> 제 목 </td>
<td> 작성자 </td>
<td> 작성일 </td>
<td> 조회수 </td>
</tr>
<% if( mList.isEmpty() ) { %>
<tr><td colspan="5"> 등록된 게시물이 없습니다. </td></tr>
<% } else { %>
<% for(BoardRec rec : mList) { %>
<tr>
<td><%= rec.getArticleId() %></td>
<td>
<% for(int i=0; i<rec.getLevel(); i++) { %>
<% } %>
<% if(rec.getLevel() != 0) { %>
<img alt='답글표시' src='imgs/board_re.gif'>
<% } %>
<a href="BoardView.jsp?id=<%= rec.getArticleId() %>"><%= rec.getTitle() %></a>
</td>
<td><%= rec.getWriterName() %></td>
<td><%= rec.getPostingDate() %></td>
<td><%= rec.getReadCount() %></td>
</tr>
<% } // end of for %>
<% } // end else %>
<tr>
<td colspan="5">
<a href="BoardInputForm.jsp">글쓰기</a>
</td>
</tr>
</table>
<a href="BoardList.jsp?page=1">[◀◀]</a>
<a href="BoardList.jsp?page=<%= pageNo-1 %>">[◀]</a>
<% for (int i=1; i<=pageTotalCount; i++) { %>
<a href='BoardList.jsp?page=<%= i %>'>[ <%= i %>]</a>
<% } %>
<a href="BoardList.jsp?page=<%= pageNo+1 %>">[▶]</a>
<a href="BoardList.jsp?page=<%= pageTotalCount %>">[▶▶]</a>
</BODY>
</HTML>
< BoardView.jsp 전체 소스 코드 >
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page import="board.service.*, board.model.*" %>
<%
// 1. 해당 게시물의 게시글번호값을 얻어온다
String id = request.getParameter("id");
// 2. Service에 getArticleById() 호출하여 그 게시글번호를 갖는 레코드를 검색한다.
ViewArticleService service = ViewArticleService.getInstance();
BoardRec rec = service.getArticleById(id);
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title> 게시글 보기 </title>
</head>
<body>
<h4> 게시판 글 보기 </h4><br/>
<table border="1" bordercolor="red">
<tr>
<td> 제 목 : </td>
<td><%= rec.getTitle() %></td>
</tr>
<tr>
<td> 작성자 : </td>
<td><%= rec.getWriterName() %></td>
</tr>
<tr>
<td> 작성일자 : </td>
<td><%= rec.getPostingDate() %></td>
</tr>
<tr>
<td> 내 용 : </td>
<td><%= rec.getContent() %></td>
</tr>
<tr>
<td colspan="2">
<a href="BoardList.jsp">목록보기</a>
<a href="BoardReplyForm.jsp?parentId=<%= rec.getArticleId() %>">답변하기</a>
<a href="BoardModifyForm.jsp?id=<%= rec.getArticleId() %>">수정하기</a>
<a href="BoardDeleteForm.jsp?id=<%= rec.getArticleId() %>">삭제하기</a>
</td>
</tr>
</table>
</body>
</html>
< BoardReplyForm.jsp 전체 소스 코드 >
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
// 답변글의 부모 게시글의 번호를 넘겨받기
String parentId = request.getParameter("parentId");
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title> 답변 글쓰기 </title>
</head>
<body>
<h4> 답변 글 쓰기 </h4><br/>
<form name='frm' action='BoardReply.jsp' method='post'>
<input type='hidden' name='parentId' value='<%= parentId %>' />
작성자 : <input type='text' name='writerName'><br/><br/>
제 목 : <input type='text' name='title'><br/><br/>
내 용 : <textarea name='content' rows='10' cols='40'></textarea><br/><br/>
패스워드(수정/삭제시 필요) :
<input type='password' name='password'><br/><br/>
<input type='submit' value='작성'>
<input type='reset' value='취소'>
</form>
</body>
</html>
< BoardReply.jsp 전체 소스 코드 >
<%@ page import="board.service.ReplyArticleService, board.model.BoardRec"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
request.setCharacterEncoding("UTF-8");
%>
<jsp:useBean id="rec" class="board.model.BoardRec">
<jsp:setProperty name="rec" property="*"/>
</jsp:useBean>
<%
// 1. 부모게시물의 게시번호를 넘겨받기
String pId = request.getParameter("parentId");
// 2. Service에 reply() 호출하여 답변글 등록하기
BoardRec reRec = ReplyArticleService.getInstance().reply(pId, rec);
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title> 답변 글 저장하기 </title>
</head>
<body>
<%= pId %> / <%= rec.getTitle() %>
답변글을 등록하였습니다. <br/><br/>
<a href="BoardList.jsp"> 목록보기 </a>
<a href="BoardView.jsp?id=<%=rec.getArticleId()%>"> 게시글 읽기 </a>
</body>
</html>
< ReplyArticleService.java 전체 소스 코드 >
package board.service;
import java.text.DecimalFormat;
import java.util.Date;
import board.model.BoardDao;
import board.model.BoardException;
import board.model.BoardRec;
public class ReplyArticleService {
private static ReplyArticleService instance;
public static ReplyArticleService getInstance() throws BoardException{
if( instance == null )
{
instance = new ReplyArticleService();
}
return instance;
}
public BoardRec reply( String pId, BoardRec rec ) throws BoardException{
int parentId = 0;
if( pId != null ) parentId = Integer.parseInt(pId);
BoardDao dao = BoardDao.getInstance();
// 부모게시글의 레코드를 얻어옴
BoardRec parent = dao.selectById(parentId);
// 부모게시글을 체크
checkParent(parent, parentId);
// 답변글에 순서번호 구하기
String maxSeqNum = parent.getSequenceNo();
String minSeqNum = getSearchMinSeqNum( parent );
String lastChildSeq = dao.selectLastSequenceNumber( maxSeqNum, minSeqNum );
String sequenceNumber = getSequenceNumber( parent,lastChildSeq);
rec.setGroupId(parent.getGroupId()); // 부모의 그룹번호와 동일하게 지정
rec.setSequenceNo(sequenceNumber); // 위에서 구한 답변글의 순서번호 지정
rec.setPostingDate( (new Date()).toString()); // 등록일
int articleId = dao.insert(rec);
rec.setArticleId(articleId);
return rec;
}
/*
* 부모글이 존재하는지 부모글이 마지막 3단계인지 확인하는 함수
*/
private void checkParent( BoardRec parent, int pId ) throws BoardException
{
if( parent == null ) throw new BoardException("부모글이 존재하지 않음 : " + pId );
int parentLevel = parent.getLevel();
if( parentLevel == 3 ) throw new BoardException("3단계 마지막 레벨 글에는 답변을 달 수 없습니다.");
}
private String getSearchMinSeqNum( BoardRec parent )
{
String parentSeqNum = parent.getSequenceNo();
DecimalFormat dFormat = new DecimalFormat("0000000000000000");
long parentSeqLongValue = Long.parseLong(parentSeqNum);
long searchMinLongValue = 0;
switch( parent.getLevel())
{
case 0 : searchMinLongValue = parentSeqLongValue / 1000000L * 1000000L; break;
case 1 : searchMinLongValue = parentSeqLongValue / 10000L * 10000L; break;
case 2 : searchMinLongValue = parentSeqLongValue / 100L * 100L; break;
}
return dFormat.format(searchMinLongValue);
}
private String getSequenceNumber( BoardRec parent, String lastChildSeq ) throws BoardException
{
long parentSeqLong = Long.parseLong( parent.getSequenceNo());
int parentLevel = parent.getLevel();
long decUnit = 0;
if ( parentLevel == 0 ){ decUnit = 10000L; }
else if ( parentLevel == 1 ){ decUnit = 100L; }
else if ( parentLevel == 2 ){ decUnit = 1L; }
String sequenceNumber = null;
DecimalFormat dFormat = new DecimalFormat("0000000000000000");
if( lastChildSeq == null ){ // 답변글이 없다면
sequenceNumber = dFormat.format(parentSeqLong-decUnit);
} else { // 답변글이 있다면, 마지막 답변글인지 확인
String orderOfLastChildSeq = null;
if( parentLevel == 0 ){
orderOfLastChildSeq = lastChildSeq.substring(10,12);
sequenceNumber = lastChildSeq.substring(0, 12) + "9999";
}else if( parentLevel == 1 ){
orderOfLastChildSeq = lastChildSeq.substring(12,14);
sequenceNumber = lastChildSeq.substring(0, 14) + "99";
}else if( parentLevel == 2 ){
orderOfLastChildSeq = lastChildSeq.substring(14,16);
sequenceNumber = lastChildSeq;
}
if( orderOfLastChildSeq.equals("00")){
throw new BoardException("마지막 자식 글이 이미 존재합니다.");
}
long seq = Long.parseLong(sequenceNumber) - decUnit;
sequenceNumber = dFormat.format(seq);
return sequenceNumber;
}
return sequenceNumber;
}
}
< BoardRec.java 전체 소스 코드 >
package board.model;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class BoardDao
{
// Single Pattern
private static BoardDao instance;
// DB 연결시 관한 변수
private static final String dbDriver = "oracle.jdbc.driver.OracleDriver";
private static final String dbUrl = "jdbc:oracle:thin:@192.168.0.17:1521:orcl";
private static final String dbUser = "scott";
private static final String dbPass = "tiger";
private Connection con;
//--------------------------------------------
//##### 객체 생성하는 메소드 : 싱글톤 사용 - 다수의 사용자가 접속할 때마다 드라이버를 생성하지 않도록 하기 위함
public static BoardDao getInstance() throws BoardException
{
if( instance == null )
{
instance = new BoardDao();
}
return instance;
}
private BoardDao() throws BoardException
{
try{
/********************************************
1. 오라클 드라이버를 로딩
( DBCP 연결하면 삭제할 부분 )
*/
Class.forName( dbDriver );
}catch( Exception ex ){
throw new BoardException("DB 연결시 오류 : " + ex.toString() );
}
}
//--------------------------------------------
//##### 게시글 입력전에 그 글의 그룹번호를 얻어온다
public int getGroupId() throws BoardException
{
PreparedStatement ps = null;
ResultSet rs = null;
int groupId=1;
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
String sql = "SELECT SEQ_GROUP_ID_ARTICLE.nextVal FROM dual";
ps = con.prepareStatement( sql );
rs = ps.executeQuery();
while(rs.next()) {
groupId = rs.getInt("nextval");
}
return groupId;
}catch( Exception ex ){
throw new BoardException("게시판 ) 게시글 입력 전에 그룹번호 얻어올 때 : " + ex.toString() );
} finally{
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
//--------------------------------------------
//##### 게시판에 글을 입력시 DB에 저장하는 메소드
public int insert( BoardRec rec ) throws BoardException
{
/************************************************
*/
ResultSet rs = null;
Statement stmt = null;
PreparedStatement ps = null;
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
String sql = "INSERT INTO article(article_id, group_id, sequence_no, posting_date, read_count, writer_name, title, content, password) "
+ "VALUES (SEQ_ARTICLE_ID_ARTICLE.nextVal, ?, ?, sysdate, ?,?,?,?,?)";
ps = con.prepareStatement(sql);
ps.setInt(1, rec.getGroupId());
ps.setString(2, rec.getSequenceNo());
ps.setInt(3, rec.getReadCount());
ps.setString(4, rec.getWriterName());
ps.setString(5, rec.getTitle());
ps.setString(6, rec.getContent());
ps.setString(7, rec.getPassword());
//
int result = ps.executeUpdate();
System.out.println(result + "행 입력 성공");
////////////////////////////////////////////////////////////////
stmt = con.createStatement();
String sqlSeq = "SELECT SEQ_ARTICLE_ID_ARTICLE.currval as articleId FROM dual"; // currval : 현재값 가져오기
rs = stmt.executeQuery(sqlSeq);
if(rs.next()) {
System.out.println("BoardDao 113line : " + rs.getInt("articleId"));
return rs.getInt("articleId");
}
///////////////////////////////////////////////////////////////
return -1;
}catch( Exception ex ){
throw new BoardException("게시판 ) DB에 입력시 오류 : " + ex.toString() );
} finally{
if( rs != null ) { try{ rs.close(); } catch(SQLException ex){} }
if( stmt != null ) { try{ stmt.close(); } catch(SQLException ex){} }
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
//--------------------------------------------
//##### 전체 레코드를 검색하는 함수
// 리스트에 보여줄거나 필요한 컬럼 : 게시글번호, 그룹번호, 순서번호, 게시글등록일시, 조회수, 작성자이름, 제목
// ( 내용, 비밀번호 제외 )
// 순서번호(sequence_no)로 역순정렬
public List<BoardRec> selectList(int firstRow, int endRow) throws BoardException
{
PreparedStatement ps = null;
ResultSet rs = null;
List<BoardRec> mList = new ArrayList<BoardRec>();
boolean isEmpty = true;
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
// String sql = "SELECT * FROM article ORDER BY sequence_no DESC";
String sql = "SELECT * FROM article WHERE article_id IN "
+ "(SELECT article_id FROM"
+ "(SELECT rownum as num, article_id FROM"
+ "(SELECT article_id FROM article ORDER BY article_id DESC))"
+ "WHERE (num >=? AND num <=?))"
// + "ORDER BY article_id DESC";
+ "ORDER BY sequence_no DESC";
ps = con.prepareStatement(sql);
ps.setInt(1, firstRow);
ps.setInt(2, endRow);
rs = ps.executeQuery();
while(rs.next()) {
BoardRec rec = new BoardRec();
rec.setArticleId(rs.getInt("article_id"));
rec.setGroupId(rs.getInt("group_id"));
rec.setSequenceNo(rs.getString("sequence_no"));
rec.setPostingDate(rs.getString("posting_date"));
rec.setReadCount(rs.getInt("read_count"));
rec.setWriterName(rs.getString("writer_name"));
rec.setTitle(rs.getString("title"));
mList.add(rec);
isEmpty = false;
}
if( isEmpty ) return Collections.emptyList();
return mList;
}catch( Exception ex ){
throw new BoardException("게시판 ) DB에 목록 검색시 오류 : " + ex.toString() );
} finally{
if( rs != null ) { try{ rs.close(); } catch(SQLException ex){} }
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
//--------------------------------------------
//##### 게시글번호에 의한 레코드 검색하는 함수
// 비밀번호 제외하고 모든 컬럼 검색
public BoardRec selectById(int id) throws BoardException
{
PreparedStatement ps = null;
ResultSet rs = null;
BoardRec rec = new BoardRec();
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
String sql = "SELECT * FROM article WHERE article_id=?";
ps = con.prepareStatement(sql);
ps.setInt(1, id);
rs = ps.executeQuery();
while(rs.next()) {
rec.setArticleId(rs.getInt("article_id"));
rec.setGroupId(rs.getInt("group_id"));
rec.setSequenceNo(rs.getString("sequence_no"));
rec.setPostingDate(rs.getString("posting_date"));
rec.setReadCount(rs.getInt("read_count"));
rec.setWriterName(rs.getString("writer_name"));
rec.setTitle(rs.getString("title"));
rec.setContent(rs.getString("content"));
}
return rec;
}catch( Exception ex ){
throw new BoardException("게시판 ) DB에 글번호에 의한 레코드 검색시 오류 : " + ex.toString() );
} finally{
if( rs != null ) { try{ rs.close(); } catch(SQLException ex){} }
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
//##### 전체 레코드 수를 검색
public int getTotalCount() throws BoardException {
PreparedStatement ps = null;
ResultSet rs = null;
int count = 0;
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
String sql = "SELECT count(*) cnt FROM article";
ps = con.prepareStatement(sql);
rs = ps.executeQuery();
while(rs.next()) {
count = rs.getInt("cnt");
}
return count;
}catch( Exception ex ){
throw new BoardException("게시판 ) 전체 레코드 수 검색시 오류 : " + ex.toString() );
} finally{
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
//--------------------------------------------
//##### 게시글 보여줄 때 조회수 1 증가
public void increaseReadCount( int article_id ) throws BoardException
{
PreparedStatement ps = null;
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
String sql = "UPDATE article SET read_count=(read_count+1) WHERE article_id=?";
ps = con.prepareStatement(sql);
ps.setInt(1, article_id);
ps.executeUpdate();
}catch( Exception ex ){
throw new BoardException("게시판 ) 게시글 볼 때 조회수 증가시 오류 : " + ex.toString() );
} finally{
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
//--------------------------------------------
//##### 게시글 수정할 때
// ( 게시글번호와 패스워드에 의해 수정 )
public int update( BoardRec rec ) throws BoardException
{
PreparedStatement ps = null;
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
String sql = "UPDATE article SET title=?, content=? WHERE article_id=? AND password=?";
ps = con.prepareStatement(sql);
ps.setString(1, rec.getTitle());
ps.setString(2, rec.getContent());
ps.setInt(3, rec.getArticleId());
ps.setString(4, rec.getPassword());
int result = ps.executeUpdate();
return result; // 나중에 수정된 수를 리턴하시오.
}catch( Exception ex ){
throw new BoardException("게시판 ) 게시글 수정시 오류 : " + ex.toString() );
} finally{
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
//--------------------------------------------
//##### 게시글 삭제할 때
// ( 게시글번호와 패스워드에 의해 삭제 )
public int delete( int article_id, String password ) throws BoardException
{
PreparedStatement ps = null;
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
String sql = "DELETE FROM article WHERE article_id=? AND password=?";
ps = con.prepareStatement(sql);
ps.setInt(1, article_id);
ps.setString(2, password);
int result = ps.executeUpdate();
return result; // 나중에 수정된 수를 리턴하시오.
}catch( Exception ex ){
throw new BoardException("게시판 ) 게시글 수정시 오류 : " + ex.toString() );
} finally{
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
//----------------------------------------------------
//##### 부모레코드의 자식레코드 중 마지막 레코드의 순서번호를 검색
// ( 제일 작은 번호값이 마지막값임)
public String selectLastSequenceNumber( String maxSeqNum, String minSeqNum ) throws BoardException
{
PreparedStatement ps = null;
ResultSet rs = null;
try{
con = DriverManager.getConnection( dbUrl, dbUser, dbPass );
String sql = "SELECT min(sequence_no) as minseq FROM article WHERE sequence_no < ? AND sequence_no >= ?";
ps = con.prepareStatement( sql );
ps.setString(1, maxSeqNum);
ps.setString(2, minSeqNum);
rs = ps.executeQuery();
if( !rs.next()) {
return null;
}
return rs.getString("minseq");
}catch( Exception ex ){
throw new BoardException("게시판 ) 부모와 연관된 자식 레코드 중 마지막 순서번호 얻어오기 : " + ex.toString() );
} finally{
if( rs != null ) { try{ rs.close(); } catch(SQLException ex){} }
if( ps != null ) { try{ ps.close(); } catch(SQLException ex){} }
if( con != null ) { try{ con.close(); } catch(SQLException ex){} }
}
}
}
5. mybatis 개념
※ JDBC의 단점 :
(1) 매번 DB와 연결하는 코드와 연결 해제하는 코드(close 메소드)를 반복적으로 작성해야 한다.
(2) 컬럼이 20라면 조회 결과에서 값을 가져올 때 매 번 컬럼의 이름을 적어야 한다.
(3) DB에서 컬럼이 하나라도 추가되면 java 파일과 jsp 파일을 수정해야 하고, 그 과정에서 서버를 닫아야 한다.
※ 마이바티스란?
: JDBC에서 SQL을 별도의 XML로 분리하여 관리하도록 돕는 프레임워크
※ 마이바티스 구조
: 각 테이블별로 CRUD를 담당하는 mapper가 다로 존재한다.
6. 마이바티스에서 DB 연결 후 목록 가져오기
(1) DB 설계
CREATE TABLE comment_tab(
comment_no number(10), -- 글 번호 (PK)
user_id varchar2(30), -- 사용자
comment_content varchar2(500), -- 글 내용
reg_date date, -- 작성 날짜
CONSTRAINT pk_comment_tab_comment_no PRIMARY KEY (comment_no)
);
(2) DB에 서너개의 데이터를 입력해둔다.
(3) DB와 연결하는 xml을 작성한다.
① src- mybatis.guest.model - Comment.java 파일 생성 후 변수 선언 및 Getter / Setter 생성하고, 직렬화 한다.
(직렬화 : 어떤 데이터를 찾을 수 있도록 가상 통로를 만들어주는 것)
public class Comment implements java.io.Serializable {
private long commentNo;
private String userId;
private String commentContent;
private String regDate;
② Mybatis-3-User-Guide_ko.pdf 파일의 5페이지 스크립트를 복사하여,
src - mybatis-config.xml 파일에 붙여넣기 한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
③ <dataSource> 태그 내에서 사용자 정보를 변경한다.
<property name="driver" value="oracle.jdbc.driver.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@192.168.0.17:1521:orcl"/>
<property name="username" value="scott"/>
<property name="password" value="tiger"/>
④ Mapper를 연결한다. (mapper가 여러개일 경우에도 등록 가능)
<mapper resource="mybatis/guest/mapper/CommentMapper.xml"/>
(4) src - mybatis - guest - mapper의 CommentMapper.xml 작성하기
① PDF의 7페이지 스크립트를 복사하여 CommentMapper.xml에 붙여넣기 한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" parameterType="int" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
② id와 데이터가 저장될 resultType을 작성하고, <mapper> 태그의 속성 중 namespace를 xml 파일명으로 바꾼다.
SQL문에서는 세미콜론( ; )을 사용하지 않는다.
<mapper namespace="CommentMapper">
<select id="selectComment" resultType="mybatis.guest.model.Comment">
select * from comment_tab
</select>
(5) CommentService.java 파일 작성
① CommentRepository.java 클래스의 객체를 생성한다.
private CommentRepository repo = new CommentRepository();
② CommentRepository.java 클래스의 selectComment( ) 메소드를 호출하고 그 결과값을 리턴한다.
(해당 메소드 작성 전이라 에러 발생할 수 있음)
public List<Comment> selectComment() {
return repo.selectComment();
}
(6) CommentRepository.java 파일 작성 - DB와 연결
※ JDBC : 연결담당 - Connection Class가 수행
※ Mybatis : 연결담당 - mybatis-config.xml의 값에 따라 SqlSession이 수행
① getSqlSessionFactory( ) 메소드에서는 Builder 메소드가 Factory를 만든다.
private SqlSessionFactory getSqlSessionFactory() {
InputStream inputStream;
try {
inputStream = Resources.getResourceAsStream("mybatis-config.xml");
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
SqlSessionFactory sessFactory = new SqlSessionFactoryBuilder().build(inputStream);
// builder가 공장을 만들고, 거기서 세션이 만들어져서 리턴된다.
return sessFactory;
}
② selectComment( ) 메소드 작성
public List<Comment> selectComment() {
// get~메소드의 결과인 공장으로 openSession() 메소드를 호출하면 세션을 얻을 수 있다.
SqlSession sqlSess = getSqlSessionFactory().openSession();
try {
return sqlSess.selectList("CommentMapper.selectComment");
}finally {
sqlSess.close();
// 실제적으로 연결을 닫는 것이 아니라 반납하는 것임
// 마이바티스는 미리 연결객체(Connection)을 몇 개 열어놓고 ConnectionPool을 관리함
}
}
(7) DB의 컬럼명과 java 파일의 변수명이 다르기 때문에, SQL에서 별칭을 부여하여 둘을 매핑 시켜준다.
→ 이러한 별칭 부여는 번거롭기 때문에 (8)에서 보다 효율적인 방법으로 변경하게 됨
select comment_no as commentNo,
user_id as userId,
comment_content as commentContent,
reg_date as regDate
from comment_tab
( 결과 )
(8) SQL문에서 컬럼명을 모두 기술하면서 별칭 부여 하려면 컬럼 수가 많을수록 번거롭기 때문에,
xml에서 환경설정을 통해 자동으로 바뀌게끔 한다.
① CommentMapper.xml에서 SQL을 다시 * 로 수정한다.
select * from comment_tab
② mybatis-config.xml 파일의 <configuration> 태그 바로 아래에 <settings> 태그를 작성해준다.
<!-- 테이블의 컬럼명과 VO의 멤버변수명이 다른 경우 (이름 규칙을 맞추었다면) -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
(결과) 목록이 정상적으로 출력됨
(9) 클래스에 별칭 부여하기
① CommentMapper.xml에서 데이터가 저장되는 resultType의 이름이 너무 길 경우 별칭을 부여하여 사용할 수 있다.
<select id="selectComment" resultType="mybatis.guest.model.Comment">
② mybatis-config.xml 환경설정 파일에서 <configuration> 태그 안에 <typeAliases> 태그로 별칭 부여
<!-- 클래스에 별칭 부여 -->
<typeAliases>
<typeAlias type="mybatis.guest.model.Comment" alias='comment'/>
</typeAliases>
③ CommentMapper.xml 에서 resultType의 이름을 별칭으로 변경
<select id="selectComment" resultType="comment">
(결과) 별칭 부여 후에도 데이터가 정상적으로 출력됨
(10) 드라이버 연결하는 정보에 아무나 접근하지 못하게 하면서 오탈자에 대한 대비를 하기 위해 properties 파일을 작성
→ DB관리자에게 해당 properties 파일만 제공하고, 계정 정보 변경이 필요할 경우 properties파일만으로 수정 가능
① src - 우클릭 - others - general - file - dbconnect.properties 파일 생성 (확장자명이 properties )
② properties 파일 내에 드라이버 연결을 위한 정보를 기술
# dbconnect.properties
dbcon.driver=oracle.jdbc.driver.OracleDriver
dbcon.url=jdbc:oracle:thin:@192.168.0.17:1521:orcl
dbcon.user=scott
dbcon.pass=tiger
③ mybatis-config.xml 파일에서 드라이버 연결에 관한 부분을 properties의 변수명으로 수정
<property name="driver" value="${dbcon.driver}"/>
<property name="url" value="${dbcon.url}"/>
<property name="username" value="${dbcon.user}"/>
<property name="password" value="${dbcon.pass}"/>
④ mybatis-config.xml 파일에서 <configuration> 태그 바로 아래에 위에서 작성한 파일과 연결하기 위한
<properties> 태그를 작성하고 속성의 resource값으로 properties 파일명을 적는다. (태그 작성 위치 중요함!)
<properties resource="dbconnect.properties" />
(결과) DB와 정상적으로 연결되어 목록이 화면에 출력됨
7. 마이바티스를 사용하여 데이터 입력하기
(1) insertCommentForm.jsp의 <form>의 자식 요소들은 Comment.java 파일의 변수들과 같은 이름을 name 속성의 값으로 가지므로, 사용자 입력값을 다음 페이지(insertCommentSave.jsp)로 전달할 수 있다.
private long commentNo;
private String userId;
private String commentContent;
private String regDate;
<form name="frm" action="insertCommentSave.jsp" >
<table>
<tr><td>글번호</td><td><input type="text" name="commentNo" size="3"/></td></tr>
<tr><td>사용자</td><td><input type="text" name="userId" size="15"/></td></tr>
<tr><td>메세지</td><td><textarea name="commentContent" rows="10" columns="40"></textarea></td></tr>
<tr><td><input type="submit" value="입력"/></td></tr>
</table>
</form>
(2) insertCommentSave.jsp 에서는 이전 화면에서 넘어오는 데이터를 Comment.java 클래스의 멤버변수로 지정한다.
<jsp:useBean id="comment" class="mybatis.guest.model.Comment">
<jsp:setProperty name="comment" property="*"/>
</jsp:useBean>
(3) insertCommentSave.jsp 에서 데이터가 들어있는 객체 comment를 인자로 하여
CommentService.java의 insertComment( ) 메소드를 호출한다.
<%
CommentService.getInstance().insertComment(comment);
%>
(4) CommentService.java 파일에서 리턴 타입이 없고 인자를 하나 받는 메소드 insertComment( ) 를 작성한다.
해당 메소드는 CommentRepository.java의 insertComment( ) 메소드를 호출하며
기존 인자를 그대로 넘겨주고 결과(int값이 올 예정)를 리턴 받는다.
public Integer insertComment(Comment c) {
return repo.insertComment(c);
}
(5) CommentRepository.java 에서 insertComment( ) 메소드를 작성
→ sql세션이 insert( ) 메소드를 수행하면
CommentMapper.xml 에서 id가 insertComment인 SQL을 찾고,
메소드의 인자로 넘어온 Comment c를 사용하여 DB에 사용자 입력값을 입력한다.
public Integer insertComment(Comment c) {
SqlSession sqlSess = getSqlSessionFactory().openSession();
try {
int result = sqlSess.insert("CommentMapper.insertComment", c);
if (result > 0) sqlSess.commit();
else sqlSess.rollback();
return result;
}finally {
sqlSess.close();
}
}
(6) CommentMapper.xml 에서 <insert> 태그를 사용하여
데이터 입력을 위해 insert( ) 메소드에서 호출할 SQL을 작성한다.
<insert id="insertComment" parameterType="comment">
INSERT INTO comment_tab(comment_no, user_id, comment_content, reg_date)
VALUES ( #{commentNO}, #{userID}, #{commentContent}, sysdate )
</insert>
(결과)
8. 마이바티스에서 작성한 글 확인하기
(1) listComment.jsp에서 작성자명을 클릭하면 해당 글 번호를 다음 페이지인 viewComment.jsp로 넘긴다.
<a href="viewComment.jsp?cId=<%=comment.getCommentNo()%>"><%= comment.getUserId()%> 님이 쓴 글</a>
(2) viewComment.jsp에서는 이전 페이지에서 넘어오는 게시물 번호를 넘겨받아 변수에 저장하고,
게시글 번호를 인자로 가져가는 CommentService.java의 selectCommentByPrimaryKey( ) 메소드를 호출하여
게시글 번호에 해당되는 데이터를 DB에서 가져와 Comment 객체에 담는다.
<!-- 키에 해당하는 글번호를 넘겨받아 서비스의 메소드 호출 -->
<%
long commentNo = Integer.parseInt( request.getParameter("cId"));
Comment comment = CommentService.getInstance().selectCommentByPrimaryKey(commentNo);
%>
(3) CommentService.java 에서 selectCommentByPrimaryKey( ) 메소드를 작성하여
CommentRepository.java의 selectCommentByPrimaryKey( ) 메소드를 호출한다 .
public Comment selecCommentByPrimaryKey(long commentNo) {
return repo.selectCommentByPrimaryKey(commentNo);
}
(4) CommentRepository.java 에서 selectCommentByPrimaryKey( ) 메소드를 작성한다.
인자로 받은 long타입의 데이터 commentId를 HashMap의 객체 담은 뒤,
sql세션이 HashMap의 값을 사용하여 selectOne( ) 메소드를 수행하게 한다.
selectOne( ) 메소드는 CommentMapper.xml에서 id가 selectCommentByPK인 SQL문을 찾아
hashmap을 인자로 하여 조건문을 수행하고 결과값을 Comment 타입으로 돌려준다.
public Comment selectCommentByPrimaryKey(long commentId) {
SqlSession sqlSess = getSqlSessionFactory().openSession();
try {
HashMap map = new HashMap();
map.put("commentId", commentId);
return sqlSess.selectOne("CommentMapper.selectCommentByPK", map);
}finally {
sqlSess.close();
}
}
(5) CommentMapper.xml 에서 <select> 태그에 id를 부여한 뒤,
인자로 사용하는 parameterType과 데이터를 받을 resultType, SQL문을 작성한다.
<select id="selectCommentByPK" parameterType="long" resultType="comment">
SELECT * comment_tab WHERE commnet_no=#{commentNo} {}안의 변수명은 무엇이든 가능하나 보기 편하려고 동일이름 부여
SELECT * from comment_tab WHERE comment_no=#{commentNo}
</select>
(6) <where> 태그와 <if> 태그를 사용하여 select문이 경우에 따라 조건식을 갖게 하기
commentId가 없을 경우에는 조건문 WHERE 절이 수행되지 않는다.
<select id="selectCommentByPK" parameterType="hashmap" resultType="comment">
SELECT * from comment_tab
<where>
<if test="commentId != null">
comment_no=#{commentId}
</if>
</where>
</select>
(결과)