1. なぜWebアプリケーションにデータベースが必要なのか?

これまでメモリ上だけでデータを保持していたWebアプリケーションは、アプリケーションサーバの再起動やマシンの電源OFFでデータが消えてしまいます。
そこで、「永続化」と呼ばれる仕組みが必要になります。
ファイルに保存する方法もありますが、大量のデータ管理検索機能などに優れたデータベースを利用するケースが一般的です。


2. Spring BootでのJDBC設定

2.1. 依存関係の追加

Spring BootでJDBCを利用するには、以下2つをpom.xmlに追加します。

<dependencies>
    <!-- JDBC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jdbc</artifactId>
    </dependency>

    <!-- MySQL Connector/J -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

2.2. application.properties の設定

Spring Bootでは application.propertiesにデータベース接続情報を記述します。
例えば以下のように設定します。

spring.datasource.url=jdbc:mysql://localhost:3306/sip_a?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B9
spring.datasource.username=newuser
spring.datasource.password=0
# 追加オプションなど

この設定により、Spring Bootは起動時に自動でDataSource(Connectionプール)を生成し、JdbcTemplate などから利用できるようにしてくれます。


3. DAOパターンをSpring Bootで実装する

3.1. DAO(Repository)クラスの基本構造

Spring Bootでは DataSourceJdbcTemplate を自動注入(DI:依存性注入)できます。
また、接続を開閉する処理を手動で書く必要がなく、JdbcTemplate が内部でConnectionを取り出して使います。

典型的には、以下のように@Repositoryアノテーションを付けたクラスをRepositoryとします。

@Repository
public class CarRepository {
  
  private final JdbcTemplate jdbcTemplate;

  // SpringがDataSourceを自動でDIし、JdbcTemplateを生成
  public CarRepository(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  // SELECT, INSERT, UPDATE, DELETEなどのメソッドを定義
}

3.2. JdbcTemplateを使ったCRUDサンプル

JdbcTemplateでは、以下のようなメソッドを使ってSQLを実行します。

  • queryForObject():単一行の取得
  • query():複数行の取得(ResultSet→RowMapper)
  • update():INSERT/UPDATE/DELETEなど行数変更系

RowMapper

SELECTで取得したレコードの各列を、JavaのオブジェクトへマッピングするためにRowMapperを定義します。

RowMapper<CarBean> carRowMapper = (rs, rowNum) -> {
    CarBean car = new CarBean();
    car.setCarId(rs.getInt("car_id"));
    car.setName(rs.getString("name"));
    car.setPrice(rs.getInt("price"));
    car.setDeletedAt(rs.getString("deleted_at"));
    return car;
};

3.3. SQLインジェクション対策(プレースホルダ)

JdbcTemplateのクエリ方法はすべてプレースホルダ?)とパラメータを分離するため、文字列連結によるSQLインジェクションを防止します。

例:

String sql = "SELECT * FROM cars WHERE car_id = ?";
CarBean car = jdbcTemplate.queryForObject(sql, carRowMapper, carId);

このように ? の部分に carId が安全にバインドされ、SQLインジェクション攻撃(例: 1 OR 1=1)を防ぎます。


4. サンプル:CarテーブルへのCRUD

以下では、先の「cars」テーブルを対象にしたCRUD例を示します。

