Skip to content

第24章 Windowsフォーム社員管理アプリ:検索

この章では、第 23 章で作った社員一覧画面に 検索機能 を追加します。

具体的には、次の 2 つの絞り込みを組み合わせられるようにします。

  • 名前(姓 または 名)の部分一致:テキストボックスに入力した文字列で検索
  • 部署:プルダウン(ComboBox)で選択した部署で絞り込み

ここで重要になるのが パラメータ化クエリ(SqlParameter)です。 ユーザーが入力した文字列をそのまま SQL に連結すると、SQL インジェクション という重大なセキュリティ問題を引き起こします。 パラメータ自体は第 17 章「17-7」で一度学びましたが、本章ではそれを 検索(部分一致・複数条件)に応用し、@param の形でユーザー入力を安全に渡す書き方を確実にします。


この章でできるようになること

Section titled “この章でできるようになること”

この章を終えると、次のことができるようになります。

  • SQL インジェクションがなぜ危険かを具体例で説明できる
  • SqlParameter を使ったパラメータ化クエリを書ける
  • LIKE 演算子による部分一致検索を実装できる
  • 複数の検索条件を組み合わせる SQL を書ける(WHERE 句の組み立て方)
  • ComboBoxDataSourceList<T> を設定して選択肢を表示できる
  • Repository クラスにメソッドを追加して機能を拡張できる(第 23 章の EmployeeRepository の拡張)
  • 検索結果を DataGridView に再表示できる

項目内容
開発環境Visual Studio 2022
プロジェクト種類Windows フォーム アプリ
対象フレームワーク.NET 8
ソリューション名KadaiWinFormsApp(第 23 章で作成済み・続けて使う)
プロジェクト名EmployeeApp(第 23 章の続き・作り直さない)
ベースとなる章第 23 章
データベースSQLServer 2022(TrainingDB)
認証方式Windows 統合認証
NuGet パッケージMicrosoft.Data.SqlClient

この章は第 23 章の EmployeeApp を続けて使います

新しいプロジェクトは作りません。第 23 章で作った KadaiWinFormsApp ソリューションの EmployeeApp を開き、そこに検索機能を足していきます(Nullable・NuGet は第 23 章で設定済みのはずです)。

Server=localhost で接続できないとき

第 23 章と同じく、SQLServer のインスタンス名によっては Server=localhost\SQLEXPRESS 等の指定が必要です。詳しくは第 23 章「23-4 接続文字列を設定する」の補足を参照してください。


  • 第 23 章を Git に提出済み、または第 23 章のコードを手元に持っている
  • TrainingDBemployees / departments が動作している
  • EmployeeRepository.GetAll() が動くことを確認した

第 23 章のアプリにベースの画面はあります。そこに次の UI を追加します。

┌──────────────────────────────────────────────────────────────┐
│ 社員一覧 [再読込]│
├──────────────────────────────────────────────────────────────┤
│ 名前:[ 山田 ] 部署:[ 総務 ▼ ] [検索] [クリア]│ ← 本章で追加
├──────────────────────────────────────────────────────────────┤
│ EmployeeId │ LastName │ FirstName │ Email │ Salary │
├────────────┼──────────┼───────────┼─────────────────┼────────┤
│ 1001 │ 山田 │ 二郎 │ yamada.jiro@... │ 500000 │
│ 1006 │ 佐々木 │ 明子 │ sasaki.akiko@.. │ 800000 │
│ ... │
├──────────────────────────────────────────────────────────────┤
│ [新規登録] [削除] │
└──────────────────────────────────────────────────────────────┘

検索の仕様:

条件動き
名前のみ入力姓 または 名 に部分一致する社員を表示
部署のみ選択その部署の社員を表示
名前+部署 両方AND 条件で絞り込み
両方とも空 / 「すべて」全社員を表示(GetAll と同じ)

24-2 SQL インジェクションの脅威

Section titled “24-2 SQL インジェクションの脅威”

検索機能を書く前に、やってはいけない書き方 を見ておきます。

ユーザーが入力した文字列を、そのまま SQL に連結する書き方です。

// ❌ やってはいけない書き方
string keyword = textBoxKeyword.Text;
string sql = "SELECT * FROM employees WHERE last_name LIKE '%" + keyword + "%'";

このコードは一見動きます。しかし、ユーザーが次のような文字列を入力したらどうなるでしょうか。

';DROP TABLE employees;--

SQL は次のように組み上がります。

SELECT * FROM employees WHERE last_name LIKE '%';DROP TABLE employees;--%'

セミコロンで SQL 文が区切られ、2 文目の DROP TABLE employees が実行されてしまい、テーブルそのものが消える可能性があります。 これは SQL インジェクション(SQL injection、SQL の注入)と呼ばれる、Web・業務システムで最も古典的かつ深刻な脆弱性の 1 つです。

入力チェックでは不十分

