
Servlet からデータベースへの単純な問い合わせではベタ書きでDB接続を行いました。今回はそこから一歩進めて、DB接続の部品化を押し進めてみます。Singleton パターンを利用したDB接続専門のクラスを作成します。
リモートリポジトリ
プロジェクトを試したい場合は、以下のリモートリポジトリをクローンしてください。
Singleton パターンによるDB接続クラス – GitHub
https://github.com/momotar/SingletonDbConnectionProvider
https://github.com/momotar/SingletonDbConnectionProvider
ソースコード
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>