spring08_JDBC/
├── src/
│   ├── main/
│   │   ├── java/com/example/demo/
│   │   │   ├── CarAppApplication.java  # メインアプリケーションクラス
│   │   │   ├── model/
│   │   │   │   ├── CarBean.java  # Carテーブルのモデル(JavaBean)
│   │   │   ├── repository/
│   │   │   │   ├── CarRepository.java  # 車情報を扱うリポジトリ(データベースアクセス層)
│   │   │   ├── controller/
│   │   │   │   ├── CarController.java  # 車情報を管理するコントローラー
│   │   │   ├── error/
│   │   │   │   ├── CustomErrorController.java  # カスタムエラーハンドリング
│   │   ├── resources/
│   │   │   ├── application.properties  # アプリケーションの設定
│   │   │   ├── templates/  # Thymeleafテンプレートフォルダ
│   │   │   │   ├── cars/
│   │   │   │   │   ├── list.html  # 車の一覧ページ
│   │   │   │   │   ├── detail.html  # 車の詳細ページ
│   │   │   │   ├── error/
│   │   │   │   │   ├── 404.html  # 404エラーページ
│   │   │   ├── static/  # 静的リソース
│   ├── test/
│   │   ├── java/com/example/demo/
│   │   │   ├── CarRepositoryTest.java  # JUnitテスト
├── pom.xml  # Mavenのビルド設定
└── mvnw, mvnw.cmd  # Maven Wrapper(Mavenをインストールせずに実行できるようにする)

4.1. Model(JavaBeans)定義

package com.example.demo.model;

import java.io.Serializable;

public class CarBean implements Serializable {
	private int carId;
	private String name;
	private int price;
	private String deletedAt; // 省略可

	public int getCarId() {
		return carId;
	}

	public void setCarId(int carId) {
		this.carId = carId;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getPrice() {
		return price;
	}

	public void setPrice(int price) {
		this.price = price;
	}

	public String getDeletedAt() {
		return deletedAt;
	}

	public void setDeletedAt(String deletedAt) {
		this.deletedAt = deletedAt;
	}

	@Override
	public String toString() {
		return "CarBean [carId=" + carId + ", name=" + name + ", price=" + price + ", deletedAt=" + deletedAt + "]";
	}
}

4.2. Repository実装例

package com.example.demo.repository;

import com.example.demo.model.CarBean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class CarRepository {

    private final JdbcTemplate jdbcTemplate;

    public CarRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    private final RowMapper<CarBean> carRowMapper = (rs, rowNum) -> {
        CarBean car = new CarBean();
        car.setCarId(rs.getInt("car_id"));
        car.setName(rs.getString("name"));
        car.setPrice(rs.getInt("price"));
        car.setDeletedAt(rs.getString("deleted_at"));
        return car;
    };

    // (1) SELECT count(*)
    public int countCars() {
        String sql = "SELECT COUNT(*) FROM cars";
        return jdbcTemplate.queryForObject(sql, Integer.class);
    }

    // (2) SELECT * FROM cars
    public List<CarBean> findAll() {
        String sql = "SELECT * FROM cars";
        return jdbcTemplate.query(sql, carRowMapper);
    }

    // (3) SELECT * FROM cars WHERE car_id = ?
    public CarBean findById(int carId) {
        String sql = "SELECT * FROM cars WHERE car_id = ?";
        return jdbcTemplate.queryForObject(sql, carRowMapper, carId);
    }

    // (4) INSERT
    public int addCar(CarBean car) {
        String sql = "INSERT INTO cars (name, price) VALUES (?, ?)";
        return jdbcTemplate.update(sql, car.getName(), car.getPrice());
    }

    // (5) UPDATE
    public int updateCar(CarBean car) {
        String sql = "UPDATE cars SET name = ?, price = ?, deleted_at = ? WHERE car_id = ?";
        return jdbcTemplate.update(sql, car.getName(), car.getPrice(), car.getDeletedAt(), car.getCarId());
    }

    // (6) DELETE - 物理削除の例
    public int deleteCar(int carId) {
        String sql = "DELETE FROM cars WHERE car_id = ?";
        return jdbcTemplate.update(sql, carId);
    }

    // 例: 検索 (LIKE)
    public List<CarBean> searchByName(String keyword) {
        String sql = "SELECT * FROM cars WHERE name LIKE ?";
        return jdbcTemplate.query(sql, carRowMapper, "%" + keyword + "%");
    }
}

説明

