Singleton パターンによるDB接続クラス

0
2360
views

Servlet からデータベースへの単純な問い合わせではベタ書きでDB接続を行いました。今回はそこから一歩進めて、DB接続の部品化を押し進めてみます。Singleton パターンを利用したDB接続専門のクラスを作成します。

リモートリポジトリ

プロジェクトを試したい場合は、以下のリモートリポジトリをクローンしてください。

ソースコード

SingletonDbConnectionProvider
│
├─src
│  └─dbconnect
│      │  DatabaseAccessService.java(メイン処理サーブレット)
│      │  SessionInvalidService.java(セッション破棄処理サーブレット)
│      │
│      ├─connector
│      │      ConnectionProvider.java(DB接続を提供するクラス)
│      │
│      ├─dao
│      │      FreshFishDao.java(DB問い合わせクラス)
│      │
│      ├─domain
│      │      FreshFish.java(鮮魚のドメインクラス)
│      │      FreshFishes.java(FreshFish のリストクラス)
│      │
│      └─properties
│              Constants.java(定数クラス)
│              Queries.java(テーブル情報クラス)
│
└─WebContent
    │  index.jsp(画面表示)
    │
    ├─META-INF
    │      context.xml(データソースの記述)
    │      MANIFEST.MF
    │
    └─WEB-INF
        └─lib
           postgresql-42.1.1.jar(JDBCドライバ)

DB接続を提供するクラス: ConnectionProvider.java

package dbconnect.connector;

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

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import dbconnect.properties.Constants;

public enum ConnectionProvider implements AutoCloseable {
	INSTANCE;
	public static ConnectionProvider getInstance() {
		return INSTANCE;
	}

	private static DataSource dataSource = null;
	private static Connection connection;
	private static final String datasourceName = Constants.FilePath.DATASOURCE_NAME;

	private synchronized static void init() throws NamingException {
		InitialContext intitCtx = new InitialContext();
		ConnectionProvider.dataSource 
                  = (DataSource) intitCtx.lookup(ConnectionProvider.datasourceName);
	}

	public synchronized static Connection getConnection() throws SQLException, NamingException {
		init();
		try (Connection connection = ConnectionProvider.connection) {
			if (ConnectionProvider.connection == null || ConnectionProvider.connection.isClosed())
				ConnectionProvider.connection = ConnectionProvider.dataSource.getConnection();
		}
		return connection;
	}

	public PreparedStatement getPreparedStatement(String sql) throws Exception {
		return getConnection().prepareStatement(sql);
	}

	public void commit() throws SQLException {
		try (Connection connection = ConnectionProvider.connection) {
			ConnectionProvider.connection.commit();
		}
	}

	public void rollback() throws SQLException {
		try (Connection connection = ConnectionProvider.connection) {
			ConnectionProvider.connection.rollback();
		}
	}

	@Override
	public void close() throws SQLException {
		try (Connection connection = ConnectionProvider.connection) {
			ConnectionProvider.connection.close();
		}
	}
}

DB問い合わせクラス: FreshFishDao.java

いわゆる DAO(Data Access Object)です。

ConnectionProvider.getInstance() メソッドで接続リソース(Connectionオブジェクト)を取得します。Connectionオブジェクトは、前述の ConnectionProvider クラスで設計されている通り Singleton(アプリ内でインスタンスが1つのみ) であることが保証されています。

また、try-with-resource によって 問い合わせの正常終了・異常終了にかかわらず、Connectionオブジェクトのクローズが保証されます。

package dbconnect.dao;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import dbconnect.connector.ConnectionProvider;
import dbconnect.domain.FreshFish;
import dbconnect.domain.FreshFishes;
import dbconnect.properties.Queries;

public class FreshFishDao {
	private static final String SQL_FIND_BY_FISH_CODE_OR_FISH_NAME
			= "SELECT * FROM " + Queries.TableName.FRESH_FISH
					+ " WHERE " + Queries.ColumnName.FISH_CODE + " = ? "
					+ " OR " + Queries.ColumnName.FISH_NAME + " = ? "
					+ " ORDER BY " + Queries.ColumnName.FRESH_FISH_ID + ";";

