前回は「DTO(Data Transfer Object)」について学びました。DTOを使いデータの受け渡しを整理し、コードをシンプルにすることができました。DTOを使うことで、「ControllerとModel」や「ControllerとView」の間でデータを効率よく受け渡すことができます。データベースの大量のデータも扱えるようになりましたね。

今回は、Javaプログラムとデータベースの連携について学びます。Webアプリケーションでは、データを保存・取得する仕組みが不可欠です。データベースとの接続方法や、データを適切にやり取りする方法を理解し、実際にデータを操作できるようになりましょう。

今回の学習の重点をオレンジの枠線で図示します。

Javaプログラムとデータベースとの連携

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

これまでに作成してきたWebアプリケーションのデータはアプリケーションサーバのメモリ上にだけ存在しています。ということはアプリケーションサーバを再起動したり、電源を切るとデータは消えてしまいます。

そこで、何らかの方法でデータを外部記憶装置に保存する必要が出てきます。(難しい表現ではデータの永続化 【perpetuation】ともいいます)その際にデータベースとファイルという候補がありますが、データベースのほうが優れているということは、この研修でもデータベースのところで学んだ通りです。

調べてみましょう

データベースはファイルに比べてどのような点が優れているのでしょうか?

調べたことのメモ:

2. JDBCとは何か?

JDBC【Java DataBase Connectivity】とは、JavaプログラムからデータベースにアクセスするためのAPI【Application Programming Interface】です。もしもJDBCが無かったら、以下の図のようにデータベースの種類ごとにJavaのプログラムを変えなくてはなりません。

もしもJDBCが無かったら

JDBCはあたかも「多言語を話す通訳者」のようなものです。

JDBCがデータベースの違いを吸収するため、どのようなデータベースに対しても、あるいはデータベースを変更しても、同じ手順で接続することができるのです。異なるデータベース製品は、DBMSの実装などで差異がありますが、JDBCはそれらの差異を隠蔽し、Javaプログラマが統一的な方法でデータベースにアクセスできるようにします。

下図はその概念図です。

JDBCがデータベースの違いを吸収する
JDBCがデータベースの違いを吸収する

当社の研修の受講者の方々は既にJDBCを使う準備ができているはずです。

pom.xmlの依存関係に以下のように書かれていれば準備OKです。

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>

3. JavaSEでJDBCを扱う

下図の通り、Javaプログラムでデータベースにアクセスし、SELECT文を実行するには、以下の3つのオブジェクトを使用します

①データベースとの接続(セッション)を表すConnection

②SQL文を表すPreparedStatement

③データベースの結果セットを表すResultSet

SELECT文を実行するための3つのオブジェクト

Connection

  • 役割: データベースへの接続を管理します。
  • 使用法: データベースへの接続を確立するために使用され、この接続を通じてSQL文を実行します。データベース接続はコストがかかるリソースです。以降のコードでは、接続を一度開いて、複数の異なる操作で再利用しています。これにより、接続と切断のオーバーヘッドが減少し、アプリケーションのパフォーマンスが向上します。

PreparedStatement:

  • 役割: パラメータ化されたSQL文を表します。
  • 使用法: SQLインジェクション攻撃(後述)を防ぐため、または同じSQL文を繰り返し実行する際に効率的に使用されます。PreparedStatementを使用することで、SQL文にパラメータを動的に挿入できます。

ResultSet:

  • 役割: SQLクエリから返されたデータを保持します。
  • 使用法: クエリの実行結果として得られたデータセットを操作し、データを読み取るために使用されます。

3.1 DAOパターンでデータベース処理を専門のクラスに任せる

1つのクラスの中にデータベース処理を書いた場合、メソッドが肥大化して理解しにくく、メンテナンスしにくいプログラムになってしまいます。そこで、データベース処理を専門のクラスに任せることにします。ビジネスロジックとデータベース処理をそれぞれ別のクラスに担当させるのです。

この考え方をDAOパターン【Data Access Object Pattern】といいます。

パターンというのは以前、MVCパターンのところでもでてきたデザインパターンのことです。

その概念を図示すると以下の図になります。

DAOパターン

3.2 データベースの接続・切断の処理をスーパークラスとして切り出す