; を弾けばいいのでは?」「シングルクォートをエスケープすればいい?」と考えがちですが、攻撃手法はいくつもあり、漏れがあると即破られます。 アプリの責務は「文字列をパラメータとして渡すこと」、SQL の解釈は DB に任せる、という分業が正解です。

これを実現するのが パラメータ化クエリ です。


24-3 パラメータ化クエリの基本

Section titled “24-3 パラメータ化クエリの基本”

第 17 章の復習

パラメータ(@名前 のプレースホルダーを SQL に書き、値は別途渡す書き方)は、第 17 章「17-7」で一度学びました。第 17 章では「指定した部署の社員数」のように 単一の値(ExecuteScalar)を求めるのに使いました。 本章では同じしくみを、複数行を返す検索(LIKE の部分一致 + 複数条件)に応用します。書き方の骨格は第 17 章と同じです。

パラメータ化クエリでは、SQL 文に プレースホルダー(@param のような名前) を書き、値は別途 SqlParameter として渡します

string sql = "SELECT * FROM employees WHERE last_name LIKE @keyword";
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@keyword", "%" + keyword + "%");

@keyword の値は SQL の構文として解釈されません。 たとえ keyword';DROP TABLE ... が入っていても、それは 検索したい文字列そのもの として DB に渡されるだけで、SQL 文として実行されることはありません。

利点説明
SQL インジェクションを防げる値が SQL 構文として解釈されない
エスケープの心配が不要シングルクォートを含む名前(O'Brien など)もそのまま渡せる
型が明示される文字列と数値、日付の取り違えがない
SQL がキャッシュされやすいDB 側でクエリプランを再利用しやすい

AddWithValue のシンプルさと注意点

Section titled “AddWithValue のシンプルさと注意点”

AddWithValue はパラメータ名と値を渡すだけで使える簡単なメソッドで、研修ではこれで十分です。 ただし、AddWithValue値から型を推測する ため、まれに型変換のずれが起きることがあります。 本格的な業務コードでは、型を明示する Parameters.Add を使うことも多くあります(発展)。

// 型を明示する書き方(発展)
command.Parameters.Add("@keyword", SqlDbType.NVarChar, 50).Value = "%" + keyword + "%";

24-4 EmployeeRepository に検索メソッドを追加する

Section titled “24-4 EmployeeRepository に検索メソッドを追加する”

第 23 章の EmployeeRepositorySearch メソッドを追加します。 ループ部分が GetAll と重複するので、SqlDataReader から Employee を組み立てる処理を private メソッド に切り出します。

EmployeeRepository.cs
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
using System.Collections.Generic;
public class EmployeeRepository
{
private readonly string _connectionString;
public EmployeeRepository(string connectionString)
{
_connectionString = connectionString;
}
public List<Employee> GetAll()
{
const string sql = @"
SELECT
e.employee_id, e.last_name, e.first_name, e.email,
e.hire_date, e.salary, e.department_id, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
ORDER BY e.employee_id";
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(sql, connection);
return ReadEmployees(command);
}
public List<Employee> Search(string keyword, int departmentId)
{
const string sql = @"
SELECT
e.employee_id, e.last_name, e.first_name, e.email,
e.hire_date, e.salary, e.department_id, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
WHERE
(@keyword = ''
OR e.last_name LIKE '%' + @keyword + '%'
OR e.first_name LIKE '%' + @keyword + '%')
AND (@departmentId = -1 OR e.department_id = @departmentId)
ORDER BY e.employee_id";
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@keyword", keyword ?? string.Empty);
command.Parameters.AddWithValue("@departmentId", departmentId);
return ReadEmployees(command);
}
public int Insert(Employee employee)
{
// 第25章で実装します
throw new NotImplementedException("Insert は第25章で実装します。");
}
public int Delete(int employeeId)
{
// 第25章で実装します
throw new NotImplementedException("Delete は第25章で実装します。");
}
private static List<Employee> ReadEmployees(SqlCommand command)
{
List<Employee> list = new List<Employee>();
using SqlDataReader reader = command.ExecuteReader();
int idxId = reader.GetOrdinal("employee_id");
int idxLast = reader.GetOrdinal("last_name");
int idxFirst = reader.GetOrdinal("first_name");
int idxEmail = reader.GetOrdinal("email");
int idxHire = reader.GetOrdinal("hire_date");
int idxSalary = reader.GetOrdinal("salary");
int idxDeptId = reader.GetOrdinal("department_id");
int idxDeptName = reader.GetOrdinal("department_name");
while (reader.Read())
{
Employee employee = new Employee
{
EmployeeId = reader.GetInt32(idxId),
LastName = reader.GetString(idxLast),
FirstName = reader.GetString(idxFirst),
Email = reader.IsDBNull(idxEmail) ? string.Empty : reader.GetString(idxEmail),
HireDate = reader.GetDateTime(idxHire),
Salary = reader.IsDBNull(idxSalary) ? 0m : reader.GetDecimal(idxSalary),
DepartmentId = reader.IsDBNull(idxDeptId) ? 0 : reader.GetInt32(idxDeptId),
DepartmentName = reader.IsDBNull(idxDeptName) ? string.Empty : reader.GetString(idxDeptName)
};
list.Add(employee);
}
return list;
}
}
ポイント説明
@keyword / @departmentId でパラメータ化SQL インジェクション対策
LIKE '%' + @keyword + '%'SQL 内で % をパラメータの両側に付けて部分一致
@keyword = '' OR ...キーワードが空文字列のときは全件マッチ
@departmentId = -1 OR e.department_id = @departmentId-1 を「指定なし」のマーカーとして使う
keyword ?? string.Emptynull が渡されたら "" として扱う

ReadEmployees をなぜ private static にしたか

Section titled “ReadEmployees をなぜ private static にしたか”
  • private:Repository の外からは使わせない(内部実装)
  • static:インスタンスの状態(_connectionString)に依存しないため、static で書ける

GetOrdinal で列番号を 1 度だけ取得 してからループに入ると、列が多いときに少し効率がよくなります。


24-5 Department クラスと DepartmentRepository

Section titled “24-5 Department クラスと DepartmentRepository”

部署プルダウン用に、部署のリストを取得する Repository も用意します。

Department.cs を新規追加します。

Department.cs
namespace EmployeeApp;
public class Department
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
}

