JDBC+Servlet ベースの EC サイト「黒馬商城」の機能拡張と改善

1. プロジェクトの出典

本プロジェクトは、GitHub 上で ruanwenjun 氏が公開している学習用プロジェクトに基づいています。リポジトリ: https://github.com/ruanwenjun/JAVAWeb-Project/tree/master/ネット商城プロジェクト

2. 実行環境と結果

2.1 実行環境

  • OS: Windows 11 x64
  • ハードウェア: 11th Gen Intel Core i5-1135G7 @ 2.40GHz (2.42 GHz), 8GB RAM
  • IDE: IntelliJ IDEA 2024.3.1.1
  • バックエンド: Java Servlet
  • フロントエンド: JSP, JavaScript, CSS
  • Java バージョン: Oracle OpenJDK 21.0.5
  • Tomcat バージョン: Tomcat 9.0.85
  • データベース: MySQL 8.0
  • コネクションプール: C3P0

2.2 実行結果

(画像省略:実際の実行画面を表示)

3. プロジェクト構成

レイヤーアーキテクチャ

  • プレゼンテーション層: Servlet がリクエストとレスポンスを処理
  • ビジネスロジック層: Service クラスが業務ロジックを実装
  • データアクセス層: DAO クラスがデータアクセスを担当
  • ユーティリティ: cn.ruanwenjun.utils パッケージ内

(画像省略:プロジェクト構造のスクリーンショット)

主要機能モジュール

3.1 ユーザーモジュール (UserServlet)
  • ユーザー登録
  • ログイン
  • 画像認証コード生成 (CheckImgServlet)
3.2 商品モジュール (ProductServlet)
  • トップページ商品表示(人気商品、新着商品)
  • カテゴリ別表示
  • 商品詳細表示
  • ページネーション対応商品一覧
3.3 カートモジュール (CartServlet)
  • カートへの商品追加
  • 数量更新
  • 商品削除
  • カート内全削除
3.4 注文モジュール (OrderServlet)
  • 注文作成
  • 配送先・連絡先確認
  • 注文確定
  • 注文履歴表示
  • 支払い処理(サードパーティ決済API連携)
  • 支払いコールバック処理 (CallBackServlet)
3.5 管理モジュール (AdminServlet)
  • 注文管理: 全注文表示、詳細確認
  • 商品管理: 全商品表示
  • 商品追加機能 (AdminAddProductServlet)

4. 主な問題点と改善策

4.1 カートデータの永続化

元のプロジェクトではカートデータがデータベースに保存されておらず、ブラウザを閉じて再度開くとカート内の商品が失われていました。ユーザー体験向上のため、カート変更のたびにデータベースへ同期保存する機能を追加しました。

コードを表示
package cn.ruanwenjun.dao;

import cn.ruanwenjun.domain.CartItem;
import cn.ruanwenjun.utils.DataSourceUtils;
import org.apache.commons.dbutils.QueryRunner;

import java.sql.SQLException;
import java.util.List;

public class CartDao {
    private QueryRunner queryRunner = new QueryRunner(DataSourceUtils.getDataSource());

    public void saveCartItem(String userId, CartItem item) throws SQLException {
        String query = "INSERT INTO cart_items (user_id, product_id, quantity, added_time) VALUES (?, ?, ?, NOW())";
        queryRunner.update(query, userId, item.getProduct().getId(), item.getQuantity());
    }

    public List<CartItem> getCartItems(String userId) throws SQLException {
        String query = "SELECT c.*, p.* FROM cart_items c JOIN product p ON c.product_id = p.id WHERE c.user_id = ?";
        // 実際の実装では ResultSetHandler を使用して結果をマッピング
        return null;
    }
}

(画像省略:カート機能の改善前後の比較)

4.2 お気に入り機能の追加

多くのECサイトが提供するお気に入り機能を実装しました。ユーザーはカートに入れずに商品を保存できます。

データベーススキーマ

コードを表示
CREATE TABLE `favorite` (
  `id` varchar(32) NOT NULL COMMENT 'お気に入りID',
  `uid` varchar(32) NOT NULL COMMENT 'ユーザーID',
  `pid` varchar(32) NOT NULL COMMENT '商品ID',
  `favorite_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'お気に入り登録日時',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uid_pid_idx` (`uid`, `pid`),
  KEY `pid_idx` (`pid`),
  CONSTRAINT `fk_favorite_user` FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON DELETE CASCADE,
  CONSTRAINT `fk_favorite_product` FOREIGN KEY (`pid`) REFERENCES `product` (`pid`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='ユーザーお気に入りテーブル';

ドメインモデル

コードを表示
package cn.ruanwenjun.domain;

import java.util.Date;

public class Favorite {
    private String id;
    private User user;
    private Product product;
    private Date favoriteTime;

    public Favorite() {}

    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public User getUser() { return user; }
    public void setUser(User user) { this.user = user; }

    public Product getProduct() { return product; }
    public void setProduct(Product product) { this.product = product; }

    public Date getFavoriteTime() { return favoriteTime; }
    public void setFavoriteTime(Date favoriteTime) { this.favoriteTime = favoriteTime; }
}

DAO 層

コードを表示
package cn.ruanwenjun.dao;

import cn.ruanwenjun.domain.Product;
import cn.ruanwenjun.utils.DataSourceUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

import java.sql.SQLException;
import java.util.List;

public class FavoriteDao {

    public void addFavorite(String id, String uid, String pid) throws SQLException {
        QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());
        String query = "INSERT INTO favorite (id, uid, pid) VALUES (?, ?, ?)";
        runner.update(query, id, uid, pid);
    }

    public void removeFavorite(String uid, String pid) throws SQLException {
        QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());
        String query = "DELETE FROM favorite WHERE uid = ? AND pid = ?";
        runner.update(query, uid, pid);
    }

    public List<Product> findFavoritesByUid(String uid) throws SQLException {
        QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());
        String query = "SELECT p.* FROM product p JOIN favorite f ON p.pid = f.pid WHERE f.uid = ? ORDER BY f.favorite_time DESC";
        return runner.query(query, new BeanListHandler<>(Product.class), uid);
    }

    public boolean isFavorite(String uid, String pid) throws SQLException {
        QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());
        String query = "SELECT COUNT(*) FROM favorite WHERE uid = ? AND pid = ?";
        Long count = (Long) runner.query(query, new ScalarHandler(), uid, pid);
        return count > 0;
    }

    public int getFavoriteCount(String uid) throws SQLException {
        QueryRunner runner = new QueryRunner(DataSourceUtils.getDataSource());
        String query = "SELECT COUNT(*) FROM favorite WHERE uid = ?";
        Long count = (Long) runner.query(query, new ScalarHandler(), uid);
        return count.intValue();
    }
}