もしも、個々のクラスでデータベースに接続する処理とデータベースから切断する処理を書くとメンテナンス性が低下してしまいます。つまり、同じ処理をアチラコチラに書くと修正が大変になります。例えば、MySQLのパスワードを変更した際にその全てのファイルの記述箇所を探して変更する必要が生じてしまいますね。(DRY【Don't Repeat Yourself】原則に反するといいます。)

そこで、本書では下図のようにデータベースの接続と切断を行うSuperDaoクラスを作成して、carsやcustomersなどの各テーブルとの操作はSuperDaoクラスのサブクラスとして作成することにします。

「全てのサブクラスは一種のSuperDaoクラス」と言えますか?

言えますね。

ですので、このクラス設計で問題ないでしょう。

また、テーブルごとにSELECT、INSERT、UPDATE、DELETEの処理がまとまっているのが直感的にわかりやすいですね。

superDAO
superDAOクラス

調べてみましょう

データベースの接続情報は、開発環境・テスト環境・本番環境で異なる場合が多いため、後から変更しやすいように管理することが重要です。

もし接続情報をソースコード内に直接記述すると、変更のたびにソースコードを編集し、再コンパイルしなければなりません。そのため、実務ではデータベースの接続情報を 設定ファイル(application.propertiesなど) に記述し、Spring Boot の設定機構を利用して読み込む方法が一般的です。

余裕があれば調べてみましょう。

データベースへの接続と切断を担当するスーパークラスは以下SuperDao.javaになりました。

package com.example.demo.model.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * Daoクラス - 各daoの基底クラス データベースへの接続、切断、クエリ実行など各dao共通の機能を実装する
 * 
 * @author Say Consulting Group
 * @version 1.0.0
 */
public class SuperDao {

	/** DB接続オブジェクト */
	protected Connection con = null;

	/** DB接続情報 */
	private static final String DB_URI = "jdbc:mysql://localhost:3306/sip_a?characterEncoding=utf8&"
			+ "useSSL=false&serverTimezone=GMT%2B9&rewriteBatchedStatements=true";
	private static final String DB_USER = "newuser";
	private static final String DB_PASS = "0";

	/**
	 * connectメソッド データベースへの接続を開始する
	 */
	protected void connect() {
		try {
			// データベース接続用ドライバをロード
			// javaSE8から不要な処理となったが、Tomcatを利用する場合は必要なので、記述
			Class.forName("com.mysql.cj.jdbc.Driver");
			// ドライバを用いてデータベースへの接続を開く
			con = DriverManager.getConnection(DB_URI, DB_USER, DB_PASS);
			System.out.println("データベースへの接続に成功しました。");
		} catch (SQLException e) {
			String errMsg = "データベースへの接続時に問題が発生しました。";
			System.err.println(errMsg);
			System.err.println(e);
		} catch (ClassNotFoundException e) {
			String errMsg = "データベース接続用クラスが見つかりません。";
			System.err.println(errMsg);
			System.err.println(e);
		}
	}

	/**
	 * closeメソッド DBとの接続を切断する 各オブジェクトを再初期化する
	 */
	protected void close() {
		try {
			if (con != null) {
				con.close();
				con = null;
			}
			System.out.println("データベースからの切断に成功しました。");
		} catch (SQLException e) {
			String errMsg = "データベースからの切断時に問題が発生しました。";
			System.err.println(errMsg);
			System.err.println(e);
		}
	}
}
  • ①SuperDao.javaにはフィールドはいくつあって、それぞれの役割はなんですか?
あなたの答え:
  • ②メソッドはいくつあって、それぞれの役割はなんですか?
あなたの答え:
  • ③SuperDaoクラスのサブクラスで上記のフィールドやメソッドは定義しなくても使えますか?
あなたの答え:

接続情報(CONNECT_STRING)の文字列には下図のような意味がありますが、覚える必要はありません。

ただし、⑤のデータベース名だけは皆さんがMySQLで作成したスキーマ名になりますのでその点だけ忘れないように書き換えてください。

接続情報の意味

上記のコードに便宜的に以下のmain()メソッドを追加して実行してみて下さい。

	public static void main(String[] args) {
		SuperDao superDao = new SuperDao();
		superDao.connect();
		superDao.close();
	}

<実行結果>

データベースへの接続に成功しました。
データベースからの切断に成功しました。

このように接続と切断だけをするスーパークラスができました。

次にこのスーパークラスを継承したクラス(WithSuperClass.java)を作成します。データベースに格納されている車の数をコンソール出力するプログラムです。詳細はひとまず置いて行数に着目してください。

package model.dao;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class WithSuperClass extends SuperDao {

	private PreparedStatement ps;

	public int countCars() {

		int ret = 0;

		connect();

		String SQL = "SELECT count(*) FROM cars";
		try {
			ps = con.prepareStatement(SQL);

			ResultSet rs = ps.executeQuery();

			rs.next();

			ret = rs.getInt("count(*)");

		} catch (SQLException e) {
			System.err.println(e);
		} finally {
			close();
		}
		return ret;
	}

	public static void main(String[] args) {
		WithSuperClass wsc = new WithSuperClass();
		System.out.println(wsc.countCars());
	}
}

調べてみましょう

もしも、スーパークラスを継承しない場合は以下のWithoutSuperClass.javaのように全61行と長くなります。(スーパークラスまで含めると上記のコードも長いのですが、サブクラスが増えるにつれコード削減効果がでることは理解いただけるものと思います)

package model.dao;

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

public class WithoutSuperClass {

	private static final String CONNECT_STRING = "jdbc:mysql://localhost:3306/sip_a?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B9&rewriteBatchedStatements=true";
	private static final String USER_ID = "newuser";
	private static final String PASSWORD = "0";
	private static Connection con = null;
	private static PreparedStatement ps = null;

	public int countCars() {
		try {
			con = DriverManager.getConnection(CONNECT_STRING, USER_ID, PASSWORD);
			System.out.println("データベースへの接続に成功しました。");
		} catch (SQLException e) {
			System.err.println("データベースへの接続時に問題が発生しました。");
			System.err.println(e);
		}

		int ret = 0;

		String SQL = "SELECT count(*) FROM cars";
		try {
			ps = con.prepareStatement(SQL);

			ResultSet rs = ps.executeQuery();

			rs.next();

			ret = rs.getInt("count(*)");

		} catch (SQLException e) {
			System.err.println(e);
		} finally {
			try {
				if (ps != null) {
					ps.close();
				}
				if (con != null) {
					con.close();
				}
				System.out.println("データベースからの切断に成功しました。");
			} catch (SQLException e) {
				System.err.println("データベースからの切断時に問題が発生しました。");
				System.err.println(e);
			}
		}
		return ret;
	}

	public static void main(String[] args) {
		WithoutSuperClass wosc = new WithoutSuperClass();
		System.out.println(wosc.countCars());
	}
}

ここからはSuperDaoを継承したCarsクラスの様々なメソッドを通じてJavaからデータベースを扱う方法を学んでいきます。

まずは、SELECT文の実行結果であるResultSetの扱い方を学びます。

4. SELECT文

ここからいよいよ本格的にJavaからMySQLを扱うことにします。ただし、WebアプリケーションでMySQLを扱う前に、JavaSEでMySQLを扱ってみましょう。この方法は、いちいちアプリケーション・サーバーを立ち上げる必要がないためテストに要する時間を短縮できます。

さらに、常に以下の3ステップを踏めば、確実に動くプログラムを得ることができます。

  1. MySQLでSQL実行
  2. JavaSEでSQL実行
  3. JavaWebアプリケーションでSQL実行

みなさんも、決していきなりWebアプリケーションでSQLを実行しようとしてはいけません。

急がば回れです。

4.1 ResultSetの構造を理解する(まずは1レコードを取得する)

以下CarsDao.javaのcountCars()メソッドはcarsテーブルの車の数を取得するJavaSEプログラムです。これ以降のコードにはコメントを付けますので参考にして下さい。

以下の2点が前提です。

  • ①プログラムを実行する前にcarsテーブルの車の数を取得するSQL文をMySQLWorkBenchで実行し、結果を以下に書き入れなさい。
あなたの答え:
package com.example.demo.model.dao;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;


public class CarsDao extends SuperDao {

	// メソッドの開始:車の数をカウントするメソッド
	public int countCars() {

		// カウントした車の数を保持するための整数変数retを初期化
		int ret = 0;

		// データベースに接続するメソッドを呼び出し
		connect();

		// 車の総数を取得するためのSQLクエリを定義
		String sql = "SELECT count(*) FROM cars";

        // try-with-resources文を使用してPreparedStatementを作成
		try (PreparedStatement ps = con.prepareStatement(sql)) {

			// クエリを実行し、結果をResultSetに格納
			ResultSet rs = ps.executeQuery();

			// ResultSetの最初の行に移動
			rs.next();

			// ResultSetから車の総数を取得してretに代入
			ret = rs.getInt("count(*)");

		} catch (SQLException e) {
			// SQL例外が発生した場合の処理
			// エラーメッセージをコンソールに出力
			System.err.println(e);
		}

		close();

		// 車の総数を返す
		return ret;
	}
(以下のメソッドはこの後、順番に紹介します)
  • ①「con.prepareStatement(sql)」これはインスタンスメソッド、スタティックメソッド、どちらですか?
あなたの答え:

ちなみに、クラス名は過去分詞形のPrepared(形容詞としてStatementを修飾)、メソッド名は動詞のprepareです。

  • ②「rs.next()」の戻り値は何ですか? コンソール出力してみましょう。
あなたの答え:
  • ③「rs.getInt("count(*)")」では何をしていると推測されますか?
あなたの答え:

rs.next()の戻り値はtrueでした。

rs.next();

この文が必要な理由は、下図にあるようなResultSetの構造にあります。

【Result】= 「結果」、【Set】= 「セット」ですので、SQLを実行した結果のセットがResultSetなのです。ResultSetはカーソル(現在の行を示すポインタ)が最初に1行目の前にあるため、rs.next() を実行してカーソルを1行目に移動しないとデータを取得できないのですね。

カーソルの概念

また、 ResultSet の一行から特定の列(や集約関数の結果)を取り出すには以下のようにインスタンスメソッドgetXxxに列名(や集約関数名)を引数として渡します

rs.getInt("count(*)")

上記の例では count(*) がint型だったためgetInt()メソッドを使いましたが、型にあわせて getString()メソッド やgetDouble()メソッドを使い分けないといけません

初学者がミスをしやすいところなので気をつけましょう。

例題

CarsDao.javaをコピーしてCustomersDao.javaとし、customersテーブルの顧客数をコンソール出力するcountCustomers()メソッドを作成しなさい。

例題

CarsDao.javaのUSERIDやPASSWORD、SQLの予約語(SELECT)、列名のcount(*)やテーブル名(cars)のスペルをわざと間違えて、どのようなエラーメッセージが出るかを観察しましょう。

観察結果のメモ(キーワード):

SELECT:
count(*):
cars:

4.2 ResultSetから全件のレコードを取得する

全件のレコードを取得するJavaプログラムを作成してみます。以下は前提条件です。

  • modelパッケージに前章で作成した以下のCarDtoクラスをコピーしてある。
package com.example.demo.model.dto;

import java.io.Serializable; // オブジェクトのバイト列化(シリアライズ)を可能にするインターフェース
import java.time.LocalDateTime; // 日付と時刻を扱うためのクラス

public class CarDto implements Serializable {

	/** 車のID(データベースの主キーを想定) */
	private int carId;

	/** 車の名前またはモデル名 */
	private String name;

	/** 車の価格(整数型で金額を格納) */
	private int price;

	/**
	 * 論理削除された日時を文字列として格納。 DBのカラムが date/datetime であっても、 プログラム側で文字列として管理する場合がある。 null
	 * や空文字の場合は「未削除」を表す想定。
	 */
	private String deletedAt;

	/**
	 * デフォルトコンストラクタ(引数なし) JavaBeans ルールに従い、 new CarsDto() のように引数無しで生成可能にする。
	 */
	public CarDto() {
	}

	/**
	 * フィールドをまとめて初期化するコンストラクタ。
	 * 
	 * @param carId     車のID
	 * @param name      車の名前
	 * @param price     車の価格
	 * @param deletedAt 論理削除された日時(未削除の場合はnullや空文字)
	 */
	public CarDto(int carId, String name, int price, String deletedAt) {
		this.carId = carId;
		this.name = name;
		this.price = price;
		this.deletedAt = deletedAt;
	}

	/**
	 * 車のIDを取得するメソッド。
	 * 
	 * @return carId
	 */
	public int getCarId() {
		return carId;
	}

	/**
	 * 車のIDを設定するメソッド。
	 * 
	 * @param carId セットしたいID
	 */
	public void setCarId(int carId) {
		this.carId = carId;
	}

	/**
	 * 車の名前を取得するメソッド。
	 * 
	 * @return name
	 */
	public String getName() {
		return name;
	}

	/**
	 * 車の名前を設定するメソッド。
	 * 
	 * @param name セットしたい車の名称
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * 車の価格を取得するメソッド。
	 * 
	 * @return price
	 */
	public int getPrice() {
		return price;
	}

	/**
	 * 車の価格を設定するメソッド。
	 * 
	 * @param price セットしたい価格
	 */
	public void setPrice(int price) {
		this.price = price;
	}

	/**
	 * 論理削除された日時を取得するメソッド。
	 * 
	 * @return deletedAt 物理削除されず、論理削除された場合は日時文字列を格納
	 */
	public String getDeletedAt() {
		return deletedAt;
	}

	/**
	 * 論理削除された日時を設定するメソッド。
	 * 
	 * @param deletedAt 論理削除した日時
	 */
	public void setDeletedAt(String deletedAt) {
		this.deletedAt = deletedAt;
	}

	/**
	 * デバッグやログ出力などでオブジェクト内容を簡単に把握できるよう、 フィールドの値を文字列として整形して返す。
	 * 
	 * @return CarsDtoのフィールド情報を整形した文字列
	 */
	@Override
	public String toString() {
		return "CarsDto [carId=" + carId + ", name=" + name + ", price=" + price + ", deletedAt=" + deletedAt + "]";
	}
}

  • ①CarsBeanクラスにmain()メソッドを加えて、CarBeanのインスタンスを複数作成し、CarsBeanにaddCarしてからコンソール出力してみなさい。
あなたの答え:
  • ②carsテーブルの全ての車のレコードを取得するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
あなたの答え:

以下のCarsDao.javaのgetCarsBean()メソッドを読み込んで質問に答えてください。

	public ArrayList<CarDto> getCarList() {

	    // CarDtoのリストを作成
	    ArrayList<CarDto> carList = new ArrayList<>();

	    // データベースに接続するメソッドを呼び出し
	    connect();

	    // 'cars'テーブルから全てのデータを選択するSQLクエリを定義
	    String sql = "SELECT * FROM cars";

	    try (PreparedStatement ps = con.prepareStatement(sql)) {

	        // クエリを実行し、結果をResultSetに格納
	        ResultSet rs = ps.executeQuery();

	        // ResultSetを繰り返し処理し、各車のデータを取得
	        while (rs.next()) {
	            // CarDtoオブジェクトを生成
	            CarDto car = new CarDto();

	            // CarDtoにcar_id, name, price, deleted_atを設定
	            car.setCarId(rs.getInt("car_id"));
	            car.setName(rs.getString("name"));
	            car.setPrice(rs.getInt("price"));
	            car.setDeletedAt(rs.getString("deleted_at"));

	            // 取得した車のデータをリストに追加
	            carList.add(car);
	        }
	    } catch (SQLException e) {
	        // SQL例外が発生した場合の処理
	        System.err.println(e);
	    }
	    close();
	    
	    // 取得したリストを返す
	    return carList;
	}

先の1件のレコードを取得したときとソースコードは何が違いますか?

  • ①SQL文の観点から違いを挙げてください。
あなたの答え:
  • ②繰り返し処理の観点から違いを挙げてください。
あなたの答え:
  • ③戻り値の観点から違いを挙げてください。
あなたの答え:
  • ④適切なmain()メソッドを加えて実行してみて下さい。
あなたの答え:

今回のResultSet rsのインスタンスの中身(イメージ)は下図になります。そのため繰り返しが必要なのでした。

ResultSet rsのインスタンスのイメージ 

例題

CustomersDao.javaにcustomersテーブルの全ての顧客のすべてのデータをコンソール出力するselectAllCustomers()メソッドを作成しなさい。

4.3 特定のレコードを取得する

次にユーザーが選択した特定のcar_idを持つレコードを取得してみます。

  • ①carsテーブルから特定のcar_id(例えば1)を持つ車のレコードを取得するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
あなたの答え:

以下のgetCar()メソッドを実行してみましょう。

	// 特定のIDを持つ車の情報をデータベースから取得するメソッド
	public CarBean getCar(int id) {

		// CarBeanの新しいインスタンスを生成
		CarBean car = new CarBean();

		// データベースへの接続を行う
		connect();

		// 指定されたcar_idに基づいて車の情報を取得するSQLクエリ
		String sql = "select * from cars where car_id = ?";

		try (PreparedStatement ps = con.prepareStatement(sql)) {

			// クエリのパラメータ(car_id)を設定する
			ps.setInt(1, id);

			// クエリを実行し、結果をResultSetに格納する
			ResultSet rs = ps.executeQuery();

			// 結果セットにデータが存在する場合のみ処理を行う
			if (rs.next()) {

				// ResultSetから車の情報を取得し、CarBeanにセットする
				car.setCarId(rs.getInt("car_id"));
				car.setName(rs.getString("name"));
				car.setPrice(rs.getInt("price"));
				car.setDeletedAt(rs.getString("deleted_at"));
			}

		} catch (SQLException e) {
			// SQL例外が発生した場合、エラーをコンソールに出力する
			System.err.println(e);
		}
		close();

		// 取得した車の情報を含むCarBeanを返す
		return car;
	}
  • ①上記のgetCar()メソッドの引数と戻り値の型を答えなさい。
あなたの答え:
  • ②仮に同じcar_idの車が複数台データベースに格納されていた場合はどうなりますか?
あなたの答え:
  • ③適切なmain()メソッドを加えて実行してみて下さい。
あなたの答え:

上記のコードではSQL文の最後が「car_id = ?」となっています。この「?」をプレースホルダといいます。このプレースホルダにはps.setInt(1, id);のところでgetCar()メソッドの引数のidが入ります。

今回はプレースホルダが1つです。しかし、プレースホルダの「?」は複数あっても良いです。その場合の数え方は左から1,2…となります。0始まりでない点は注意して下さい。配列の添字やコレクションフレームワークのインデックスのようにJavaは0から数えるのが一般的です。しかし、プレースホルダの番号はそうでないのは、初学者が間違えやすいところです。

また、第2引数はセットしたい値です。気をつけないといけないのは、今回はint値であったため、setInt()メソッドでしたが、文字列であればsetString()メソッドになるという点です。

4.4 SQLインジェクションとは?

SQLインジェクションは、Webアプリケーションのデータベースを不正に操作する攻撃手法です。SQLインジェクションにより、個人情報やパスワード、クレジットカード情報などが盗まれることがあります。

ここからのお話は想像力をたくましくしてお聞きください。つまり、以下のサンプルコードSQLInjection.javaはWebアプリケーションではないですが、もしも、Webアプリケーションで同じことが行われたらどうなるかと想像しながら聞いてください。

package model.dao;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;

public class SQLInjection extends SuperDao {

	public static String SQL = "SELECT * FROM customers where customer_id = ";

	PreparedStatement ps = null;
	ResultSet rs = null;

	public void getACustomer() {

		try {
			connect();
			System.out.println("customer_idを整数値で指定してください");
			try (Scanner sc = new Scanner(System.in)) {
				String customer_id = sc.nextLine();
				SQL += customer_id;
			}

			ps = con.prepareStatement(SQL);

			rs = ps.executeQuery();
			while (rs.next()) {
				System.out.print(rs.getInt("customer_id") + ":");
				System.out.print(rs.getString("name") + ":");
				System.out.print(rs.getString("mail") + ":");
				System.out.print(rs.getString("mobile") + ":");
				System.out.println(rs.getString("pass"));
			}
		} catch (SQLException e) {
			System.err.println("データベースへの接続時に問題が発生しました。");
			System.err.println(e);
		} finally {
			close();
		}
	}

	public static void main(String[] args) {
		SQLInjection sqlInjection = new SQLInjection();
		sqlInjection.getACustomer();
	}
}

上記プログラムを実行して、

customer_idを指定してください

につづけて適当な数値を入力してエンターするとその番号の顧客の個人情報が標準出力に表示されます。(このプログラムは1件のレコードを取得する仕様に対して、複数件のレコードを取得する繰り返しがあり、不自然ですが、SQLインジェクションの解説につなげる意図ですので容赦ください)

  • ①「SQL += customer_id;」ここでは何をしていますか?
あなたの答え:

上記コードの実行画面で、例えば以下のように入力するとどうなりますか?

1 or 1 = 1

全てのレコードが取得できたはずです。実はこれはSQLインジェクションとして知られた攻撃(の簡略版)なのです。パスワードが全て盗まれてしまいましたね。


ここでもう一度SQL文をよく見てみます。

下図の赤枠で囲った「1 = 1」の部分が常にtrueと評価されるために全てのレコードという意味になるのですね。(SELECT * FROM customers where id = true と同じですので試してください )

このようにSQL文の組み立てを文字列連結によっているのは悪い書き方です。

したがって 「1 = 1」でなくても常にtrueと評価される式であれば何でもSQLインジェクション攻撃になります。

SQLインジェクションが成立する理由

おそらく最近もSQLインジェクションの被害事例があると思いますのでリンクをたどってみてください。

新しいプログラム言語にはこれから説明するようなSQLインジェクション対策が施されているにもかかわらず、未だに古い言語や古い書き方をして被害に合う組織が多いことは本当に残念です。

また、この研修では扱いませんが、Webアプリケーションフレームワークを使って開発したり、WAF【Web Application Firewall】の仕組みを導入することによりSQLインジェクション対策を施すことも有効です。

プレースホルダを使用してSQLインジェクション攻撃を避ける

SQLインジェクション対策はどうすればよいのでしょうか?

プレースホルダを使用するという方法があります。

プレースホルダとは以下のサンプルコードにある「?」です。この「?」は以下のコードによりキーボードから入力された値に置き換わるのです。

ps.setString(1, customer_id);

この setString()メソッドの第1引数はプレースホルダの番号です。この番号はSQL文の左から順に1から数え、0からではない点に注意するのでした。気をつけないといけないのは、前回はint値であったため、setInt()メソッドでしたが、文字列なのでsetString()メソッドになるということでした。

以下のPlaceholder.javaはcustomer_idで検索できる先ほど同様のプログラムです。ただし、プレースホルダでSQLインジェクション対策をしてありますので実行して「1 or 1 = 1」を入力して確かめてください。

package model.dao;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;

public class Placeholder extends SuperDao {

	public static final String SQL = "SELECT * FROM customers where customer_id = ?";

	PreparedStatement ps = null;
	ResultSet rs = null;

	public void getACustomer() {

		try {
			connect();
			System.out.println("customer_idを整数値で指定してください");

			String customer_id;
			try (Scanner sc = new Scanner(System.in)) {
				customer_id = sc.nextLine();
			}

			ps = con.prepareStatement(SQL);
			ps.setString(1, customer_id);

			rs = ps.executeQuery();
			while (rs.next()) {
				System.out.print(rs.getInt("customer_id") + ":");
				System.out.print(rs.getString("name") + ":");
				System.out.print(rs.getString("mail") + ":");
				System.out.print(rs.getString("mobile") + ":");
				System.out.println(rs.getString("pass"));
			}
		} catch (SQLException e) {
			System.err.println("データベースへの接続時に問題が発生しました。");
			System.err.println(e);
		} finally {
			close();
		}
	}

	public static void main(String[] args) {
		Placeholder placeholder = new Placeholder();
		placeholder.getACustomer();
	}
}

今度は、一人だけが抽出されましたね。

なお、PreparedStatementのプレースホルダ(?)は、リテラル値の置き換えにのみ使用されるため、値を動的に挿入するために使用されますが、SQL の構造自体(テーブル名やカラム名、SQLキーワードなど)を変更するためには適していません。

例えば、プレースホルダは「SELECT * FROM users WHERE user_id = ?」といった場合において「?」に具体的な値を安全に挿入するのには適しています。しかし、テーブル名やカラム名、SQLコマンドの部分をプレースホルダを通じて変更することはできません。つまり「? * FROM customers;」のような使い方はできません。もし、できるとすると「?」が「DELETE」に置き換えられ大変なことになります。また、「SELECT ? FROM customers;」のような使い方もできません。もし、できるとすると「?」が「password」などに置き換えられて、これまた大変なことになりますね。これらを試みるとエラーになることが一般的です。

例題4

SQLInjection.javaに対してもSQLインジェクション対策を施しなさい。

4.5 任意のフィールドで昇順または降順に並べ替える

以下のgetSortedCarsBean()メソッドでは、SQLインジェクション対策をしたうえで、任意のフィールドで昇順または降順に並べ替えています。

  • ①carsテーブルからpriceの降順に全ての車のレコードを取得するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
あなたの答え:

詳細はコメントを読んで講師の解説をお聞きください。また、main()メソッドからこのメソッドを実行してみてください。

	// 任意のフィールドで昇順または降順に並べ替えるメソッド
	public List<CarDto> getSortedCarList(String sortField, boolean isAscending) {
		ArrayList<CarDto> carList = new ArrayList<>();

		connect();

		// sortFieldが有効なフィールド名であるか確認
		if (!isValidSortField(sortField)) {
			throw new IllegalArgumentException("Invalid sort field: " + sortField);
		}

		String sortOrder = isAscending ? "ASC" : "DESC";
		String sql = "SELECT * FROM cars ORDER BY " + sortField + " " + sortOrder;

		try (PreparedStatement ps = con.prepareStatement(sql)) {

			// クエリを実行し、結果をResultSetに格納
			ResultSet rs = ps.executeQuery();

			// ResultSetを繰り返し処理し、各車のデータを取得
			while (rs.next()) {
				// 一時的に車の情報を保持するCarDtoオブジェクトのインスタンスを生成
				CarDto car = new CarDto();

				// CarDtoにcar_id, name, price, deleted_atを設定
				car.setCarId(rs.getInt("car_id"));
				car.setName(rs.getString("name"));
				car.setPrice(rs.getInt("price"));
				car.setDeletedAt(rs.getString("deleted_at"));

				// 取得した車のデータをリストに追加
				carList.add(car);
			}
		} catch (SQLException e) {
			// SQL例外が発生した場合の処理
			System.err.println(e);
		}
		close();

		// 取得したリストを返す
		return carList;
	}

  • ①「throw new IllegalArgumentException("Invalid sort field: " + sortField)」これは何を投げているのでしたか?
あなたの答え:
  • ②「isAscending ? "ASC" : "DESC"」 こういう文をなんと呼ぶのでしたか?
あなたの答え:
  • ③適切なmain()メソッドを加えて実行してみて下さい。
あなたの答え:

4.6 あいまい検索を使ってレコードを取得する

Google検索の例を見ても明らかなように、あいまい検索はアプリケーションでとても大きな力を発揮します。

  • ①carsテーブルからあいまい検索でnameに"車"を含む全てのレコードを取得するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
あなたの答え:

詳細はコメントを読んで講師の解説をお聞きください。また、main()メソッドからこのメソッドを実行してみてください。

	// キーワードに基づいて車を検索し、その結果をリストに格納して返すメソッド
	public List<CarDto> searchCarList(String keyword) {

	    // CarDtoのリストを生成
	    List<CarDto> carList = new ArrayList<>();

	    // データベースへの接続を行う
	    connect();

	    // 指定されたキーワードに基づいて車の情報を検索するSQLクエリ
	    String sql = "SELECT * FROM cars WHERE name LIKE ?";

	    try (PreparedStatement ps = con.prepareStatement(sql)) {

	        // クエリのパラメータ(nameの検索キーワード)をあいまい検索で設定する
	        ps.setString(1, "%" + keyword + "%");

	        // クエリを実行し、結果をResultSetに格納する
	        ResultSet rs = ps.executeQuery();

	        // 結果セットの各行を繰り返し処理し、車の情報を取得する
	        while (rs.next()) {
	            // 一時的に車の情報を保持するCarDtoの新しいインスタンスを生成
	            CarDto car = new CarDto();

	            // ResultSetから車の情報を取得し、CarDtoにセットする
	            car.setCarId(rs.getInt("car_id"));
	            car.setName(rs.getString("name"));
	            car.setPrice(rs.getInt("price"));
	            car.setDeletedAt(rs.getString("deleted_at"));

	            // 取得した車の情報をリストに追加する
	            carList.add(car);
	        }
	    } catch (SQLException e) {
	        // SQL例外が発生した場合、エラーをコンソールに出力する
	        System.err.println(e);
	    }
	    close();

	    // 検索結果を含むリストを返す
	    return carList;
	}
  • ①「"%" + keyword + "%"」なぜ、これであいまい検索になるのでしたか?
あなたの答え:

5. SELECT文以外の操作

5.1 INSERT文

次にレコードの挿入です。

  • ①carsテーブルにname=電気自動車 price=5000000というレコードを挿入するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
あなたの答え:

以下のaddCar()メソッドを読み込んで質問に答えてください。

// 新しい車のデータをデータベースに追加するメソッド
public int addCar(CarDto car) {

    // 返り値として使用する整数型変数を初期化
    int ret = 0;

    // データベースへの接続を行う
    connect();

    // 新しい車を追加するためのSQL文を定義
    String sql = "INSERT INTO cars(name, price) VALUES(?, ?);";

    try (PreparedStatement ps = con.prepareStatement(sql)) {

        // PreparedStatementに車の名前と価格をセット
        ps.setString(1, car.getName());
        ps.setInt(2, car.getPrice());

        // SQL文を実行し、処理された行数をretに代入
        ret = ps.executeUpdate();

    } catch (SQLException e) {
        // SQL処理中に例外が発生した場合のエラーハンドリング
        System.err.println(e);
    }

    close();

    // 処理された行数を返す
    return ret;
}
  • ①レコードのInsertにResultSetオブジェクトは必要ですか?
あなたの答え:
  • ②このSQL文を解説しなさい。
あなたの答え:
  • ③「ps.executeUpdate()」の戻り値を答えなさい。
あなたの答え:
  • ④上記のINSERT文ではcar_idを指定していません。なぜ、これで良いのですか?MySQLの知識で答えてください。
あなたの答え:

これまで学んだSELECT文の場合は、SQLを実行するメソッドがexecuteQuery()でしたね。しかし、今回のINSERT文では、executeUpdate()ですので気をつけましょう。【Query】=問い合わせ、【Update】=更新と意味から考えれば間違うことがなくなるでしょう。

  • ⑤このあと学ぶUPDATE文、DELETE文はexecuteQuery()とexecuteUpdate()どちらを使うと思いますか?
あなたの答え:

今回SQLに埋め込まれた“?”マークをプレースホルダといいました。このプレースホルダにps.setInt()メソッドやps.setString()メソッドを使って実際の値を入れることでSQL文を完成させるのでした。メソッドの引数はそれぞれ、(位置,値)でしたね。

プレースホルダの「?」の位置は左から順に1から数えます。(0からでない点に注意するのでした)

例題

上記の例を参考にcustomersテーブルにあなたのチームメンバー一人を追加するJavaプログラムを作成しなさい。(結果は先の例題3のプログラムで確認のこと)

※必ずMySQLで一度実行してからJavaで実行すること

5.2 UPDATE文

次にレコードの更新を見てみましょう。

  • ①carsテーブルのcar_idが3のレコードのdeleted_atを今年の4月1日0時0分0秒に更新するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
あなたの答え:

以下のupdateCar()メソッドは何をしていますか?

// データベース内の特定の車の情報を更新するためのメソッド
public int updateCar(CarDto car) {
    // 更新に成功したレコードの数を保持する変数を初期化
    int ret = 0;

    // データベースに接続
    connect();

    // 車の情報を更新するSQL文を定義
    String sql = "UPDATE cars SET name = ?, price = ?, deleted_at = ? WHERE car_id = ?";

    try (PreparedStatement ps = con.prepareStatement(sql)) {

        // SQL文のパラメータを設定
        ps.setString(1, car.getName()); // 車の名前
        ps.setInt(2, car.getPrice()); // 価格
        ps.setString(3, car.getDeletedAt()); // 削除日時
        ps.setInt(4, car.getCarId()); // 車のID

        // SQL文を実行し、影響を受けたレコードの数を取得
        ret = ps.executeUpdate();

    } catch (SQLException e) {
        // SQL例外が発生した場合、エラーを出力
        System.err.println(e);
    }

    close();

    // 更新に成功したレコードの数を返す
    return ret;
}
  • ①このプログラムの処理内容は?
あなたの答え:

SQLのUPDATE文を実行するメソッドがexecuteUpdate()であるということは覚えやすいと思います。しかし、INSERT文やDELETE文もexecuteUpdate()であるということは忘れやすいので気をつけましょう。

例題

上記の例を参考にcustomersテーブルのチームメンバーのレコードを更新するJavaプログラムを作成しなさい。(結果は先の例題3のプログラムで確認のこと)

5.3 DELETE文

MySQLの時にお話ししたようにDELETE文は、本研修でほとんど活躍の場がありませんのでサンプルコードも紹介しません。

CRUD【Create, Read, Update, Delete】処理を全て紹介し終わったところで、SQLを実行する2つのメソッドについて下表にまとめておきます。

SQLの実行メソッド対応SQL文戻り値考え方
executeQuery()SELECT文ResultSetオブジェクト(必要に応じてプレースホルダをsetXXX()メソッドで埋めてから)SQLを実行してResultSetを取得、
next()メソッドでカーソルを移動し、そのレコードから必要な列をgetXXX()メソッドで取得する。
executeUpdate()INSERT文
UPDATE文
DELETE文
主として処理したレコード数SQLのプレースホルダをsetXXX()メソッドで埋めてから実行する。ResultSetは使わない。
SQLの実行メソッドのまとめ

例題

SuperDaoクラスを継承したCustomersDao.javaクラスを作成し、顧客情報をすべて表示するselect()メソッドを作成しなさい。

テスト用のメインメソッドから動作を確認すること。

また、余裕があれば、他のメソッド(一人の顧客情報を表示するメソッド、データの挿入・更新メソッド)も実装しなさい。

6. Webアプリケーションとデータベースを連携させる

これまでの知識を応用してデータベースに入っているユーザー情報を使ってシステムにログインする処理を書いてみましょう。できるだけ過去に作成したプログラムを再利用することにします。

また、前提として下図のようなlogin_userテーブルがsip_aスキーマにあることとします。

login_user1
login_user2
login_userテーブルのデータ型とデータ

新規作成クラス

<LoginDao.java>

継承するクラス:SuperDao

属性:なし

操作

操作名可視性引数リスト返却値static説明
loginpublicString id, String passwordboolean-SQL を実行して、レコード件数が0件を超えていたらtrueを返す、超えていなければfalseを返す。
package model.dao;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class LoginDao extends SuperDao {

	// ログインメソッド。ユーザーIDとパスワードを受け取り、ログイン成功かどうかを返す
	public boolean login(String id, String pass) {
		boolean isSuccess = false;

		// ユーザーIDとパスワードに一致するレコードが存在するかをカウントするクエリ
		String query = "SELECT COUNT(*) FROM login_user WHERE login_id = ? AND password = ?";

		// データベースに接続する
		connect();

		// try-with-resourcesを使用してPreparedStatementとResultSetを自動的にクローズ
		try (PreparedStatement ps = con.prepareStatement(query)) {

			// クエリの最初のプレースホルダーにユーザーIDをセット
			ps.setString(1, id);
			// クエリの二番目のプレースホルダーにパスワードをセット
			ps.setString(2, pass);

			// クエリを実行して結果セットを取得
			ResultSet rs = ps.executeQuery();

			// 結果セットが存在し、最初のカラムの値が0より大きければログイン成功
			if (rs.next()) {
				isSuccess = rs.getInt("COUNT(*)") > 0;
			}

		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			// データベース接続を閉じる
			close();
		}

		// ログイン成功または失敗を返す
		return isSuccess;
	}
}
  • ①上記LoginDaoにmain()メソッドを追加して「id:imai password:p」でログインできること、それ以外ではログインできないことを確かめなさい。
確かめた結果:

<LoginController.java>

継承するクラス:なし

属性:なし

操作

操作名可視性引数リスト返却値static説明
showLoginPagepublicなしString-トップページ(/)にアクセスされたときに、ログイン画面(login.html)を表示する。
loginpublicString id, String pass, HttpServletRequest request, Model modelString-ログインフォームから送信されたidとpassを取得し、入力チェックを行う。入力が空の場合、エラー情報を設定し、login-error.htmlを表示する。入力がある場合はLoginDaoを用いてログイン認証を行う。認証が成功するとセッションにidとメッセージをセットし、メンバー専用ページ(/member-only2)へリダイレクトする。認証が失敗した場合、エラー情報を設定しlogin-error.htmlを表示する。
logoutpublicHttpServletRequest requestString-現在のセッションが存在すれば、セッションを無効化(ログアウト)してからログインページ(/login)へリダイレクトする。
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.demo.dao.LoginDao;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

@Controller
public class LoginController {

    @GetMapping("/")
    public String showLoginPage() {
        return "login"; // `login.html` を表示
    }

    @PostMapping("/login")
    public String login(
            @RequestParam("id") String id,
            @RequestParam("pass") String pass,
            HttpServletRequest request,
            Model model) {

        // DAOを手動でインスタンス化
        LoginDao loginDao = new LoginDao();

        // 入力チェック
        if (id.isEmpty() || pass.isEmpty()) {
            model.addAttribute("error", "ユーザーIDまたはパスワードを入力してください");
            return "login-error"; // `login-error.html` に遷移
        }

        // 認証処理
        if (loginDao.login(id, pass)) {
            HttpSession session = request.getSession();
            session.setAttribute("id", id); // セッションにユーザーIDを保存

            return "redirect:/member-only2"; // ログイン成功時、メンバー専用ページへリダイレクト
        } else {
            model.addAttribute("error", "ユーザーIDまたはパスワードが間違っています");
            return "login-error"; // `login-error.html` に遷移
        }
    }

    @GetMapping("/logout")
    public String logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate(); // セッションを無効化
        }
        return "redirect:/login"; // ログアウト後にログインページへ
    }
}