DepartmentRepository.cs を新規追加します。

DepartmentRepository.cs
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
using System.Collections.Generic;
public class DepartmentRepository
{
private readonly string _connectionString;
public DepartmentRepository(string connectionString)
{
_connectionString = connectionString;
}
public List<Department> GetAll()
{
List<Department> list = new List<Department>();
const string sql = "SELECT department_id, department_name FROM departments ORDER BY department_id";
using SqlConnection connection = new SqlConnection(_connectionString);
connection.Open();
using SqlCommand command = new SqlCommand(sql, connection);
using SqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
list.Add(new Department
{
DepartmentId = reader.GetInt32(reader.GetOrdinal("department_id")),
DepartmentName = reader.GetString(reader.GetOrdinal("department_name"))
});
}
return list;
}
}

第 23 章の画面に、次のコントロールを追加します。DataGridView の上に 検索パネル をはさむイメージです。

第 23 章で DataGridViewLocation = 12, 50 に置きましたが、検索パネルを上部に挟むため、まず DataGridViewLocation12, 90Size860, 360 に変更します。Anchor は引き続き Top, Bottom, Left, Right のままにしておきます。

検索パネルのコントロール配置

Section titled “検索パネルのコントロール配置”
コントロール(Name)プロパティ
Label(任意)Text = 名前:Location = 12, 56
TextBoxtextBoxKeywordLocation = 60, 53Size = 150, 23
Label(任意)Text = 部署:Location = 230, 56
ComboBoxcomboBoxDepartmentLocation = 280, 53Size = 150, 23DropDownStyle = DropDownList
ButtonbuttonSearchText = 検索Location = 450, 51Size = 90, 28
ButtonbuttonClearText = クリアLocation = 550, 51Size = 90, 28

DropDownStyleDropDownList にすると、ユーザーは選択肢以外を入力できなくなります(誤入力防止)。

ボタンのイベント紐付け(buttonSearch / buttonClear のダブルクリック)は、次の 24-7 で機能を書くのと一緒に 行います。ここではコントロールを配置するだけで大丈夫です。


24-7 フォームに検索機能を組み込む

Section titled “24-7 フォームに検索機能を組み込む”

第 23 章で作った一覧表示の Form1.cs を土台に、検索の部品を 1 つずつ足していきます。第 23 章と同じく、ヘルパー(自分で呼ぶ部品)を先に用意し、そのあとボタンの イベントを 1 つずつ紐付けて中身を書きます

この節の進め方

ソースを丸ごと貼り付けるのではなく、ステップごとに自分の手で組み立てます。各ステップのあとでビルド(Ctrl+Shift+B)を試すと安心です。完成形(答え合わせ用)は 24-9 にあります。先に見ず、まず自分で積み上げてみてください。

ステップ1 フィールドを増やす(社員 + 部署の 2 つの Repository)

Section titled “ステップ1 フィールドを増やす(社員 + 部署の 2 つの Repository)”

第 23 章では Repository は 1 つ(_repository)でしたが、本章では部署プルダウン用に DepartmentRepository も使います。区別のため社員用を _employeeRepository に改名し、_departmentRepository を追加します。あわせて「すべての部署」を表す定数 AllDepartments も用意します。

Form1.cs
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
public partial class Form1 : Form
{
private const string ConnectionString =
"Server=localhost;Database=TrainingDB;Integrated Security=true;TrustServerCertificate=true;";
private const int AllDepartments = -1; // ← 追加:「すべての部署」を表す値
private readonly EmployeeRepository _employeeRepository; // ← 第23章の _repository を改名
private readonly DepartmentRepository _departmentRepository; // ← 追加
public Form1()
{
InitializeComponent();
_employeeRepository = new EmployeeRepository(ConnectionString);
_departmentRepository = new DepartmentRepository(ConnectionString); // ← 追加
}
}
書いたもの意図
AllDepartments = -1「すべての部署」を表すマーカー値(パラメータにもこの値を渡す)
_employeeRepository / _departmentRepository社員用・部署用の 2 つの DB アクセス窓口

