Struts1 [データベース]

データベースMySQL に連動したWeb アプリをStruts で作成します。Struts 1.2 からはデータベース用のリソースであるDataSource が非推奨に なってしまったので、Jakarta-commons のDBCP を使う事にします。
struts [validator]のサンプル(bbs02.war) をベースに、実装してゆきます。bbs05 として使います。今回のサンプル(bbs05.war)
更新日 2016-02-13

MySQL5 のセットアップ

データベースエンジンにはいろいろありますが、とりあえず有名どころでフリーのMySQL を使ってみましょう。詳しいインストールはコチラを参照してください。

データベースとユーザーの新規作成

データベース名bbs_db を作成します。続いて操作用の一般ユーザーを作ります。ユーザー名は"user_bbs"、パスワードは"pass_bbs" とします。
/usr/local/mysql/bin/mysql -u root -p
create database bbs_db;

// ユーザーの作成
grant USAGE on *.* to user_bbs@ホスト名 identified by 'pass_bbs';
grant all privileges on bbs_db.* to user_bbs@ホスト名;
flush privileges;
exit;
まずグローバル権限を設定し、次にbbs_db に対する権限を設定しています。USAGE は権限無しで、all は全権限ありです。これで データベースbbs_db にだけフルアクセスできるユーザーが出来ました。ホスト名にはMySQL にアクセスしてくるクライアントのそれを 指定します。同じコンピュータ内にあればlocalhost でいけそうですが(実際Windows ではOK)、Linux ではホスト名になってしまい MySQL へのログインに失敗するので、しっかり指定します。セキュリティに不安がなければホスト名に'%'を指定して、全てのホストからの 接続を許可する選択も可能です。

DBCP の設定

Tomcat からJDBC を使ってデータベースに接続する為の設定をします。アクションクラスからDriverManager で直接JDBC ドライバを読み込んで データベースに接続する事もできますが、Jakarta プロジェクトの一つであるDBCP (Data Base Connection Pooling?) を使うとありがちな オーバーヘッドを抑えてくれてパフォーマンスが良いです。またデータベースの接続をTomcat サイドに行わせる事で、各Web アプリケーションにおいて データベースの種類を意識しないコーディングが可能になります。
struts1.1 以前ではDataSource というデータベース用のリソースがありましたが、これは1.2 以降では非推奨となり削除されています。ユーザーは 自力で実装するか、DBCP といった別モノを使う必要があります。DataSource を利用したアプリやサンプル・本は多数ありましたが、一切お奨めできなくなりました。 それどころかstruts1.2 ではstruts-config.xml のDataSouces に何か書くだけでエラーになります。私もこれにハマりました:-<

JDBC ドライバ

まずは何といってもMySQL をJava から使う為のコネクタをインストールしておく必要があります。MySQL のDownload Connector/J 5.0から mysql-connector-java-5.0.5.tar.gz をダウンロードします。展開した中にあるmysql-connector-java-5.0.5-bin.jar だけを$CATALINA_HOME/common/lib にコピーします。このjar を環境変数 CLASSPATH に含める必要はありません。
cd /tmp
wget "http://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.0.5.tar.gz/
from/http://mirror.mysql-partners-jp.biz/"

tar zxvf mysql-connector-java-5.0.5.tar.gz
cd mysql-connector-java-5.0.5
cp mysql-connector-java-5.0.5-bin.jar $CATALINA_HOME/lib
次いでDBCP はJakarta Commons プロジェクトからダウンロードします。同じプロジェクト中のCollectionspoolLogging にも依存してるので必須です。それぞれダウンロードしたら解凍して $CATALINA_HOME/common/lib にjar ファイルをコピーしておきます。
cd /tmp
wget "http://www.meisei-u.ac.jp/mirror/apache/dist/jakarta/commons/\
dbcp/binaries/commons-dbcp-1.2.1.tar.gz"

tar zxvf commons-dbcp-1.2.1.tar.gz
cd commons-dbcp-1.2.1
cp commons-dbcp-1.2.1.jar $CATALINA_HOME/lib

cd /tmp
wget "http://sunsite.tus.ac.jp/pub/apache/jakarta/commons/\
collections/binaries/commons-collections-3.2.tar.gz"


tar zxvf commons-collections-3.2.tar.gz
cd commons-collections-3.2
cp commons-collections-3.2.jar $CATALINA_HOME/lib

cd /tmp
wget "http://sunsite.tus.ac.jp/pub/apache/jakarta/commons/\
pool/binaries/commons-pool-1.3.tar.gz"

tar zxvf commons-pool-1.3.tar.gz
cd commons-pool-1.3
cp commons-pool-1.3.jar $CATALINA_HOME/lib

cd /tmp
wget "http://sunsite.tus.ac.jp/pub/apache/jakarta/commons/\
logging/binaries/commons-logging-1.1.tar.gz"

tar zxvf commons-logging-1.1.tar.gz
cd commons-logging-1.1
cp commons-logging*.jar $CATALINA_HOME/lib

Tomcat のserver.xml を編集する