<MemberController.java>

継承するクラス:なし

属性:なし

操作

操作名可視性引数リスト返却値static説明
memberPagepublicHttpSession session, Model modelString-メンバー専用ページ(/member-only2)にアクセスされたときに、セッション内にユーザーID(id)が存在するか確認する。セッションにidがない場合はログインページ(/login)へリダイレクトし、存在する場合はメンバー専用ページ(member-only2.html)を表示する。
videoPagepublicHttpSession session, Model modelString-ビデオページ(/video)にアクセスされた際に、セッション内にユーザーID(id)が存在するか確認する。セッションにidがない場合はログインページ(/login)へリダイレクトし、存在する場合はvideo.htmlを表示する。
package com.example.demo.controller;

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

import jakarta.servlet.http.HttpSession;

@Controller
public class MemberController {

	@GetMapping("/member-only2")
	public String memberPage(HttpSession session, Model model) {
		// セッションがない場合、またはIDがない場合はログインページへリダイレクト
		if (session.getAttribute("id") == null) {
			return "redirect:/login";
		}

		return "member-only2"; // `member-only2.html` を表示
	}

	@GetMapping("/video")
	public String videoPage(HttpSession session, Model model) {
		// セッションがない場合、またはIDがない場合はログインページへリダイレクト
		if (session.getAttribute("id") == null) {
			return "redirect:/login";
		}
		
		return "video"; // `video.html` を表示
	}
}