ビルドの状態:通ります(EmployeeRepositoryDepartmentRepository は 24-4・24-5 で作成済み)。

ステップ2 画面から呼ぶ部品(ヘルパーメソッド)を用意する

Section titled “ステップ2 画面から呼ぶ部品(ヘルパーメソッド)を用意する”

イベントから呼び出す共通処理を、先に ヘルパーメソッド として用意します。これらは イベントではなく、自分のコードから呼ぶ部品 です。コンストラクタの後ろに、次の 4 つを書きます。

(1) エラー表示をまとめる ShowError

第 23 章では LoadEmployees の中に try-catch を直接書きましたが、本章では検索・プルダウン初期化でも同じエラー表示を使うので、共通メソッドに切り出します。

private static void ShowError(Exception ex)
{
string title = ex is SqlException ? "DB エラー" : "エラー";
string message = ex is SqlException sqlEx
? $"SQLServer 関連のエラーが発生しました。\nエラー番号:{sqlEx.Number}\nメッセージ:{sqlEx.Message}"
: $"予期しないエラーが発生しました。\n内容:{ex.Message}";
MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
}

(2) 部署プルダウンを初期化する InitializeDepartmentCombo

private void InitializeDepartmentCombo()
{
try
{
List<Department> departments = _departmentRepository.GetAll();
// 先頭に「すべての部署」を表す項目を追加
departments.Insert(0, new Department
{
DepartmentId = AllDepartments,
DepartmentName = "(すべての部署)"
});
comboBoxDepartment.DataSource = departments;
comboBoxDepartment.DisplayMember = nameof(Department.DepartmentName);
comboBoxDepartment.ValueMember = nameof(Department.DepartmentId);
comboBoxDepartment.SelectedValue = AllDepartments;
}
catch (Exception ex)
{
ShowError(ex);
}
}

(3) 全件表示の LoadEmployees(第 23 章とほぼ同じ。_employeeRepository を使い、エラーは ShowError に任せる)

private void LoadEmployees()
{
try
{
List<Employee> list = _employeeRepository.GetAll();
dataGridViewEmployees.DataSource = list;
}
catch (Exception ex)
{
ShowError(ex);
}
}

(4) 検索する SearchEmployees

private void SearchEmployees()
{
try
{
string keyword = textBoxKeyword.Text.Trim();
int departmentId = (int)comboBoxDepartment.SelectedValue;
List<Employee> list = _employeeRepository.Search(keyword, departmentId);
dataGridViewEmployees.DataSource = list;
}
catch (Exception ex)
{
ShowError(ex);
}
}
書いたもの意図
DisplayMember / ValueMemberプルダウンの表示は DepartmentName、値は DepartmentId
textBoxKeyword.Text.Trim()前後の空白を除去してから検索
(int)comboBoxDepartment.SelectedValue選択中の部署 ID(ValueMemberDepartmentId なので int)

ビルドの状態:通ります(まだイベントから呼んでいませんが、メソッドが存在するだけなら問題ありません)。

ステップ3 起動時に一覧と部署プルダウンを用意する(Form の Load)

Section titled “ステップ3 起動時に一覧と部署プルダウンを用意する(Form の Load)”

イベントの紐付け:Form Designer でフォームの空白部分をクリック → プロパティウィンドウの 稲妻アイコンLoad をダブルクリック(第 23 章で紐付け済みなら、生成済みの Form1_Load をそのまま使います)。

Form1_Load の中を次のようにします。第 23 章の一覧表示に加えて、部署プルダウンの初期化を呼びます。

private void Form1_Load(object sender, EventArgs e)
{
InitializeDepartmentCombo();
LoadEmployees();
}

意図:起動時に「部署の選択肢をそろえる」→「一覧を出す」の順で準備する。

ステップ4 再読込ボタン(buttonReload の Click)

Section titled “ステップ4 再読込ボタン(buttonReload の Click)”

第 23 章と同じく、buttonReload をダブルクリックして紐付け、中身は一覧の取り直しだけです。

private void buttonReload_Click(object sender, EventArgs e)
{
LoadEmployees();
}

ステップ5 検索ボタン(buttonSearch の Click)

Section titled “ステップ5 検索ボタン(buttonSearch の Click)”

イベントの紐付け:Form Designer で buttonSearchダブルクリック → 空の buttonSearch_Click が生成される。

中身は、ステップ2で用意した検索ヘルパーを呼ぶだけです。

private void buttonSearch_Click(object sender, EventArgs e)
{
SearchEmployees();
}

意図:「検索」を押したら、入力中のキーワードと部署で絞り込む。

ステップ6 クリアボタン(buttonClear の Click)

