関連ページ
参考URL
前回ページ
ではデータベースの基礎とSQLite3単体での操作方法を紹介した。
このページではTomcatのJSP/サーブレットとSQLite3の連携の仕方を学んでいく。Tomcatの知識はすでにある程度あるものとする。
今回のページは結構長い。
一応Windows環境で作っていくが、Tomcatのディレクトリ構成はLinuxもWindowsも大差ないんでLinuxでも応用はできると思う。
TomcatとSQLiteとの連携の準備
今回データベースの作成とテーブルの作成だけは事前にSQLite3で直接作っておく。
sqlite3の実行ファイルにアクセスし、book.dbを作成、その中にmangaテーブルを作成。
カラムはname, explainの2つのみ。
.open book.db
create table manga(name, explain);
sqlite3.exeと同一階層に、book.dbが作成されていることを確認。
Tomcat側、webappsの下にbookというフォルダを作り、ここを今回のテストページのルートディレクトリとする。
後ほどサーブレットのアノテーションを使ってroot.jspにアクセスさせるので、index.htmlもindex.jspも作っていない。
作成したbook.dbもここにコピペ。
JavaからSQLクライアントにアクセスするには、JDBCというライブラリが必要になる。
ただSQLite3の場合、昔はJDBCライブラリ1個で十分だったが、最近2つに分かれたようだ。(2024年4月現在)
こちらのサイト
にアクセスし、「sqlite-jdbc-」と「slf4j-api-」の最新verをダウンロード。
落としたJDBCとAPIのライブラリはbook/WEB-INF/libの下に置く。
テーブルに対応したbeanを作成
Javaプロジェクトでデータベースとの連携する際には、BeanとDAOを使った設計パターンが推奨されている。
ここで言うBeanは、テーブルのデータ構造をJava側で表したクラスのこと。
Beanはシンプルで最小の構成が望ましい。
ここではmangaテーブルに対応するMangaクラスを作成し、ファイルの階層はbook/WEB-INF/src/beanの下とする。
Mangaクラスのコード全文は下の通り。
package bean;
public class Manga implements java.io.Serializable
{
private String name;
private String explain;
public Mange(
String name,
String explain)
{
this.name = name;
this.explain = explain;
}
public String getName()
{
return name;
}
public String getExplain()
{
return explain;
}
public void setName(String id)
{
this.name = id;
}
public void setExplain(String title)
{
this.explain = title;
}
}
見ての通り、メンバ変数にはMangaテーブルのカラム要素であるname, explainしか存在しない。
関数もコンストラクタと、各メンバ変数に対応したgetterとsetterしかない。
余談だがサーバサイドのコーディング文化では、変数だけでなく関数も最初の文字を小文字で表す。
個人的にはすごい違和感あるけど今回はその文化に倣う。
DAOでデータベースと連携
DAOはData Access Objectの略で、データベースとJavaの橋渡しをこのクラスに担当させる。
それ以外の機能は極力持たせない。
汎用性のためJDBCにアクセスするDAOクラスと、それを継承してSQL文を発行するMangaDAOクラスを別々に実装する。
DAOクラスのファイルの階層はbook/WEB-INF/src/daoの下とする。
コード全文は次の通り。
package dao;
import javax.servlet.ServletContext;
import java.sql.Connection;
import java.sql.DriverManager;
public class DAO
{
protected ServletContext context;
public DAO(ServletContext context)
{
this.context = context;
}
public Connection getConnection() throws Exception
{
Class.forName("org.sqlite.JDBC");
var conn = DriverManager.getConnection("jdbc:sqlite:" + context.getRealPath("book.db"));
return conn;
}
}
関数ごとに詳しく解説。
public DAO(ServletContext context)
{
this.context = context;
}
後ほどサーブレットから呼ばれるコンストラクタ部分。メンバ変数にServletContextを代入してるだけになる。
下のgetConnection()関数ですぐ使用する。
public Connection getConnection() throws Exception
{
Class.forName("org.sqlite.JDBC");
var conn = DriverManager.getConnection("jdbc:sqlite:" + context.getRealPath("book.db"));
return conn;
}
こちらは後ほど継承クラス先から呼ばれる。JDBCにアクセスし、そのインスタンスをConnectionとして返す。
データベース接続は例外エラーが出やすい部分ではあるので、関数にthrows Exceptionを付けて後ほどサーブレット側で例外処理も書く。
Class.forName("org.sqlite.JDBC");
この部分はSQLite3のJDBC内のクラスにアクセスする際の決まり文句になる。
これがMySQLだと文字列が"com.mysql.jdbc.Driver"になったりする。
DriverManager.getConnection("jdbc:sqlite:" + context.getRealPath("book.db"));
ここでパスを指定しデータベースに接続している。
今回はローカル環境にあるbook.dbにアクセスしてるが、代わってサーバURLを打ち込みサーバにアクセスすることも可能。
context.getRealPath
、contextはコンストラクタで代入したServletContextになる。
ServletContext.getRealPathで、このホームページのルートディレクトリから続く絶対パスを取得できる。
つまりbookのルートディレクトリにあるbook.dbというファイルにアクセスしている。
DAOでSQL文の発行
DAOクラスを継承し、SQL文を発行するMangaDAOクラスを作成する。
ファイルの階層はDAOクラスと一緒のbook/WEB-INF/src/daoの下とする。
コード全文は次の通り。
package dao;
import bean.Manga;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletContext;
public class MangaDAO extends DAO
{
private Connection con;
public void createConnection() throws Exception
{
if(con == null)
{
con = getConnection();
}
}
public void closeConnection() throws Exception
{
if(con != null)
{
con.close();
con = null;
}
}
public MangaDAO(ServletContext context) {
super(context);
}
public List<Manga> getAllList() throws Exception
{
PreparedStatement st;
st = con.prepareStatement
("select * from manga");
ResultSet rs = st.executeQuery();
List<Manga> list = new ArrayList<Manga>();
while(rs.next())
{
Manga a = new Manga(
rs.getString("name"),
rs.getString("explain")
);
list.add(a);
}
st.close();
return list;
}
public void add(String name, String explain) throws Exception
{
PreparedStatement st = con.prepareStatement
("insert into manga(name, explain) values(?, ?)");
st.setString(1, name);
st.setString(2, explain);
st.executeUpdate();
st.close();
}
public void delete(String name) throws Exception
{
PreparedStatement st = con.prepareStatement
("delete from manga where name = ?");
st.setString(1, name);
st.executeUpdate();
st.close();
}
}
用意したSQLとしての機能は次の通り。
関数ごとに詳しく解説。
private Connection con;
public void createConnection() throws Exception
{
if(con == null)
{
con = getConnection();
}
}
まだデータベースと接続していない場合は継承元のgetConnectionを呼びConnectionを生成、それを保存する。
すでにデータベースと接続している場合は何もしない。
public void closeConnection() throws Exception
{
if(con != null)
{
con.close();
con = null;
}
}
Conneciton.closeで必要なくなったデータベースとの接続を切る。
ページを全て読み込み終わったら必ずこの処理を実行する必要がある。
public MangaDAO(ServletContext context) {
super(context);
}
MangaDAOのコンストラクタ。親クラスのコンストラクタを呼んでいるだけ。
public List<Manga> getAllList() throws Exception
{
PreparedStatement st;
st = con.prepareStatement
("select * from manga");
ResultSet rs = st.executeQuery();
List<Manga> list = new ArrayList<Manga>();
while(rs.next())
{
Manga a = new Manga(
rs.getString("name"),
rs.getString("explain")
);
list.add(a);
}
st.close();
return list;
}
全レコード取得の関数、throws Exceptionを付けて例外対応をしてある。
まず
con.prepareStatement("select * from manga");
で発行したいSQL文をプリコンパイルし、PreparedStatementに保存。
実際のSQL文発行のコードは
st.executeQuery();
の部分で、結果をResultSetに保存している。
"select * from manga"
のSQL文を発行しているので、結果はbook.dbの全てのレコードになる。
while(rs.next())
、ここで取得した全レコードを一行ずつ抽出して回している。
それぞれの抽出したレコードは、BeanのMangaクラスに変換しArrayListに追加。
rs.getString("name")
と
rs.getString("explain")
で分かる通り、レコードに対してgetStringを打つことで、そのカラムの情報を取得できる。
st.close();
ここで必要なくなったプリコンパイルObjectを破棄。
SQLでやりたい事が終わったら、すみやかにこのcloseでリソースを開放することが推奨されている。
この際紐づいたResultSetのインスタンスも同時に開放される。
ドキュメント読むと、一応ガベージコレクションを待つだけでも普通に開放されるようではある。
最後に
return list;
で、Mangaクラスに変換した全レコードを返している。
public void add(String name, String explain) throws Exception
{
PreparedStatement st = con.prepareStatement
("insert into manga(name, explain) values(?, ?)");
st.setString(1, name);
st.setString(2, explain);
st.executeUpdate();
st.close();
}
レコード追加の関数、こちらもthrows Exceptionを付けてある。
引数はレコードに必要なnameとexplainのカラムの情報が指定してある。
con.prepareStatement("insert into manga(name, explain) values(?, ?)");
でレコード追加のSQL文をプリコンパイルし保存。
values(?, ?)
部分が特徴的で、JavaからSQLに可変の数値や文字列を与えたい場合、まずこの?という文字列でプリコンパイルする。
st.setString(1, name);
と
st.setString(2, explain);
、この部分がSQLのパラメーターの代入コード。
第一引数に1を指定した場合、プリコンパイルされたSQL文内の最初の?の位置に第二引数が代入される。
C言語文化圏と違い、最初のインデックスは0ではなく1なので注意。
st.executeUpdate();
でSQL文を発行し、最後に
st.close();
で必要なくなったPrepareStatementを閉じて終了。
public void delete(String name) throws Exception
{
PreparedStatement st = con.prepareStatement
("delete from manga where name = ?");
st.setString(1, name);
st.executeUpdate();
st.close();
}
レコード削除の関数、こちらもthrows Exceptionを付けてある。
やってることは概ねレコード追加と変わらない。SQL文が違うくらい。
引数に渡されたnameと一致したレコードを、データベースから削除している。
サーブレットの実装
最初に述べた通りTomcatの基本的な知識はあるものとしてるので、要点だけ簡潔に解説していく。
実装するのはレコード全表示、レコード追加、レコード削除のサーブレットの3つ。
AddMangaとDelteMangaに関しては、処理を終えた後即時Rootにアクセスし直す。
ファイルの階層は全てbook/WEB-INF/src/rootの下とする。
RootクラスではdoGetとdoPostの2つを用意している。
これの理由は、AddMangaとDeleteMangaからrequest.getRequestDispatcherでRootクラスにアクセスしてくるため。
response.sendRedirect("/")ではRootにアクセスできなかったので、その代替え処理になる。
処理は全く一緒なためcommonRequestという関数を用意しそこを通している。
package root;
import bean.Manga;
import dao.MangaDAO;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;
@WebServlet(urlPatterns = {"/"})
public class Root extends HttpServlet
{
private void commonRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
try {
MangaDAO dao = new MangaDAO(getServletContext());
dao.createConnection();
List<Manga> allList = dao.getAllList();
String resultHtml = "";
for (Manga manga : allList) {
resultHtml += manga.getName() + ", " + manga.getExplain();
resultHtml += "<br>";
}
request.setAttribute("allList", resultHtml);
dao.closeConnection();
request.getRequestDispatcher("/root.jsp").include(request, response);
} catch(Exception e) {
System.err.println(e.getMessage());
}
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
commonRequest(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
commonRequest(request, response);
}
}
データベースは通信状態などから例外エラーが出やすいので、一応try catchで例外処理をしている。
ここではコンソールにエラー文を出してるだけだが、真面目に作るならエラーページに飛ばすべきだと思う。
MangaDAO dao = new MangaDAO(getServletContext());
で初期化を実行、
dao.createConnection();
でbook.dbと接続。
List allList = dao.getAllList();
で全てのレコードをJavaクラスとして取得、
for (Manga manga : allList)
で一つずつ回す。
resultHtml += manga.getName() + ", " + manga.getExplain();
で漫画の名前と説明をテンプレに代入しresultHtmlに追加。
request.setAttribute("allList", resultHtml);
で、jsp側の${allList}に結果を代入する。
用事が終わったら
dao.closeConnection();
で忘れずにデータベースとの接続を切る。
最後に
request.getRequestDispatcher("/root.jsp")
でroo.jspにアクセスして終了。
package root;
import dao.MangaDAO;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;
@WebServlet(urlPatterns = {"/add_manga"})
public class AddManga extends HttpServlet
{
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
try {
MangaDAO dao = new MangaDAO(getServletContext());
dao.createConnection();
dao.add(request.getParameter("name"), request.getParameter("explain"));
dao.closeConnection();
request.getRequestDispatcher("/").forward(request, response);
} catch(Exception e) {
System.err.println(e.getMessage());
}
}
}
dao.createConnection();
まではRootと一緒。
dao.add(request.getParameter("name"), request.getParameter("explain"));
で名前と説明を指定し新しくレコードを追加している。
dao.closeConnection();
で忘れずにデータベースとの接続を切る。
request.getRequestDispatcher("/").forward(request, response);
で"/"のアノテーションに接続、つまりRootクラスにアクセスしている。
package root;
import dao.MangaDAO;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;
@WebServlet(urlPatterns = {"/delete_manga"})
public class DeleteManga extends HttpServlet
{
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
try {
MangaDAO dao = new MangaDAO(getServletContext());
dao.createConnection();
dao.delete(request.getParameter("name"));
dao.closeConnection();
request.getRequestDispatcher("/").forward(request, response);
} catch(Exception e) {
System.err.println(e.getMessage());
}
}
}
あんまりやってる事がAddMangaと変わらない。
dao.delete(request.getParameter("name"));
ここが唯一違う部分で、名前が一致したレコードを削除している。
方法は何でもいいので、src以下の全てのソースファイルをコンパイルしてclassesフォルダに配置する。
自分はいちいちコマンドを打ち込むのが面倒なので、src直下にbatファイルを作り対応した。
下はそのコード全文。
set CLASSPATH=C:\SQLite\tomcat\lib\servlet-api.jar
javac -encoding utf-8 -d ..\classes -sourcepath . bean\*.java
javac -encoding utf-8 -d ..\classes -sourcepath . dao\*.java
javac -encoding utf-8 -d ..\classes -sourcepath . root\*.java
CLASSPATHに必要なのはTomcatのservlet-api.jarだけで、jsp-api.jarもJDBCへのパスもいらない。
batをダブルクリックし、実行した結果は次の通り。
jspの実装
root.jspのソースの中身は次の通り。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Test Database</title>
<style type="text/css">
body {
background-color:blanchedalmond;
}
</style>
</head>
<body>
<h2>Mangaテーブルだよ!</h2>
<h3>全レコード</h3>
${allList}
<h3>レコードの追加</h3>
<form action="add_manga" method="post">
[名前]<input type="text" name="name">
[説明]<input type="text" name="explain">
<button type="submit">レコードを追加</button>
</form>
<h3>レコードの削除</h3>
<form action="delete_manga" method="post">
[名前]<input type="text" name="name">
<button type="submit">レコードを削除</button>
</form>
</body>
</html>
${allList}
の部分でサーブレットから受け取った全レコードを表示。
その下にレコードを追加とレコード削除のボタンを置いている。
それぞれ
form action="add_manga"
と
form action="delete_manga"
で対応するアノテーションのサーブレットにジャンプする。
見た目は下の感じで、自由にレコードの追加と削除がホームページ上で出来る。
今回のテストでは全レコードの表示とレコードの追加/削除しか実装しなかったが、SQLで実行できることは全てサーブレットでも出来る。
一番最初にSQLite3単体でやったテーブルの追加も、やろうと思えばサーブレット側で実装が可能となっている。
0
0