Service 層

コードを表示
package cn.ruanwenjun.service;

import cn.ruanwenjun.dao.FavoriteDao;
import cn.ruanwenjun.domain.Product;

import java.sql.SQLException;
import java.util.List;
import java.util.UUID;

public class FavoriteService {
    private FavoriteDao favoriteDao = new FavoriteDao();

    public boolean addFavorite(String uid, String pid) {
        try {
            if (favoriteDao.isFavorite(uid, pid)) {
                return false;
            }
            String id = UUID.randomUUID().toString();
            favoriteDao.addFavorite(id, uid, pid);
            return true;
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }

    public boolean removeFavorite(String uid, String pid) {
        try {
            favoriteDao.removeFavorite(uid, pid);
            return true;
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }

    public List<Product> getFavoritesByUser(String uid) {
        try {
            return favoriteDao.findFavoritesByUid(uid);
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }

    public boolean isFavorite(String uid, String pid) {
        try {
            return favoriteDao.isFavorite(uid, pid);
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }

    public int getFavoriteCount(String uid) {
        try {
            return favoriteDao.getFavoriteCount(uid);
        } catch (SQLException e) {
            e.printStackTrace();
            return 0;
        }
    }
}

Servlet 層

コードを表示
package cn.ruanwenjun.web.servlet;

import cn.ruanwenjun.domain.Product;
import cn.ruanwenjun.domain.User;
import cn.ruanwenjun.service.FavoriteService;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

public class FavoriteServlet extends BasicServlet {

    public void addFavorite(HttpServletRequest request, HttpServletResponse response) throws IOException {
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            response.getWriter().write("{\"success\":false, \"message\":\"ログインが必要です\"}");
            return;
        }
        String pid = request.getParameter("pid");
        FavoriteService service = new FavoriteService();
        boolean success = service.addFavorite(user.getUid(), pid);
        response.getWriter().write("{\"success\":" + success + "}");
    }

    public void removeFavorite(HttpServletRequest request, HttpServletResponse response) throws IOException {
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            response.getWriter().write("{\"success\":false, \"message\":\"ログインが必要です\"}");
            return;
        }
        String pid = request.getParameter("pid");
        FavoriteService service = new FavoriteService();
        boolean success = service.removeFavorite(user.getUid(), pid);
        response.getWriter().write("{\"success\":" + success + "}");
    }

    public void myFavorites(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            response.sendRedirect(request.getContextPath() + "/login.jsp");
            return;
        }
        FavoriteService service = new FavoriteService();
        List<Product> favoriteList = service.getFavoritesByUser(user.getUid());
        request.setAttribute("favoriteList", favoriteList);
        request.getRequestDispatcher("/favorite.jsp").forward(request, response);
    }

    public void isFavorite(HttpServletRequest request, HttpServletResponse response) throws IOException {
        User user = (User) request.getSession().getAttribute("user");
        if (user == null) {
            response.getWriter().write("{\"isFavorite\":false}");
            return;
        }
        String pid = request.getParameter("pid");
        FavoriteService service = new FavoriteService();
        boolean isFavorite = service.isFavorite(user.getUid(), pid);
        response.getWriter().write("{\"isFavorite\":" + isFavorite + "}");
    }
}

(画像省略:お気に入り機能の動作例)

5. まとめ(課題と考察)

この JDBC+Servlet プロジェクトを拡張する上で最大の困難は、元のコードベースを理解することでした。実務経験の少ない大学2年生の筆者にとって、このプロジェクトはかなり大規模に感じられました。GitHub から clone してローカル環境(IDEA, Tomcat, JDK, MySQL)にデプロイするまでに約1時間。初回実行時に多くのリソースが欠落していることが判明し、コードの解読と修正にさらに2日間を要しました。何度か諦めてよりシンプルなプロジェクトに乗り換えようと考えましたが、前期に JDBC+Servlet を使った図書管理システムを自力で完成させていた経験から、このECサイトも必ず理解できると信じて取り組みました。

改善点を検討する過程で、ユーザー視点に立って「このECサイトにどんな機能があれば良いか」を考えることの重要性を実感しました。開発者からユーザーへの視点転換により、プロジェクトの不足点がより明確になりました。この経験は、今後のシステム設計と開発において非常に有益な視点を提供してくれました。

タグ: JDBC Servlet C3P0 MySQL Tomcat

5月24日 06:56 投稿