前回は「DTO(Data Transfer Object)」について学びました。DTOを使いデータの受け渡しを整理し、コードをシンプルにすることができました。DTOを使うことで、「ControllerとModel」や「ControllerとView」の間でデータを効率よく受け渡すことができます。データベースの大量のデータを扱う準備ができましたね。
今回は、Javaプログラムとデータベースの連携について学びます。Webアプリケーションでは、データを保存・取得する仕組みが不可欠です。データベースとの接続方法や、データを適切にやり取りする方法を理解し、実際にデータを操作できるようになりましょう。
今回の学習の重点をオレンジの枠線で図示します。

1. なぜ、Webアプリケーションにデータベースが必要なのか?
これまでに作成してきたWebアプリケーションのデータはアプリケーションサーバーのメモリ上にだけ存在しています。ということはアプリケーションサーバーを再起動したり、電源を切るとデータは消えてしまいます。
そこで、何らかの方法でデータを外部記憶装置に保存する必要が出てきます。(難しい表現ではデータの永続化 【perpetuation】ともいいます)その際にデータベースとファイルという候補がありますが、データベースのほうが優れているということは、この研修でもデータベースのところで学んだ通りです。
2. JDBCとは何か?
JDBC【Java DataBase Connectivity】とは、JavaプログラムからデータベースにアクセスするためのAPI【Application Programming Interface】です。もしもJDBCが無かったら、以下の図のようにデータベースの種類ごとにJavaのプログラムを変えなくてはなりません。

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

当社の研修の受講者の方々は既に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

Connection
- 役割: データベースへの接続を管理します。
- 使用法: データベースへの接続を確立するために使用され、この接続を通じてSQL文を実行します。
PreparedStatement:
- 役割: パラメータ化されたSQL文を表します。
- 使用法: SQLインジェクション攻撃(後述)を防ぐため、または同じSQL文を繰り返し実行するために使用されます。PreparedStatementを使用することで、SQL文にパラメータを動的に挿入できます。
ResultSet:
- 役割: SQLクエリから返されたデータを保持します。
- 使用法: クエリの実行結果として得られたデータセットを操作し、データを読み取るために使用されます。
3.1 DAOパターンでデータベース処理を専門のクラスに任せる
1つのクラスの中にデータベース処理を書いた場合、メソッドが肥大化して理解しにくく、メンテナンスしにくいプログラムになってしまいます。そこで、データベース処理を専門のクラスに任せることにします。ビジネスロジックとデータベース処理をそれぞれ別のクラスに担当させるのです。
この考え方をDAOパターン【Data Access Object Pattern】といいます。
パターンというのは以前、MVCパターンのところでもでてきたデザインパターンのことです。
その概念を図示すると以下の図になります。