Section titled “ステップ6 クリアボタン(buttonClear の Click)”

イベントの紐付け:Form Designer で buttonClearダブルクリック → 空の buttonClear_Click が生成される。

中身は、入力を空に戻して全件を再表示します。

private void buttonClear_Click(object sender, EventArgs e)
{
textBoxKeyword.Text = string.Empty;
comboBoxDepartment.SelectedValue = AllDepartments;
LoadEmployees();
}

意図:キーワードを消し、部署を「(すべての部署)」に戻し、全件を表示し直す。

ここまでで検索・クリアが動きます。実行(F5)して、次の 24-8 のパターンで確かめてください


次のパターンで動作を確認します。

入力期待する結果
名前=「山田」、部署=「(すべての部署)」山田 二郎 だけ表示
名前=「」、部署=「総務」総務部の社員(4 名)が表示
名前=「藤」、部署=「営業」佐藤 昭夫(部分一致で藤を含む、営業)が表示
名前=「」、部署=「(すべての部署)」全 10 名表示
名前=「'」(シングルクォート 1 文字)エラーにならず、該当 0 件として表示される(SQL インジェクションが効かないことの確認)

24-9 完成形の確認(答え合わせ用)

Section titled “24-9 完成形の確認(答え合わせ用)”

まずは 24-7 のステップを自分で積み上げてから見てください

これは、24-7 のステップ1〜6 をすべて終えたあとの Form1.cs完成形 です。詰まったときの 答え合わせ に使ってください。最初からこれを貼り付けると、イベントの紐付けやメソッドの置き場所が身につきません。