	public FreshFishes findByFishCodeOrFishName(String targetFishName, int targetFishCode)
			throws SQLException, Exception {
		List fishRecords = new ArrayList<>();

		try (ConnectionProvider connectionProvider = ConnectionProvider.getInstance();
				PreparedStatement statement = connectionProvider
						.getPreparedStatement(SQL_FIND_BY_FISH_CODE_OR_FISH_NAME);) {

			statement.setInt(1, targetFishCode);
			statement.setString(2, targetFishName);
			try (ResultSet resultSet = statement.executeQuery()) {
				while (resultSet.next()) {

					int freshFishId = resultSet.getInt(Queries.ColumnName.FRESH_FISH_ID);
					int fishCode = resultSet.getInt(Queries.ColumnName.FISH_CODE);
					String fishName = resultSet.getString(Queries.ColumnName.FISH_NAME);
					String district = resultSet.getString(Queries.ColumnName.DISTRICT);
					int unitPrice = resultSet.getInt(Queries.ColumnName.UNIT_PRICE);

					FreshFish fish = new FreshFish.Builder().freshFishID(freshFishId)
							.fishCode(fishCode).fishName(fishName).district(district)
							.unitPrice(unitPrice).build();

					fishRecords.add(fish);
				}
			}
		}

		fishRecords = Collections.unmodifiableList(fishRecords);
		return new FreshFishes(fishRecords);
	}
}

テーブル情報クラス: Queries.java

テーブル名や列名の情報を定数にしたクラスです。

package dbconnect.properties;

public final class Queries {
	private Queries() {
	}

	public static class TableName {
		public static final String FRESH_FISH = "fresh_fish";
	}

	public static class ColumnName {
		public static final String FRESH_FISH_ID = "fresh_fish_id";
		public static final String FISH_CODE = "fish_code";
		public static final String FISH_NAME = "fish_name";
		public static final String DISTRICT = "district";
		public static final String UNIT_PRICE = "unit_price";
	}
}

鮮魚のドメインクラス: FreshFish.java

テーブルに対応するドメインクラスです。Builder パターンを使った不変クラスにしています。

package dbconnect.domain;

public class FreshFish {
	private final int freshFishID;
	private final int fishCode;
	private final String fishName;
	private final String district;
	private final int unitPrice;

	public static class Builder {
		private int freshFishID;
		private int fishCode;
		private String fishName;
		private String district;
		private int unitPrice;

		public Builder freshFishID(int val) {
			this.freshFishID = val;
			return this;
		}

		public Builder fishCode(int val) {
			this.fishCode = val;
			return this;
		}

		public Builder fishName(String val) {
			this.fishName = val;
			return this;
		}

		public Builder district(String val) {
			this.district = val;
			return this;
		}

		public Builder unitPrice(int val) {
			this.unitPrice = val;
			return this;
		}

		public FreshFish build() {
			return new FreshFish(this);
		}
	}

	FreshFish(Builder builder) {
		this.freshFishID = builder.freshFishID;
		this.fishCode = builder.fishCode;
		this.fishName = builder.fishName;
		this.district = builder.district;
		this.unitPrice = builder.unitPrice;
	}

	public int freshFishIdValue() {
		return new Integer(this.freshFishID);
	}

	public int fishCodeValue() {
		return new Integer(this.fishCode);
	}

	public String fishNameValue() {
		return new String(this.fishName);
	}

	public String districtValue() {
		return new String(this.district);
	}

	public int unitPriceValue() {
		return new Integer(this.unitPrice);
	}
}

FreshFish のリストクラス: FreshFishes.java

FreshFish のリストを保持するクラスです。テーブルへの問い合わせ結果をここに詰め込みます。リストを取得する deepCopyListValue() を用意し、オブジェクトが不変であることを保証します。

package dbconnect.domain;

import java.util.ArrayList;
import java.util.List;

public class FreshFishes {
	private List fishes;

	public FreshFishes() {
	}

	public FreshFishes(List fishes) {
		this.fishes = fishes;
	}

	public List deepCopyListValue() {
		List copy = new ArrayList<>(this.fishes);
		return copy;
	}
}

メイン処理サーブレット: DatabaseAccessService.java