3.2 データベースの接続・切断の処理をスーパークラスとして切り出す
もしも、個々のクラスでデータベースに接続する処理とデータベースから切断する処理を書くとメンテナンス性が低下してしまいます。つまり、同じ処理をアチラコチラに書くと修正が大変になります。例えば、MySQLのパスワードを変更した際にその全てのファイルの記述箇所を探して変更する必要が生じてしまいますね。(DRY【Don't Repeat Yourself】原則に反するといいます。)
そこで、本書では下図のようにデータベースの接続と切断を行うSuperDaoクラスを作成して、carsやcustomersなどの各テーブルとの操作はSuperDaoクラスのサブクラスとして作成することにします。
「CarsDaoは一種のSuperDaoクラス」と言えますか?
言えますね。
ですので、このクラス設計で問題ないでしょう。
また、テーブルごとにSELECT、INSERT、UPDATE、DELETEの処理がまとまっているのが直感的にわかりやすいですね。

データベースへの接続と切断を担当するスーパークラスは以下SuperDao.javaになりました。
package com.example.demo.model.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* SuperDao - データベース接続の取得を担当する基底クラス
*/
public class SuperDao {
// 接続情報は本来外部ファイル化すべきですが、
// 教育上の簡略化のため直接記述します
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";
/**
* データベース接続を取得する
* 呼び出し元で try-with-resources を使用して確実に close
* @return データベース接続オブジェクト
* @throws SQLException 接続失敗時にスロー
*/
protected Connection getConnection() throws SQLException {
return DriverManager.getConnection(DB_URI,
DB_USER, DB_PASS);
}
}- ①SuperDao.javaにはフィールドはいくつありますか?
| あなたの答え: |
- ②メソッドはいくつあって、それぞれの役割はなんですか?
| あなたの答え: |
- ③SuperDaoクラスのサブクラスで上記のフィールドやメソッドは定義しなくても使えますか?
| あなたの答え: |
接続情報(DB_URI)の文字列には下図のような意味がありますが、覚える必要はありません。
ただし、⑤のデータベース名だけは皆さんがMySQLで作成したスキーマ名になりますのでその点だけ忘れないように書き換えてください。

上記のコードに便宜的に以下のmain()メソッドを追加して実行してみて下さい。
public static void main(String[] args) {
SuperDao dao = new SuperDao();
System.out.println("テストを開始します...");
// try-with-resources 文を使用して接続テストを実施
try (Connection con = dao.getConnection()) {
if (con != null && !con.isClosed()) {
System.out.println("成功:正常に接続されました!");
System.out.println("接続先: " + DB_URI);
}
} catch (SQLException e) {
System.err.println("失敗:エラーが発生しました。");
System.err.println("原因: " + e.getMessage());
e.printStackTrace();
}
// tryブロックを抜けると自動的に con.close() が呼ばれる
System.out.println("接続は自動的に切断されました");
}<実行結果>
| データベース接続テストを開始します... 成功:データベースに正常に接続されました! 接続先: jdbc:mysql://localhost:3306/sip_a?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B9&rewriteBatchedStatements=true 接続は自動的に切断されました に切断されました) |
このように接続と切断だけをするスーパークラスができました。
次にこのスーパークラスを継承したクラス(WithSuperClass.java)を作成します。データベースに格納されている車の数をコンソール出力するプログラムです。詳細はひとまず置いて行数に着目してください。
package com.example.demo.model.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class WithSuperClass extends SuperDao {
public int countCars() {
int ret = 0;
String sql = "SELECT count(*) FROM cars";
try (Connection conn = getConnection();
PreparedStatement ps =
conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
ret = rs.getInt(1);
}
} catch (SQLException e) {
System.err.println("データ取得中にエラーが発生。");
e.printStackTrace();
}
return ret;
}
public static void main(String[] args) {
WithSuperClass wsc = new WithSuperClass();
System.out.println("登録台数: " + wsc.countCars());
}
}ここでは、rs.getInt(1)の"1"に着目して下さい。JDBCでは、「列の番号は1から数える」というルールがあります。
Javaの配列などは0から数えますが、SQLの結果(ResultSet)は「1番目の列、2番目の列…」と人間が数える直感に合わせる仕様になっています。そのため、rs.getInt(0)と書くとエラーになります。
今回のSQLはSELECT count(*)と1列目しか指定していないため、そのデータを取り出すために1を指定しています。
ここからはSuperDaoを継承したCarsクラスの様々なメソッドを通じてJavaからデータベースを扱う方法を学んでいきます。
まずは、SELECT文の実行結果であるResultSetの扱い方を学びます。
4. SELECT文
ここからいよいよ本格的にJavaからMySQLを扱うことにします。ただし、WebアプリケーションでMySQLを扱う前に、JavaSEでMySQLを扱ってみましょう。この方法は、いちいちアプリケーションサーバーを立ち上げる必要がないためテストに要する時間を短縮できます。
さらに、常に以下の3ステップを踏めば、確実に動くプログラムを得ることができます。
- MySQLでSQL実行
- JavaSEでSQL実行
- JavaWebアプリケーションでSQL実行
みなさんも、決していきなりWebアプリケーションでSQLを実行しようとしてはいけません。
急がば回れです。
4.1 ResultSetの構造を理解する(まずは1レコードを取得する)
以下CarsDao.javaのcountCars()メソッドはcarsテーブルの車の数を取得するJavaSEプログラムです。これ以降のコードにはコメントを付けますので参考にしてください。
データベース(MySQL)講座の中でユーザーやパスワードは既に設定してあり、テーブルも作成済みであることが前提です。
- ①プログラムを実行する前にcarsテーブルの車の数を取得するSQL文をMySQL WorkBenchで実行し、結果を以下に書き入れなさい。
| あなたの答え: |
package com.example.demo.model.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.example.demo.model.dto.CarDto;
/**
* CarsDaoクラス - carsテーブルに対するデータ操作を担当するクラス
*/
public class CarsDao extends SuperDao {
// メソッドの開始:車の数をカウントするメソッド
public int countCars() {
// カウントした車の数を保持するための整数変数retを初期化
int ret = 0;
// 車の総数を取得するためのSQLクエリを定義
String sql = "SELECT count(*) FROM cars";
// try-with-resources文を使用する
// これによりclose()の呼び出し忘れを防ぎ、確実にリソースを解放する
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// ResultSetの最初の行に移動し、値が存在すれば取得
if (rs.next()) {
// ResultSetから車の総数(列番号1)を取得してretに代入
ret = rs.getInt(1);
}
} catch (SQLException e) {
// SQL例外が発生した場合の処理
// エラー内容を標準エラー出力に表示
e.printStackTrace();
}
// 車の総数を返す
return ret;
}- ①「rs.next()」の戻り値は何ですか? コンソール出力してみましょう。
| あなたの答え: |
- ②「rs.getInt(1)」では何をしていると推測されますか?
| あなたの答え: |
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()メソッドを使い分けないといけません。
初学者がミスをしやすいところなので気をつけましょう。
また、タイプミスを発生させないために以下のように書くことが推奨されます。
rs.getInt(1)
4.2 ResultSetから全件のレコードを取得する
全件のレコードを取得するJavaプログラムを作成してみます。以下は前提条件です。
- modelパッケージに前章で作成した以下のCarDtoクラスをコピーしてあるので配布済みのサンプルコードを確認すること。
- ①carsテーブルの全ての車のレコードを取得するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
| あなたの答え: |
以下のCarsDao.javaのgetCarList()メソッドを読み込んで質問に答えてください。
// 車の全件リストを取得するメソッド
public List<CarDto> getCarList() {
// CarDtoのリストを初期化
List<CarDto> carList = new ArrayList<>();
// 'cars'テーブルから全てのデータを選択するSQLクエリを定義
String sql = "SELECT * FROM cars";
// ConnectionとPreparedStatementを取得し、自動クローズを保証
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// ResultSetを繰り返し処理し、各車のデータを取得
while (rs.next()) {
// CarDtoオブジェクトを生成し、データをセット
CarDto car = new 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例外が発生した場合のエラーハンドリング
e.printStackTrace();
}
// 取得したリストを返す
return carList;
}先の1件のレコードを取得したときとソースコードは何が違いますか?
- ①SQL文の観点から違いを挙げてください。
| あなたの答え: |
- ②繰り返し処理の観点から違いを挙げてください。
| あなたの答え: |
- ③戻り値の観点から違いを挙げてください。
| あなたの答え: |
- ④適切なmain()メソッドを加えて実行してみて下さい。
| あなたの答え: |
今回のResultSet rsのインスタンスの中身(イメージ)は下図になります。そのため繰り返しが必要なのでした。