Form1.cs
namespace EmployeeApp;
using Microsoft.Data.SqlClient;
public partial class Form1 : Form
{
private const string ConnectionString =
"Server=localhost;Database=TrainingDB;Integrated Security=true;TrustServerCertificate=true;";
private const int AllDepartments = -1;
private readonly EmployeeRepository _employeeRepository;
private readonly DepartmentRepository _departmentRepository;
public Form1()
{
InitializeComponent();
_employeeRepository = new EmployeeRepository(ConnectionString);
_departmentRepository = new DepartmentRepository(ConnectionString);
}
private void Form1_Load(object sender, EventArgs e)
{
InitializeDepartmentCombo();
LoadEmployees();
}
private void buttonReload_Click(object sender, EventArgs e)
{
LoadEmployees();
}
private void buttonSearch_Click(object sender, EventArgs e)
{
SearchEmployees();
}
private void buttonClear_Click(object sender, EventArgs e)
{
textBoxKeyword.Text = string.Empty;
comboBoxDepartment.SelectedValue = AllDepartments;
LoadEmployees();
}
private void InitializeDepartmentCombo()
{
try
{
List<Department> departments = _departmentRepository.GetAll();
// 先頭に「すべての部署」を表す項目を追加
departments.Insert(0, new Department
{
DepartmentId = AllDepartments,
DepartmentName = "(すべての部署)"
});
comboBoxDepartment.DataSource = departments;
comboBoxDepartment.DisplayMember = nameof(Department.DepartmentName);
comboBoxDepartment.ValueMember = nameof(Department.DepartmentId);
comboBoxDepartment.SelectedValue = AllDepartments;
}
catch (Exception ex)
{
ShowError(ex);
}
}
private void LoadEmployees()
{
try
{
List<Employee> list = _employeeRepository.GetAll();
dataGridViewEmployees.DataSource = list;
}
catch (Exception ex)
{
ShowError(ex);
}
}
private void SearchEmployees()
{
try
{
string keyword = textBoxKeyword.Text.Trim();
int departmentId = (int)comboBoxDepartment.SelectedValue;
List<Employee> list = _employeeRepository.Search(keyword, departmentId);
dataGridViewEmployees.DataSource = list;
}
catch (Exception ex)
{
ShowError(ex);
}
}
private static void ShowError(Exception ex)
{
string title = ex is SqlException ? "DB エラー" : "エラー";
string message = ex is SqlException sqlEx
? $"SQLServer 関連のエラーが発生しました。\nエラー番号:{sqlEx.Number}\nメッセージ:{sqlEx.Message}"
: $"予期しないエラーが発生しました。\n内容:{ex.Message}";
MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

症状原因対処
検索結果が常に空LIKE のワイルドカード位置間違いSQL 内で '%' + @keyword + '%' のように両側に % を付ける
ComboBox に何も表示されないDisplayMember / ValueMember のスペルミスプロパティ名を nameof(Department.DepartmentName) などで指定して typo を防ぐ
comboBoxDepartment.SelectedValuenullバインド前のタイミングで参照DataSource 設定後に SelectedValue を読む
int departmentId = (int)comboBoxDepartment.SelectedValue; で例外初期化前の nullint にキャストしようとした本文では Form1_Load 内の InitializeDepartmentCombo で必ず初期化しているため通常は発生しない。万一に備える場合は int departmentId = comboBoxDepartment.SelectedValue is int v ? v : AllDepartments; のように is int v パターンでガードする
名前検索でヒットしないTrim を忘れて末尾の空白がついているtextBoxKeyword.Text.Trim() を使う
シングルクォートを入れたら落ちたパラメータ化していない箇所が残っている+ で SQL を組み立てている箇所がないか確認
string プロパティ(DepartmentName など)に警告 CS8618 が出るプロジェクトの Nullableenable のまま(disable にし忘れ。新規プロジェクトは既定で enable)EmployeeApp.csproj<Nullable>disable</Nullable> に変更する(第 1 章 1-1・第 23 章 23-2)。これは警告でありエラーではなく、消さなくても実行はできる。実行時の「アプリケーション制御ポリシー」ブロックとは無関係(→ 付録 E)

  • SQL インジェクションとは何か、なぜ危険かを説明できる
  • パラメータ化クエリの基本パターン(@param + SqlParameter)を書ける
  • LIKE で部分一致検索を書ける
  • 検索条件が空のときに全件返す SQL の書き方を 1 つ挙げられる
  • ComboBoxDataSource / DisplayMember / ValueMember の役割を説明できる
  • 「すべての部署」のような全件表示用の特別な値(マーカー)の使い方を説明できる
  • Repository に新しいメソッド(Search)を追加できる
  • 複数の Repository(EmployeeRepositoryDepartmentRepository)をフォームから使える

  1. SQL インジェクションを 1 文で説明してください。
  2. パラメータ化クエリの「パラメータ」とは何ですか。
  3. LIKE '%' + @keyword + '%'% は何を意味しますか。
  4. 「全件」を表すために -1 を使っていますが、なぜ 0 ではなく -1 にしているか、考えてみてください。
  5. DisplayMemberValueMember の使い分けを説明してください。
  6. AddWithValue の利点と注意点を 1 つずつ挙げてください。
  7. クリアボタンを押したときの動作を、自分の言葉で 3 ステップで説明してください。

第 24 章も、第 23 章と同じ チームで自走するハンズオン形式 で進めます。 チームの役割分担(リーダー / 技術部長 / タイムキーパー)と「自走のすすめ」は 第 17 章「ここからはチームで進める」、ミニ発表の進め方は 第 23 章「演習課題」 を参照してください。本文を手順書として、ペア・チームで確認し合いながら、自分たちのペースでアプリを組み上げます(提供ソースを使うので、できあがる検索アプリは全員ほぼ同じ動作になります)。

検索が動いたら、演習課題に進みます。第 23 章と同じく、必須課題は、できあがったソースを読み解いて「なぜそう書くのか」をコメントとして書き残す 作業です。本章はとくに パラメータ化クエリ(SQL インジェクション対策) がヤマなので、その「なぜ」を自分の言葉にすることを重視します。発展課題として、検索条件のカスタマイズにも挑戦できます。


  1. チームで本文 24-2〜24-9 の 実装ステップ に沿って、検索アプリを組み上げる(= 検索が動く)。24-7 のステップ1〜6 は自分の手で。詰まったら 24-9 の完成形で答え合わせ
  2. 「24-8 動作確認」の表のパターンで動作を確かめる(シングルクォート 1 文字でも落ちない ことも忘れずに)
  3. 【必須課題 24-1】 できあがったソースに「なぜ」コメントを付ける
  4. 【発展課題 24-2 / 24-3】 余裕があれば検索条件をカスタマイズする
  5. タイムキーパーの合図で手を止め、チーム内で ミニ発表(下記)を行う

必須課題は、第 23 章から続けている EmployeeApp プロジェクト にそのまま書き込みます(「なぜ」コメントも同じプロジェクト)。発展課題だけは、同じ KadaiWinFormsApp ソリューション内に 別プロジェクト として作ります。

課題プロジェクト内容
課題 24-1(必須)EmployeeApp(本文の続き)検索アプリ本体 + 「なぜ」コメント
課題 24-2(発展)Ext_SalaryRange(新規)給与レンジ検索を追加
課題 24-3(発展)Ext_HireYearFilter(新規)入社年で絞り込み

ひととおり動いたら、チーム内で一人ずつ ごく簡単に発表 します。次の 3 つを話すだけで十分です。

  1. デモ:検索を実際に動かして見せる(名前で部分一致、部署で絞り込み、クリアで全件)
  2. 1 問説明:自分が付けた「なぜ」コメントから 1 つ、または上の「ペア確認」から 1 つを選び、自分の言葉で説明する(チームで 1 人は「なぜ + で連結せずパラメータ化するのか」 を説明できると安心です)
  3. 一言:コメントを書いていて一番「なるほど」と思った点、または詰まったポイントを一言

発表は「説明できる = 理解できている」の確認です

点数をつけるためのものではありません。本章のヤマは パラメータ化クエリで SQL インジェクションを防ぐ ことです。なぜ + で連結してはいけないのか、自分の言葉で言えるか確かめましょう。



課題 24-1 ソースを読み解いて「なぜ」コメントを付ける

Section titled “課題 24-1 ソースを読み解いて「なぜ」コメントを付ける”

本文 24-2〜24-9 の実装ステップで組み上げた検索アプリ(EmployeeApp)の Form1.csEmployeeRepository.cs を読み返し、第 23 章の課題 23-1 と同じ要領 で、次の 2 種類のコメントを書き込んでください。

  • (A) メソッドの役割:各メソッドの 上の行 に、何をするメソッドかを 1 行(//)で書く
  • (B) 難所の「なぜ」:下の表の各箇所に、「なぜそう書くのか」「何のためか」前の行 に自分の言葉で書く(言い換えコメントは NG。→ 第 23 章「課題 23-1」・第 7 章「コラム:コメントの書き方」)

本章のヤマは パラメータ化クエリ です。とくに「なぜ + で連結してはいけないのか」を自分の言葉にできるかが鍵になります。

(B) 「なぜ」コメントを付ける箇所

ファイル箇所説明する観点(= ここに「なぜ」を書く)
EmployeeRepository.cscommand.Parameters.AddWithValue("@keyword", ...)なぜ + で連結せずパラメータで渡すのか(何を防ぐか)
EmployeeRepository.csLIKE '%' + @keyword + '%'% は何を表すか / なぜ両側に付けるか
EmployeeRepository.cs@keyword = '' OR e.last_name LIKE ...なぜ @keyword = '' で「指定なし=全件」になるのか
EmployeeRepository.cs@departmentId = -1 OR e.department_id = @departmentIdなぜ -1 を「指定なし」のしるしに使うのか
EmployeeRepository.cskeyword ?? string.Emptyなぜ null を空文字に置き換えるのか
EmployeeRepository.csReadEmployeesprivate static に切り出した点なぜ GetAllSearch で共通化したのか
Form1.csprivate const int AllDepartments = -1;なぜ定数(マーカー値)にして使い回すのか
Form1.cscomboBoxDepartment.DisplayMember / ValueMemberそれぞれ何を担当しているのか
Form1.cs(int)comboBoxDepartment.SelectedValueなぜ int で取り出せるのか
Form1.cstextBoxKeyword.Text.Trim()なぜ Trim() するのか
Form1.csShowError を共通メソッドにした点なぜ各メソッドに try-catch を書かず 1 か所にまとめたのか

確認すること

  • (A) 各メソッドの上に「役割」を 1 行コメントした
  • (B) 表のすべての箇所に「なぜ/何のため」のコメントを前行で書いた
  • とくに パラメータ化の「なぜ」(SQL インジェクション対策) を自分の言葉で書けた
  • 言い換えコメントになっていない/本文の解説の丸写しになっていない
  • 検索アプリが動く(名前部分一致・部署絞り込み・クリア・シングルクォートで落ちない)


課題 24-2 給与レンジ検索を追加する

Section titled “課題 24-2 給与レンジ検索を追加する”

KadaiWinFormsApp ソリューションに新しいプロジェクト Ext_SalaryRange を作成し、EmployeeApp のコードをコピーした上で 給与の下限/上限 での絞り込みを追加してください。

仕様

  • 画面に「給与:下限 [ ] 上限 [ ]」の TextBox を 2 つ追加
  • 空欄なら「指定なし」と扱う
  • 数値以外が入力されたら MessageBox で警告(decimal.TryParse を使う)
  • 基本の形は salary >= @min AND salary <= @max。ただし 下限・上限は片方だけ指定 もありえます。空欄(指定なし)の扱いには何通りかあるので、好きな方法でどうぞ(どの方法でも 値は必ずパラメータで渡す)。
    • (ア)極端な既定値でうめる:空欄のとき @min0@max に非常に大きい値(999999999 など)を入れ、salary >= @min AND salary <= @max で一律に書く。いちばん手軽ですが、「下限 0」「上限 10 億」というマジックナンバーが出るのと、上限をいくつにするか決めうちになるのが弱点です。
    • (イ)SQL 側で「指定なし」を吸収する:WHERE (@min IS NULL OR salary >= @min) AND (@max IS NULL OR salary <= @max) と書き、空欄のパラメータには DBNull.Value を渡す。「@min が NULL なら下限条件は素通り」になり、1 本の SQL で片方だけ・両方・指定なしのすべてに対応できます。本文の検索で使った @keyword = '' OR ...(指定なしを SQL 内で表す)と同じ発想 です。
    • (ウ)C# 側で WHERE を組み立てる:指定された条件だけを WHERE に足していく。下限が入っていれば文字列に AND salary >= @min を足し、その時だけ command.Parameters.AddWithValue("@min", min) する、というふうに。実務でいちばんよく使う形で、条件が増えても拡張しやすいのが利点です。

      ここで足すのは 条件の枠(AND salary >= @min)だけ で、値は必ず @min などのパラメータで渡します。ユーザーが入れた数値そのものを SQL 文字列につなぐわけではないので、本章で学んだインジェクション対策とは矛盾しません。

  • もちろん、どの案でもパラメータ化クエリで書く

KadaiWinFormsApp ソリューションに新しいプロジェクト Ext_HireYearFilter を作成し、EmployeeApp のコードをコピーした上で 入社年(西暦) での絞り込みを追加してください。

仕様

  • 画面に「入社年:[ ]」の TextBox を 1 つ追加
  • 空欄なら「指定なし」と扱う
  • 4 桁の数字以外が入ったら MessageBox で警告
  • SQL は次のどちらで書いても構いません。どちらの場合も年や日付は必ずパラメータで渡し、SQL 文字列に年を直接埋め込まないこと(本章で学んだパラメータ化の徹底)。
    • おすすめ(まずはこちら・分かりやすさ重視):WHERE YEAR(hire_date) = @year … 入力された年をそのまま @year に渡すだけで済み、いちばん直感的に書けます。YEAR(hire_date) のように 列を関数で加工すると、その列のインデックスは使えなくなります が、研修のような小さなテーブルでは速度はまず問題になりません(データが小さいうちは、分かりやすさを優先して構いません)。
    • 速度を意識するなら:C# 側で DateTime startDate = new DateTime(year, 1, 1);DateTime endDate = new DateTime(year + 1, 1, 1); を作り、WHERE hire_date >= @startDate AND hire_date < @endDate で絞り込みます。列(hire_date)をそのまま比較するのでインデックスが効きやすく、データ量が増えても速度が落ちにくいのが利点です。終わりを 「翌年の 1 月 1 日より前」 とすることで「その年いっぱい」を取りこぼしなく拾えます(<= 12/31 のように終端を書くより安全です)。
  • 空欄(指定なし)のときの扱い も、課題 24-2 と同じく何通りかあります。どれでも構いません(やはり 年の値は必ずパラメータで渡します)。
    • 年が空欄ならそもそも年の条件を付けない:年が入っているときだけ年の条件を足し、空欄なら本章の通常の検索クエリをそのまま実行する。
    • SQL 側で吸収する:WHERE (@year IS NULL OR YEAR(hire_date) = @year) とし、空欄のときは @yearDBNull.Value を渡す(課題 24-2 の(イ)と同じやり方)。

  • 全プロジェクトが KadaiWinFormsApp ソリューションに入っている
  • 各プロジェクトで Nullable を disable にしている
  • Microsoft.Data.SqlClient を NuGet で追加した
  • パラメータ化クエリで書いている(+ での SQL 連結が残っていない)
  • シングルクォート入りの入力で落ちない
  • 各メソッドの上に「役割」を 1 行コメントした
  • 指定の難所すべてに「なぜ」コメントを前行で書いた(パラメータ化の理由を含む)
  • SqlException をキャッチして MessageBox でエラーを表示する
  • binobj.vs フォルダが Git 管理に入っていない
  • チーム内でミニ発表(デモ + 1 問説明 + 一言)を行った

完成したところまでを保存して提出します(タイマーはありません。自分のペースで区切りのよいところまで)。

Terminal window
git status
git add .
git commit -m "Chapter24: 検索アプリ完成+なぜコメント / <パラメータ化の「なぜ」を一言>"
git push origin main

提出方法:Git が使えないときはサーバへコピー

Git の調子が悪いときは、講師の指示で push の代わりに KadaiWinFormsApp フォルダをサーバ上の自分のフォルダへコピーして提出します。 その場合は、コミットメッセージの代わりに、提出先へエクスプローラーの右クリック →「新規作成」→「テキスト ドキュメント」で 提出メモ.txt を作り、「どこまで完成したか」「詰まったポイント」を書いておいてください。

Git の詳しい操作は、付録 C「Git のインストールと提出ルール」 を参照してください。


  • SQL インジェクションは、入力をそのまま SQL に連結したときに起きる重大な脆弱性
  • 防御の正しいやり方は パラメータ化クエリ(@param + SqlParameter)
  • LIKE '%' + @keyword + '%' で部分一致検索を実装できる
  • @keyword = '' OR ... の形で「指定なしなら全件」を SQL 内で表現できる
  • ComboBoxDataSource / DisplayMember / ValueMember でリストをバインドできる
  • 「すべて」のようなメタな選択肢には -1 などのマーカー値を使う
  • Repository にメソッドを追加して機能を拡張する流れを身に付けた

次章 第 25 章「Windowsフォーム社員管理アプリ:編集・更新」 では、いよいよ第 23 章から枠だけ用意していた 新規登録削除 を実装し、加えて 編集・更新(UPDATE)機能を追加します。

新しい話題として、

  • 一覧から行を選んで編集画面を開く 画面遷移
  • 編集画面での入力チェック
  • 編集後に一覧画面に戻って自動で再読み込み

を扱います。 これで Windows フォーム社員管理アプリは、CRUD(Create / Read / Update / Delete)が一通り揃った業務アプリの最小完成形になります。