必要なソースコードは以上です。

ただし、superDao、member-only2.htmlとlogin-error.html、video.htmlは再利用しているため掲載を割愛しています。

穴埋め問題

問題1(PreparedStatementの使用方法)

次のコードは、データベースにSQL文を発行して結果を取得するメソッドです。空欄を適切に埋めてください。

public int countCars() {
    connect();

    String sql = "SELECT ①(_______) FROM cars";

    int ret = 0;
    try (PreparedStatement ps = con.prepareStatement(②(_____))) {
        ResultSet rs = ps.executeQuery();

        rs.③(    )();
        ret = rs.④(        )("count(*)");

    } catch (SQLException e) {
        System.err.println(e);
    }
    close();
    return ret;
}

問題2(INSERT文のJava実装)

以下はcarsテーブルに新しいレコードを挿入するメソッドです。空欄を埋めてください。

public int insertCar(String name, int price) {
    int ret = 0;
    connect();

    String sql = "INSERT INTO cars(name, price) VALUES(①(   ), ②(  ))";

    try (PreparedStatement ps = con.prepareStatement(sql)) {
        ps.setString(③( ), name);
        ps.setInt(④( ), price);

        ret = ps.⑤(        )();
    } catch (SQLException e) {
        System.err.println(e);
    }
    close();
    return ret;
}