  • 注1)このコードは 依存性注入(Dependency Injection, DI) の典型的な例です。CarRepository クラスは JdbcTemplate を必要としますが、自分でインスタンスを作成せず、コンストラクタの引数として受け取ります。Spring Boot では JdbcTemplate は Springの管理するBean(コンポーネント) として定義されています。そのため、@Repository を付与した CarRepository は SpringのDIコンテナ によって管理され、JdbcTemplate が自動的に注入 されます。これにより、CarRepository は データベース接続の設定を意識せずに利用できる ため、テストや保守性が向上します。
  • 注2)RowMapper クラスは、Spring JDBC (JdbcTemplate) を使用する際に、データベースの ResultSet を Java オブジェクトに変換するためのインターフェース です。
  • 注2)->(アロー演算子)は、ラムダ式の「引数」と「処理」を区切る記号 です。

構文

(引数) -> { 処理 }


左側(引数リスト): rs, rowNum → メソッドに渡される値

右側(処理): { ... } → 実行するコード

  • countCars()queryForObject(sql, Integer.class) で単一の値(int)を取得。JdbcTemplate.queryForObject() を使用するときに、Integer.class を指定する理由は、データベースから取得する値の型を明示的に指定する必要があるからです。
  • findAll()query(sql, RowMapper) で複数行をリストに取得
  • findById(): 単一行を queryForObject で取得
  • addCar()INSERTupdate(sql, …)
  • updateCar()UPDATEupdate(sql, …)
  • deleteCar()DELETEupdate(sql, …)

Spring Bootが DataSource を自動的に生成し、JdbcTemplate をコンストラクタインジェクションするため、手動で Connection con = ...; con.close(); のような記述は不要です。

4.3. 動作テスト

Spring Bootでは、以下のようにテストクラス(JUnit)やメインクラスから呼び出し、動作を確認できます。
たとえばJUnitテストで書く場合:

package com.example.demo;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.example.demo.model.CarBean;
import com.example.demo.repository.CarRepository;

@SpringBootTest
class CarRepositoryTest {

	@Autowired
	CarRepository carRepository;

	@Test
	void testCountCars() {
		int cnt = carRepository.countCars();
		System.out.println("Car count = " + cnt);
		assert cnt >= 0; // 0以上であることを確認
	}

	@Test
	void testFindAll() {
		List<CarBean> cars = carRepository.findAll();
		for (CarBean car : cars) {
			System.out.println(car);
		}
		assert cars != null; // nullではないことを確認
	}
}

アプリケーションを起動してテストを実行すると、コンソールに結果が出力されます。


5. Webアプリケーションとデータベースの連携

5.1. ControllerとTemplateの例

Thymeleaf + Spring Boot の場合、ControllerCarRepository を呼び出し、取得したデータを Model に格納 →Thymeleaf テンプレートで表示、という流れになります。

例)一覧表示

CarController.java

package com.example.demo.controller;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.model.CarBean;
import com.example.demo.repository.CarRepository;

@Controller
@RequestMapping("/cars")
public class CarController {

	private final CarRepository carRepository;

	public CarController(CarRepository carRepository) {
		this.carRepository = carRepository;
	}

	@GetMapping("") 
	public String list(Model model) {
		// 全件取得
		List<CarBean> cars = carRepository.findAll();
		model.addAttribute("cars", cars);
		return "cars/list"; // "cars/list.html" に対応
	}

	@GetMapping("/{carId}")
	public String detail(@PathVariable("carId") int carId, Model model) {
		CarBean car = carRepository.findById(carId);
		model.addAttribute("car", car);
		return "cars/detail";
	}
}

list.html (Thymeleaf)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Car List</title>
</head>
<body>
<h1>Car List</h1>
<p>Total: <span th:text="${cars.size()}"></span></p>
<table>
  <thead>
    <tr><th>ID</th><th>Name</th><th>Price</th></tr>
  </thead>
  <tbody>
    <tr th:each="car : ${cars}">
      <td th:text="${car.carId}"></td>
      <td th:text="${car.name}"></td>
      <td th:text="${car.price}"></td>
    </tr>
  </tbody>
</table>
</body>
</html>