DBCP はTomcat で動くリソースなので、$CATALINA_HOME/conf/server.xml に記述します。各Web アプリケーションからは、このリソースを経由して データベースに接続する事になります。GlobalNamingResources エレメントの下にResouce タグを新規作成します。
<GlobalNamingResources>

	<!-- 追加 -->
	<Resource name="MySQL_DBCP" auth="Container" type="javax.sql.DataSource"
				 maxActive="100" maxIdle="30" maxWait="10000"
				 username="user_bbs" password="pass_bbs"
				 driverClassName="com.mysql.jdbc.Driver"
				 url="jdbc:mysql://127.0.0.1:3306/bbs_db?autoReconnect=true&amp;
useUnicode=true&amp;characterEncoding=SJIS" />
username, password 属性にMySQL にログインする為のID、パスワードを設定します。

個別のアプリ専用の接続設定の場合

アプリサイドのコンテキストXML に直接リソースを設定する事もできます。当然、そのアプリ内のみでの 利用となります。

コンテキストXML ファイルを作成する。

server.xml のGlobalNamingResources エレメントに設定したリソースを各Web アプリから利用する為には、ResourceLink を用いてローカルにマッピングする必要が あります。このContext 情報はserver.xml の中のHost エレメント内にも記述できますが、Tomcat5.5 では別ファイルで記述する事が推奨されています。 アプリの保存されているディレクトリ配下のMETA-INF ディレクトリにcontext.xml というファイルを用意します。
<Context path="/bbs05" docBase="bbs05" debug="0" reloadable="true">
	<ResourceLink name="MySQL_DBCP" global="MySQL_DBCP" type="javax.sql.DataSource"/>
</Context>
これでWeb アプリのアクションクラスといったJava コード内からサーブレットコンテキストの属性MySQL_DBCP 名で利用できるようになります。 (逆にResourceLink でなくResource 自体を記述すれば、このアプリ内のみで利用可能なリソースを用意できます)

アプリケーションのweb.xml を編集

最期にアプリケーションの/WEB-INF/web.xml にも登録しておきます。<web-app> タグ中のラストに、server.xml に書いたResouce のname, auth, type 属性値を以下のように書きます。
<resource-ref>
	<res-ref-name>MySQL_DBCP</res-ref-name>
	<res-type>javax.sql.DataSource</res-type>
	<res-auth>Container</res-auth>
</resource-ref>
これでWeb アプリのアクションクラスといったJava コード内からサーブレットコンテキストの属性MySQL_DBCP 名で利用できるようになります。

簡易掲示板を作る

ハンドルネームとコメント、日時をデータベースに保存してゆく簡易掲示板を作成します。データベースに連動させれば、 永続的な掲示板になります。

bbs.jsp を改良する

入力フォームとデータベースから一覧表示する二つがメインとなります。まずフォームにハンドルネームおよびコメント用のテキストボックスを設置します。 次いでデータベースに入力された過去のコメントを一覧出力するlogic:iterator 部を記述します。
<%@page contentType="text/html; charset=Shift_JIS" pageEncoding="Shift_JIS" %>
<%@ taglib uri="/tags/struts-html" prefix="html" %>
<%@ taglib uri="/tags/struts-bean" prefix="bean" %>

<html:html locale="true">
<head>
	<title>bbs.jsp</title>
	<html:javascript formName="bbs_dvf" />
</head>
<body>
	<div>
		<html:form action="/bbs" method="POST" onsubmit="return validateBbs_dvf(this);">
			<html:text property="name" size="10" />
			<html:text property="comment" size="30" />
			<html:submit property="submit" value="実行" />
		</html:form>
	</div>

	<logic:notEmpty name="MemoList" scope="request" >
		<table border="2">
			<logic:iterate id="memotable" name="MemoList" scope="request">
				<tr>
					<td width="120"><bean:write name="memo" property="name" /></td>
					<td width="400"><bean:write name="memo" property="comment" /></td>
					<td width="180"><bean:write name="memo" property="time" /></td>
				</tr>
			</logic:iterate>
		</table>
	</logic:notEmpty>
</body>
</html:html>
最初のjsp リクエスト時にはアクションが実行されないので、当然リクエストスコープにMemoList インスタンスは ありません。そこでlogic:iterator は例外を発するので、login:notEmpty を用いてチェックしてる訳です。なお logic:iterate はコレクションオブジェクトにシーケンシャルアクセスする為のタグライブラリです。

struts-config.xml の修正

アクションフォームのプロパティがname, comment となりましたので、変更します。
<form-bean name="bbs_dvf" type="org.apache.struts.validator.DynaValidatorForm">
	<form-property name="name" type="java.lang.String" />
	<form-property name="comment" type="java.lang.String" />
</form-bean>

varidation.xml の修正

プロパティ名が変わったので、コチラも修正します。とりあえずアクションフォームのプロパティ名name に関して required(入力チェック) とします。
<form-validation>
    <formset>
        <form name="bbs_dvf">
            <field property="name" depends="required">
            </field>
        </form>
    </formset>