4.3 特定のレコードを取得する
次にユーザーが選択した特定のcar_idを持つレコードを取得してみます。
- ①carsテーブルから特定のcar_id(例えば1)を持つ車のレコードを取得するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
| あなたの答え: |
以下のgetCar()メソッドを実行してみましょう。
// 特定のIDを持つ車の情報を取得するメソッド
public CarDto getCar(int id) {
// CarDtoのインスタンスを生成
// 見つからない場合は中身が空のDtoを返す想定
CarDto car = new CarDto();
// 指定されたcar_idに基づいて車の情報を取得するSQL
String sql = "SELECT * FROM cars WHERE car_id = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
// クエリのパラメータ(car_id)を設定
ps.setInt(1, id);
// クエリを実行し、結果をResultSetに格納
try (ResultSet rs = ps.executeQuery()) {
// データが存在する場合のみCarDtoにセット
if (rs.next()) {
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) {
// エラー情報を出力
e.printStackTrace();
}
// 車の情報を返す
return car;
}- ①上記のgetCar()メソッドの引数と戻り値の型を答えなさい。
| あなたの答え: |
- ②仮に同じcar_idの車が複数台データベースに格納されていた場合はどうなりますか?
| あなたの答え: |
- ③適切なmain()メソッドを加えて実行してみて下さい。
| あなたの答え: |
- ④先ほどのcountCars()メソッドでは戻り値が車の台数1つだけだったのでDTOの出番はありませんでした。あなたならいくつ以上のデータからDTOの活用を考えますか?
| あなたの答え: |
上記のコードではSQL文の最後が「car_id = ?」となっています。この「?」をプレースホルダといいます。このプレースホルダにはps.setInt(1, id);のところでgetCar()メソッドの引数のidが入ります。
今回はプレースホルダが1つです。しかし、プレースホルダの「?」は複数あっても良いです。その場合の数え方は左から1,2…となります。プレイスホルダは1始まりで0始まりでない点は間違えやすいので注意して下さい。
また、第2引数はセットしたい値です。気をつけないといけないのは、今回はint値であったため、setInt()メソッドでしたが、文字列であればsetString()メソッドになるという点です。
4.4 SQLインジェクションとは?
SQLインジェクションは、Webアプリケーションのデータベースを不正に操作する攻撃手法です。SQLインジェクションにより、個人情報やパスワード、クレジットカード情報などが盗まれることがあります。
ここからのお話は想像力をたくましくしてお聞きください。つまり、以下のサンプルコードSQLInjection.javaはWebアプリケーションではないですが、もしも、Webアプリケーションで同じことが行われたらどうなるかと想像しながら聞いてください。
package com.example.demo.model.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;
public class SQLInjection extends SuperDao {
public void getACustomer() {
String baseSql = "SELECT * FROM customers where customer_id = ";
System.out.println("customer_idを整数値で指定してください");
String inputId = "";
try (Scanner sc = new Scanner(System.in)) {
inputId = sc.nextLine();
}
// 脆弱性の再現:ユーザー入力をそのままSQL文字列に結合
String dangerSql = baseSql + inputId;
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(dangerSql);
ResultSet 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) {
e.printStackTrace();
}
}
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を注入【injection】することからこの名前があります。
ここでもう一度SQL文をよく見てみます。
下図の赤枠で囲った「1 = 1」の部分が常にtrueと評価されるために全てのレコードという意味になるのですね。(SELECT * FROM customers where id = true と同じですので試してください )
このようにSQL文の組み立てを文字列連結によっているのは悪い書き方です。
したがって 「1 = 1」でなくても常にtrueと評価される式であれば何でもSQLインジェクション攻撃になります。

おそらく最近もSQLインジェクションの被害事例があると思いますのでリンクをたどってみてください。
新しいプログラム言語にはこれから説明するようなSQLインジェクション対策が用意されているにもかかわらず、未だに古い書き方をして被害に合う組織が多いことは本当に残念です。
また、この研修では扱いませんが、Webアプリケーションフレームワークを使って開発したり、WAF【Web Application Firewall】の仕組みを導入することによりSQLインジェクション対策を施すことも有効です。
プレースホルダを使用してSQLインジェクション攻撃を避ける
SQLインジェクション対策はどうすればよいのでしょうか?
プレースホルダを使用するという方法があります。
プレースホルダとは以下のサンプルコードにある「?」です。この「?」は以下のコードによりキーボードから入力された値に置き換わるのです。
ps.setString(1, customer_id);
この setString()メソッドの第1引数はプレースホルダの番号です。この番号はSQL文の左から順に1から数え、0からではない点に注意するのでした。
以下のPlaceholder.javaはcustomer_idで検索できる先ほど同様のプログラムです。ただし、プレースホルダでSQLインジェクション対策をしてありますので実行して「1 or 1 = 1」を入力して確かめてください。
package com.example.demo.model.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;
public class Placeholder extends SuperDao {
private static final String SQL =
"SELECT * FROM customers where customer_id = ?";
public void getACustomer() {
System.out.println("customer_idを整数値で指定してください");
String customerId = "";
try (Scanner sc = new Scanner(System.in)) {
customerId = sc.nextLine();
}
// try-with-resourcesを使用してリソースを自動解放
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(SQL)) {
// プレースホルダに値をセット
// 型安全かつエスケープ処理が行われる
ps.setString(1, customerId);
try (ResultSet 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) {
e.printStackTrace();
}
}
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.5 任意のフィールドで昇順または降順に並べ替える
以下のgetSortedCarList()メソッドでは、SQLインジェクション対策をしたうえで、任意のフィールドで昇順または降順に並べ替えています。
- ①carsテーブルからpriceの降順に全ての車のレコードを取得するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
| あなたの答え: |
詳細はコメントを読んで講師の解説をお聞きください。また、main()メソッドからこのメソッドを実行してみてください。
// 任意のフィールドで並べ替えたリストを取得するメソッド
public List<CarDto> getSortedCarList(String sortField, boolean isAscending) {
List<CarDto> carList = new ArrayList<>();
// sortFieldが有効なフィールド名であるか確認(SQLインジェクション対策)
if (!isValidSortField(sortField)) {
throw new IllegalArgumentException("無効なソート項目です: " + sortField);
}
// 並び順を決定
String sortOrder = isAscending ? "ASC" : "DESC";
// ORDER BY句はプレースホルダ(?)が使えないため文字列結合を利用
String sql = "SELECT * FROM cars ORDER BY " + sortField + " " + sortOrder;
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
CarDto car = new 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) {
e.printStackTrace();
}
return carList;
}- ①「throw new IllegalArgumentException(省略);」これは何を投げているのでしたか?
| あなたの答え: |
- ②「isAscending ? "ASC" : "DESC"」 こういう文をなんと呼ぶのでしたか?
| あなたの答え: |
- ③適切なmain()メソッドを加えて実行してみて下さい。
| あなたの答え: |
4.6 あいまい検索を使ってレコードを取得する
Google検索の例を見ても明らかなように、あいまい検索はアプリケーションでとても大きな力を発揮します。
- ①carsテーブルからあいまい検索でnameに"車"を含む全てのレコードを取得するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
| あなたの答え: |
詳細はコメントを読んで講師の解説をお聞きください。また、main()メソッドからこのメソッドを実行してみてください。
// キーワードに基づいて車を検索するメソッド
public List<CarDto> searchCarList(String keyword) {
List<CarDto> carList = new ArrayList<>();
// 名前(name)に対する部分一致検索のSQL
String sql = "SELECT * FROM cars WHERE name LIKE ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
// あいまい検索用のキーワードを設定
ps.setString(1, "%" + keyword + "%");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
CarDto car = new 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) {
e.printStackTrace();
}
return carList;
}
// 指定されたフィールド名が許可されているかチェックするメソッド
private boolean isValidSortField(String field) {
List<String> validFields =
Arrays.asList("car_id", "name", "price", "deleted_at");
return validFields.contains(field);
}- ①「"%" + keyword + "%"」なぜ、これであいまい検索になるのでしたか?
| あなたの答え: |
5. SELECT文以外の操作
5.1 INSERT文
次にレコードの挿入です。
- ①carsテーブルにあなたお好きな車をお好きな価格で挿入するSQL文を以下に記述してから、MySQLWorkBenchで実行しなさい。
| あなたの答え: |
以下のaddCar()メソッドを読み込んで質問に答えてください。
// 新しい車のデータを追加するメソッド
public int addCar(CarDto car) {
// 影響を受けた行数を保持する変数
int ret = 0;
// nameとpriceを挿入するSQL
String sql = "INSERT INTO cars(name, price) VALUES(?, ?)";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
// 値をセット
ps.setString(1, car.getName());
ps.setInt(2, car.getPrice());
// 更新系SQLを実行し、件数を取得
ret = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
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からでない点に注意するのでした)
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;
// 全項目をID指定で更新するSQL
String sql =
"UPDATE cars SET name = ?, price = ?, deleted_at = ? WHERE car_id = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
// パラメータの設定
ps.setString(1, car.getName());
ps.setInt(2, car.getPrice());
ps.setString(3, car.getDeletedAt());
ps.setInt(4, car.getCarId());
// SQLを実行
ret = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
return ret;
}- ①このプログラムのポイント(今までのコードとの違い)は?
| あなたの答え: |
SQLのUPDATE文を実行するメソッドがexecuteUpdate()であるということは覚えやすいと思います。しかし、INSERT文やDELETE文もexecuteUpdate()であるということは忘れやすいので気をつけましょう。
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は使わない。 |
6. Webアプリケーションとデータベースを連携させる
これまでの知識を応用してデータベースに入っているユーザー情報を使ってシステムにログインする処理を書いてみましょう。できるだけ過去に作成したプログラムを再利用することにします。
また、前提として下図のようなlogin_userテーブルがsip_aスキーマにあることとします。