問題3(ResultSetを使った全レコード取得のループ処理)

以下のコードは、ResultSetから複数のレコードを取得する処理です。空欄に正しいコードを記述してください。

String sql = "SELECT * FROM cars";
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery();

while (rs.①(    )()) {
    CarBean car = new CarBean();
    car.setCarId(rs.getInt("car_id"));
    car.setName(rs.getString("②(     )"));
    car.setPrice(rs.getInt("price"));

    carsBean.③(        )(car);
}

問題4(プレースホルダの使い方)

次のSQL文におけるプレースホルダのセット方法を示すコードです。空欄を埋めなさい。

String sql = "UPDATE cars SET price = ? WHERE car_id = ?";
PreparedStatement ps = con.prepareStatement(sql);

ps.setInt(①( ), 2500000);  // price を 250万円に更新
ps.setInt(②( ), 10);       // car_id が10のレコードを更新

int updatedRows = ps.executeUpdate();

問題5(ResultSetのデータ取得方法)

次のResultSetオブジェクトrsから、文字列型の列nameと整数型の列priceを取得するコードの空欄を埋めなさい。

if (rs.next()) {
    String name = rs.①(        )("name");
    int price = rs.②(       )("price");
}

問題6(プレースホルダのインデックス番号)

次のPreparedStatementに対するプレースホルダの番号指定を埋めてください。