</form-validation>

データベースとテーブルを準備する

MySQL 上に今回使うためのデータベースならびにテーブルを作成します。データベース名はbbs_db、テーブル名はmemotable です。テーブルの構成は インデックス番号となるid、ハンドル名を格納するname、コメントを収めるcomment、そして作成日時が自動で収められるタイムスタンプtime です。 mysql というコマンドでコンソールに入って操作します。
mysql -u root -p
create database bbs_db;
use bbs_db;
create table memotable (
	id int auto_increment,
	name varchar(32),
	comment text,
	time timestamp,
	index(id)
);

memotable クラスを作成する

データベースから取得したデータを格納する為のクラスです。C++ なら単に構造体で済ませるところですが、Java では何でもクラスです。
// Compile command. You must be current file's directry.
// javac -classpath "../lib/struts.jar;../../../../common/lib/servlet-api.jar;"
 -d "." MemoTable.java

package mypackage;

import java.io.*;
import java.util.*;
import java.text.*;

public class MemoTable implements Serializable
{
	private String m_strName = "";
	private String m_strComment = "";
	private String m_strTime = "";

	public void setName( String value){		this.m_strName = value;		}
	public String getName(){	return this.m_strName;	}

	public void setComment( String value){	this.m_strComment = value;	}
	public String getComment(){		return this.m_strComment;	}

	public void setTime( String value){		this.m_strTime = value;		}
	public String getTime(){	return this.m_strTime;	}
}
プロパティへのアクセスメソッドだけの単純なクラス。コンパイルしておきましょう。

bbsAction.java を改造する

アクションフォームに格納されたリクエストデータをデータベースに入力します。続いてデータベースより抽出した 過去のコメントをリクエストスコープに割り当てたコレクションオブジェクトに格納します。
// Compile command. You must be current file's directry.
// javac -classpath "../lib/struts.jar;../../../../common/lib/servlet-api.jar;"
 -d "." bbsAction.java

package mypackage;

import mypackage.MemoTable;

import java.sql.*;
import java.util.*;
import javax.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.struts.*;
import org.apache.struts.action.*;
import org.apache.struts.validator.*;
import java.io.*;
import javax.naming.*;

public class bbsAction extends Action
{
	public ActionForward execute
	(
		ActionMapping mapping,
		ActionForm form,
		HttpServletRequest req,
		HttpServletResponse res
	)
	throws Exception
	{
		InitialContext ic = new InitialContext();
		DataSource refDataSource = (DataSource)ic.lookup("java:comp/env/MySQL_DBCP");
		Connection refConnection = refDataSource.getConnection();

		// データベースに登録
		{
			DynaValidatorForm myForm = (DynaValidatorForm)form;

			String strName = (String)myForm.get("name");
			String strComment = (String)myForm.get("comment");

			String strSQL = "insert into memotable(name, comment) values(\'"
							 + strName + "\',\'" + strComment + "\')";
			PreparedStatement ps = refConnection.prepareStatement( strSQL );
			ps.executeUpdate();
		}


		// データベースからデータを抽出
		{
			ArrayList refList = new ArrayList();

			if (refConnection != null)
			{
				String strSQL = "select * from memotable order by time desc";
				PreparedStatement ps = refConnection.prepareStatement( strSQL );
				ResultSet rs = ps.executeQuery();
				while (rs.next())
				{
					MemoTable refMemoTable = new MemoTable();
					{
						refMemoTable.setName( rs.getString("name") );
						refMemoTable.setComment( rs.getString("comment") );
						refMemoTable.setTime( rs.getString("time") );
					}
					refList.add( refMemoTable );
				}
			}

			req.setAttribute( "MemoList", refList);
		}

		return mapping.findForward( "success" );
	}
}
PerparedStatement の実行にはexcuteUpdate() とexcuteQuery() の二つがあり、値を返す場合は後者です。戻り値のResultSet にはレコードが 保持されていて、最初は先頭のひとつ前を指しています。ResultSet::next() を繰り返してシーケンシャルにアクセスします。memotable クラスに 値を格納して行きます。
後はコンパイルしてTomcat をリスタートすれば完了です。

動作確認

ブラウザで/bbs05/bbs.jsp にアクセスして確認してください。

エラーについて

発生しがちなエラーについてです。

Cannot load JDBC driver class 'com.mysql.jdbc.Driver'

ダウンロードしたMySQL Connecter/J の中にmysql-connector-java-3.1.10-bin.jar 以外の他のjar(例えばmysql-connector-java-3.1.10-bin-g.jar) などが ありますが、これを両方$CATALINA_HOME/common/lib に入れるとこのエラーが出ます。

日本語が??? になる

データベースの文字セットがutf8 でないと出るようです。

Cannot get a connection, pool exhausted

アクセスが集中している訳でも無く、しかもある日突然エラーとなって回復しないトラブルです。単純にコネクションリソースの枯渇を意味し、 明示的にコネクションリソースを閉じないコーディングが原因です。参考文献