detail.html (Thymeleaf)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Car Detail</title>
</head>
<body>
<table>
  <h1>車の詳細情報</h1>
    
    <div>
        <p><strong>ID:</strong> <span th:text="${car.carId}"></span></p>
        <p><strong>名前:</strong> <span th:text="${car.name}"></span></p>
        <p><strong>価格:</strong> <span th:text="${car.price}"></span> 円</p>
        <p><strong>削除日時:</strong> <span th:text="${car.deletedAt} ?: 'なし'"></span></p>
    </div>

    <a href="/cars">← 車一覧へ戻る</a>
  </tbody>
</table>
</body>
</html>

404.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 - ページが見つかりません</title>
    <style>
        /* cssは省略 */
    </style>
</head>
<body>
    <h1>404 - ページが見つかりません</h1>
    <p>指定された車の情報は存在しません。</p>
    <a href="/cars">車一覧へ戻る</a>
</body>
</html>

5.2. ログイン処理サンプル

JSP/Servletの例で作ったLoginDaoを、Spring Boot + Thymeleafでも同様に書き換え可能です。
login_userテーブルを用意して、ID/PASSをチェックするイメージです。

LoginRepository.java

@Repository
public class LoginRepository {

    private final JdbcTemplate jdbcTemplate;

    public LoginRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public boolean login(String loginId, String password) {
        // COUNT(*)でチェック
        String sql = "SELECT COUNT(*) FROM login_user WHERE login_id = ? AND password = ?";
        Integer cnt = jdbcTemplate.queryForObject(sql, Integer.class, loginId, password);
        return (cnt != null && cnt > 0);
    }
}

LoginController.java

@Controller
public class LoginController {

    private final LoginRepository loginRepo;

    public LoginController(LoginRepository loginRepo) {
        this.loginRepo = loginRepo;
    }

    @GetMapping("/login")
    public String showLoginForm() {
        return "loginForm"; // loginForm.html
    }

    @PostMapping("/login")
    public String doLogin(
        @RequestParam("id") String id,
        @RequestParam("pass") String pass,
        HttpSession session
    ) {
        if (id == null || id.isEmpty() || pass == null || pass.isEmpty()) {
            return "redirect:/login-error";
        }
        if (loginRepo.login(id, pass)) {
            // ログイン成功
            session.invalidate();
            session = session.getSession(true);
            session.setAttribute("id", id);
            return "member-only2"; // or redirect
        } else {
            return "redirect:/login-error";
        }
    }
}

loginForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form th:action="@{/login}" method="post">
  ユーザーID:<input type="text" name="id" required><br>
  パスワード:<input type="password" name="pass" required><br>
  <button type="submit">ログイン</button>
</form>
</body>
</html>


まとめ

  • spring-boot-starter-data-jdbcmysql-connector-j を使うと、JdbcTemplate を通じて手軽にMySQLへアクセスできる
  • application.properties に接続情報(URL, username, password)を記述するだけで、Spring BootがDataSourceを用意してくれる
  • DAOパターン(Repository)クラスでは JdbcTemplateコンストラクタインジェクションし、select / insert / update / delete などの処理をメソッド化
  • SQLインジェクション対策は、?(プレースホルダ)+ パラメータ指定で実装できる
  • SELECTには query, queryForObject などを使用 → RowMapper でオブジェクトに変換
  • INSERT/UPDATE/DELETEには update(...) を使用 → 更新した行数を返す
  • Controller で Repository(DAO)を呼び出し、Thymeleaf テンプレートへ結果を渡す流れでWebアプリが完成

以上で、Spring Boot + JDBC によるデータベース連携の基本が押さえられます。
JSP/Servlet時代に書いていた接続・切断コードtry-catch-finallyは、Spring Boot環境では JdbcTemplate が内部でマネジメントするため、シンプルに実装できます。

アプリケーションの規模が大きくなっても、この構造(RepositoryクラスごとにCRUDメソッドを定義 → Controllerで呼び出し → Thymeleafで表示)を守ると、わかりやすく保守しやすい設計を維持できます。

「JDBCでデータベースのデータを活用する」 最後までお読みいただきありがとうございます。