String sql = "INSERT INTO customers (name, email, age) VALUES (?, ?, ?)";
PreparedStatement ps = con.prepareStatement(sql);

ps.setString(①( ), "山田太郎");           // nameにセット
ps.setString(②( ), "yamada@example.com"); // emailにセット
ps.setInt(③( ), 30);                      // ageにセット

ps.executeUpdate();

問題7(SQLインジェクション対策)

次のコードは、SQLインジェクション攻撃を防ぐためにPreparedStatementを利用しています。空欄に適切なコードを記述してください。

String customerId = request.getParameter("customer_id");
String sql = "SELECT * FROM customers WHERE customer_id = ①( )";

try (PreparedStatement ps = con.prepareStatement(sql)) {
    ps.②(______)(1, customerId); // プレースホルダに値をセット

    ResultSet rs = ps.executeQuery();
    if (rs.next()) {
        System.out.println(rs.getString("name"));
    }
} catch (SQLException e) {
    System.err.println(e);
}


例題

講師から受け取ったサンプルプロジェクトのトップページに以下の変更を加えなさい。

1.データベースに登録されている車の台数を表示する

2.各車の購入ボタンを押すと、その車の情報だけが記述されたページに遷移する


<まとめ:隣の人に正しく説明できたらチェックを付けましょう>

