-100p

-10p

+10p

+100p

Tomcatサーブレットとデータベースの連携

Tomcatサーブレットとデータベース(SQLite3)との連携、JDBCの導入。
Javaのデザインパターン、BeanとDAOについて。

関連ページ 参考URL
前回ページ ではデータベースの基礎とSQLite3単体での操作方法を紹介した。
このページではTomcatのJSP/サーブレットとSQLite3の連携の仕方を学んでいく。Tomcatの知識はすでにある程度あるものとする。
今回のページは結構長い。

一応Windows環境で作っていくが、Tomcatのディレクトリ構成はLinuxもWindowsも大差ないんでLinuxでも応用はできると思う。

TomcatとSQLiteとの連携の準備

SQLite3をTomcatに配置

今回データベースの作成とテーブルの作成だけは事前に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もここにコピペ。

JDBCライブラリのダウンロード

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クラス全文

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の下とする。

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の下とする。

MangaDAOクラス全文

コード全文は次の通り。
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としての機能は次の通り。
 getAllList 
: mangaテーブルの全てのレコードを取得
 add 
: mangaテーブルに新しいレコードを追加
 delete 
: mangaテーブルの指定の名前のレコードを削除

関数ごとに詳しく解説。
    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つ。
 Rootクラス(アノテーション"/") 
: レコード全表示
 AddMangaクラス(アノテーション"/add_manga") 
: レコード追加
 DeleteMangaクラス(アノテーション"/delete_manga") 
: レコード削除
AddMangaとDelteMangaに関しては、処理を終えた後即時Rootにアクセスし直す。

ファイルの階層は全てbook/WEB-INF/src/rootの下とする。

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にアクセスして終了。

AddMangaクラス全文

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クラスにアクセスしている。

DeleteMangaクラス全文

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")); 
ここが唯一違う部分で、名前が一致したレコードを削除している。

Javaソースをコンパイル

方法は何でもいいので、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

-100p

-10p

+10p

+100p