DB接続クラス/DB問い合わせクラス(DAO)/ドメインクラス、それぞれに明確に役割を分担させることで、メイン処理のサーブレットをすっきりさせることができます。

package dbconnect;

import java.io.IOException;
import java.sql.SQLException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import dbconnect.dao.FreshFishDao;
import dbconnect.domain.FreshFishes;

/**
 * Servlet implementation class ConnectionService
 */
@WebServlet("/DatabaseAccessService")
public class DatabaseAccessService extends HttpServlet {
	private static final long serialVersionUID = 1L;

	/**
	 * @see HttpServlet#HttpServlet()
	 */
	public DatabaseAccessService() {
		super();
	}

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doPost(request, response);
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		HttpSession session = request.getSession(false);
		session.removeAttribute("Message");
		session.removeAttribute("ErrorMessage");
		session.removeAttribute("FreshFishes");

		String targetFishName = "いわし";
		int targetFishCode = 501;

		FreshFishDao fishDao = new FreshFishDao();
		FreshFishes fishes = new FreshFishes();
		try {
			fishes = fishDao.findByFishCodeOrFishName(targetFishName, targetFishCode);
			session.setAttribute("Message", "データの接続に成功しました.");
		} catch (SQLException e) {
			e.printStackTrace();
			session.setAttribute("ErrorMessage", "データベースへの問い合わせに失敗しました.");
		} catch (Exception e) {
			e.printStackTrace();
			session.setAttribute("ErrorMessage", "データベースへの接続に失敗しました.");
		}
		session.setAttribute("FreshFishes", fishes);
		response.sendRedirect("index.jsp");
	}

}

画面表示: index.jsp

<?xml version="1.0" encoding="UTF-8" ?>
<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<%@ page import="dbconnect.domain.FreshFishes"%>
<%@ page import="dbconnect.domain.FreshFish"%>
<%@ page import="java.util.List"%>
<!DOCTYPE HTML SYSTEM "about:legacy-compat">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<link
  href="https://fonts.googleapis.com/earlyaccess/notosansjapanese.css"
  rel="stylesheet" />
<link rel="stylesheet"
  href="${pageContext.request.contextPath}/css/main.css" />
<title>DB接続テスト</title>
</head>
<body>
  <header>
    <h1>DB接続テスト</h1>
  </header>
  <article>
    <table>
      <tr>
        <th>接続対象のテーブル名</th>
        <th>レコードの抽出条件</th>
      </tr>
      <tr>
        <td>fresh_fish</td>
        <td>魚種コード = 501 もしくは 魚種名 = いわし</td>
      </tr>
    </table>

    <form action="DatabaseAccessService" method="post">
      <input type="submit" name="ExecFindAll" value="DB からデータを取得する" />
    </form>
    <%
    	String message = (String) session.getAttribute("Message");
    	String errorMessage = (String) session.getAttribute("ErrorMessage");
    	if (message == null)
    		message = "";
    	if (errorMessage == null)
    		errorMessage = "";
    %>
    <p class="message"><%=message%></p>
    <p class="error-message"><%=errorMessage%></p>
    <%
    	if (session.getAttribute("FreshFishes") != null) {
    %>
    <p>上記条件でDBから抽出したレコードの詳細は以下の通りです。</p>
    <table>
      <tr>
        <th>鮮魚ID</th>
        <th>魚種コード</th>
        <th>魚種名</th>
        <th>産地</th>
        <th>価格</th>
      </tr>
      <%
      	FreshFishes fishes = (FreshFishes) session.getAttribute("FreshFishes");
      		int i = 0;
      		for (FreshFish fish : fishes.deepCopyListValue()) {
      %>
      <tr>
        <td><%=fish.freshFishIdValue()%></td>
        <td><%=fish.fishCodeValue()%></td>
        <td><%=fish.fishNameValue()%></td>
        <td><%=fish.districtValue()%></td>
        <td><%=fish.unitPriceValue()%> 円</td>
      </tr>
      <%
      	}
      %>
    </table>
    <form action="SessionInvalidService" method="post">
      <input type="submit" name="ExecDeleteDisplayContents"
        value="表示結果をクリアする" />
    </form>
    <%
    	}
    %>
  </article>
</body>
</html>

アプリケーション実行結果