□ JDBC がデータベースの違いを吸収するため、データベースの変更に強くなる

□ JavaプログラムからSELECT文を利用するためには、Connection、PreparedStatement、ResultSetの3つのオブジェクトが必要である

□ データベース処理を専門のクラスに任せるのがDAOパターンである

□ いきなりDAOクラスから書き始めない。MySQLでSQL文をテストしてからJavaプログラムに組み込む。

□ ResultSet の一行から特定の列(や集約関数の結果)を取り出すにはrs.getInt("count(*)")のように列名(や集約関数名)で取り出したい列を指定する。ただし、型にあわせて getString()メソッド やgetDoubleメソッドを使い分けないといけない

□ プレースホルダを使用することでSQLインジェクション攻撃を避けることができる

□ プレースホルダの?の位置は左から順に1から数える。0からでない点に注意する

□ SELECT文にはexecuteQuery()メソッドが、INSERT文・UPDATE文・DELETE文にはexecuteUpdate()メソッドがそれぞれ対応する

今回は、JDBCでデータベースと接続する方法を見てきました。これでJavaWebアプリケーションとデータベースを組み合わせることができました。

学んできたことの振り返り

Spring Bootを通じてWebアプリの世界に一歩を踏み出した皆さん、本当にお疲れさまでした。MVCやThymeleafを学んだことで、ユーザー視点を意識した開発が可能になり、セッション管理やDTOの活用でデータを整える術を手にしました。データベースとの連携を経験し、皆さんはエンジニアとしての土台を築きました。

ここまでの学習内容でこのあと受講者の皆さんに配布するサンプルWebアプリケーションを自力で読み取れるようになったことでしょう!

各チームでオリジナルなWebアプリケーション作成に取り掛かりましょう!