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では DataSource
や JdbcTemplate
を自動注入(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()
:INSERT
→update(sql, …)
updateCar()
:UPDATE
→update(sql, …)
deleteCar()
:DELETE
→update(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 の場合、Controller で CarRepository
を呼び出し、取得したデータを 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-jdbc と mysql-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で表示)を守ると、わかりやすく保守しやすい設計を維持できます。