新規作成クラス
<LoginDao.java>
継承するクラス:SuperDao
属性:なし
操作
| 操作名 | 可視性 | 引数リスト | 返却値 | static | 説明 |
| login | public | String id, String password | boolean | - | SQL を実行して、レコード件数が0件を超えていたらtrueを返す、超えていなければfalseを返す。 |
package com.example.demo.dao;
import java.sql.Connection;
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 = ?";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(query)) {
// クエリのプレースホルダーにユーザーIDとパスワードをセット
ps.setString(1, id);
ps.setString(2, pass);
// クエリを実行して結果セットを取得
try (ResultSet rs = ps.executeQuery()) {
// 結果セットが存在し、最初のカラムの値が0より大きければログイン成功
if (rs.next()) {
// 列名指定よりも列番号(1)での取得がタイポに強く推奨されます
isSuccess = rs.getInt(1) > 0;
}
}
} catch (SQLException e) {
// 例外発生時のスタックトレースを出力
e.printStackTrace();
}
// ログイン成功または失敗の結果を返す
return isSuccess;
}
public static void main(String[] args) {
LoginDao loginDao = new LoginDao();
// テスト実行
boolean success = loginDao.login("imai", "p");
System.out.println("ログイン成功: " + success);
}
}- ①上記LoginDaoにmain()メソッドを追加して「id:imai password:p」でログインできること、それ以外ではログインできないことを確かめなさい。
| 確かめた結果: |
<LoginController.java>
継承するクラス:なし
属性:なし
操作
| 操作名 | 可視性 | 引数リスト | 返却値 | static | 説明 |
|---|---|---|---|---|---|
| showLoginPage | public | なし | String | - | トップページ(/)にアクセスされたときに、ログイン画面(login.html)を表示する。 |
| login | public | String id, String pass, HttpSession session, Model model | String | - | ログインフォームから送信されたidとpassを取得し、入力チェックを行う。入力が空の場合、エラーメッセージをmodelに設定し、login-error.htmlを表示する。入力がある場合はLoginDaoを用いて認証を行う。認証成功時はセッションにidを保存し、/member-only2にリダイレクトする。認証失敗時はエラーメッセージをmodelに設定し、login-error.htmlを表示する。 |
| logout | public | HttpSession session | String | - | 現在のセッションが存在すればそれを無効化(ログアウト)し、再ログインページ(/re-login)へリダイレクトする。 |
| reLoginPage | public | なし | String | - | /re-loginにアクセスされたとき、ログイン画面(login.html)を再度表示する。 |
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 com.example.demo.dao.LoginDao;
import jakarta.servlet.http.HttpSession;
@Controller
public class LoginController {
@GetMapping("/")
public String showLoginPage() {
return "login"; // `login.html` を表示
}
@PostMapping("/login")
public String login(
String id,
String pass,
HttpSession session,
Model model) {
LoginDao loginDao = new LoginDao();
// 入力チェック
if (id.isEmpty() || pass.isEmpty()) {
model.addAttribute("error", "ユーザーIDまたはパスワードを入力ください");
return "login-error";
}
// 認証処理
if (loginDao.login(id, pass)) {
session.setAttribute("id", id); // セッションにユーザーIDを保存
return "redirect:/member-only2";
} else {
model.addAttribute("error", "ユーザーIDまたはパスワードが間違っています");
return "login-error";
}
}
@GetMapping("/custom-logout")
public String logout(HttpSession session) {
if (session != null) {
session.invalidate();
}
return "redirect:/re-login";
}
@GetMapping("/re-login")// 再度ログインする場合
public String reLoginPage() {
return "login";
}
}<MemberController.java>
継承するクラス:なし
属性:なし
操作
| 操作名 | 可視性 | 引数リスト | 返却値 | static | 説明 |
|---|---|---|---|---|---|
| memberPage | public | HttpSession session, Model model | String | - | /member-only2 にアクセスされたとき、セッションに id が存在する場合は member-only2.html を表示する。セッションが存在しない、または id が未設定の場合はログインページ(/login)にリダイレクトする。 |
| videoPage | public | HttpSession session, Model model | String | - | /video にアクセスされたとき、セッションに id が存在する場合は video.html を表示する。セッションが存在しない、または id が未設定の場合はログインページ(/login)にリダイレクトする。 |
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は再利用しているため掲載を割愛しています。
今回は、JDBCでデータベースと接続する方法を見てきました。これでJavaWebアプリケーションとデータベースを組み合わせることができました。

Spring Bootを通じてWebアプリの世界に一歩を踏み出した皆さん、本当にお疲れさまでした。MVCやThymeleafを学んだことで、ユーザー視点を意識した開発が可能になり、セッション管理やDTOの活用でデータを整える術を手にしました。データベースとの連携を経験し、皆さんはエンジニアとしての土台を築きました。
ここまでの学習内容でこのあと受講者の皆さんに配布するサンプルWebアプリケーションを自力で読み取れるようになったことでしょう!
各チームでオリジナルなWebアプリケーション作成に取り掛かりましょう!
