第17章 DBデータをオブジェクトとして扱う
この章の目的
Section titled “この章の目的”この章では、Oracle Databaseから取得したデータを、C#のオブジェクトとして扱う方法を学習します。
第16章では、OracleDataReader で取得した値を、その場でコンソールに表示しました。
int employeeId = Convert.ToInt32(reader["employee_id"]);string employeeName = reader["employee_name"].ToString() ?? "";
Console.WriteLine($"{employeeId}:{employeeName}");この方法でも、データを表示するだけなら問題ありません。
しかし、Webアプリやデスクトップアプリでは、取得したデータをすぐに表示するだけでなく、次のような処理を行うことがあります。
一覧画面に表示する詳細画面に渡す検索結果として保持するLINQで絞り込むDataGridViewに表示するViewに渡すそのため、DBから取得した1行分のデータを、C#のクラスに変換して扱うことが重要になります。
この章では、次の流れを学習します。
employees表をSELECTする ↓1行分のデータをEmployeeオブジェクトに変換する ↓複数行をList<Employee>に入れる ↓List<Employee>をforeachやLINQで扱うこれにより、後のWebアプリ編、デスクトップアプリ編につながる準備が整います。
この章でできるようになること
Section titled “この章でできるようになること”この章を終えると、次のことができるようになります。
- DBの1行をC#のオブジェクトとして扱う意味を説明できる
EmployeeクラスをDBの列に合わせて作成できるDepartmentクラスを作成できるOracleDataReaderの1行をEmployeeオブジェクトに変換できる- 複数行の取得結果を
List<Employee>に格納できる - DBのNULLをC#の
nullとして扱える string?、int?、decimal?をDBデータに合わせて使える- DBアクセス処理をメソッドに分離できる
EmployeeRepositoryのようなクラスにDB処理をまとめる考え方を理解できる- JOIN結果を画面表示用のオブジェクトに変換できる
- 後のWebアプリ・デスクトップアプリで利用するデータ取得処理の土台を作れる
本章で使用する環境
Section titled “本章で使用する環境”| 項目 | 内容 |
|---|---|
| 開発環境 | Visual Studio 2022 |
| プロジェクト種類 | コンソール アプリ |
| 対象フレームワーク | .NET 8 |
| プロジェクト名 | Chapter17_DbObjects |
| DB | Oracle Database |
| 接続ユーザー | pingt |
| 主に使用する表 | employees、departments |
| 使用するNuGetパッケージ | Oracle.ManagedDataAccess.Core |
作業前チェック
Section titled “作業前チェック”作業を始める前に、次の内容を確認してください。
- C#からOracle Databaseに接続できる
-
OracleConnectionを使える -
OracleCommandを使える -
OracleDataReaderを使える -
reader.Read()でSELECT結果を読み取れる -
DBNull.Valueの意味を理解している -
List<T>を使える -
Employee?やdecimal?などのnull許容型を理解している - 第16章の内容をGitに提出済みである
17-1 DBの1行をオブジェクトとして考える
Section titled “17-1 DBの1行をオブジェクトとして考える”これまでの取得方法
Section titled “これまでの取得方法”第16章では、次のようにDBから値を取り出して表示しました。
while (reader.Read()){ int employeeId = Convert.ToInt32(reader["employee_id"]); string employeeName = reader["employee_name"].ToString() ?? ""; DateTime hireDate = Convert.ToDateTime(reader["hiredate"]);
Console.WriteLine($"{employeeId}:{employeeName}:{hireDate.ToString("yyyy/MM/dd")}");}このコードは、SELECT結果をその場で表示するだけなら分かりやすいです。
しかし、後からデータを検索したり、画面に渡したりする場合には、少し扱いにくくなります。
1行分をEmployeeオブジェクトにする
Section titled “1行分をEmployeeオブジェクトにする”employees 表の1行は、1人分の社員情報を表しています。
employee_idemployee_nameyomijob_idmanager_idhiredatesalarycommissiondepartment_idC#側では、この1行を Employee オブジェクトとして表すことができます。
class Employee{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } = ""; public string? Yomi { get; set; } public int JobId { get; set; } public int? ManagerId { get; set; } public DateTime HireDate { get; set; } public decimal? Salary { get; set; } public decimal? Commission { get; set; } public int DepartmentId { get; set; }}このようにすると、DBの1行をC#の1つのオブジェクトとして扱えます。
複数行はListにする
Section titled “複数行はListにする”SELECT結果が複数行ある場合は、List<Employee> に格納します。
List<Employee> employees = new List<Employee>();1行読み取るたびに Employee オブジェクトを作成し、リストに追加します。
Employee employee = new Employee();
employees.Add(employee);イメージは次の通りです。
employees表 ↓ SELECT
1行目 → Employeeオブジェクト → List<Employee>へ追加2行目 → Employeeオブジェクト → List<Employee>へ追加3行目 → Employeeオブジェクト → List<Employee>へ追加17-2 Employeeクラスを作成する
Section titled “17-2 Employeeクラスを作成する”Employeeクラス
Section titled “Employeeクラス”まず、DBの employees 表に対応する Employee クラスを作成します。
class Employee{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } = ""; public string? Yomi { get; set; } public int JobId { get; set; } public int? ManagerId { get; set; } public DateTime HireDate { get; set; } public decimal? Salary { get; set; } public decimal? Commission { get; set; } public int DepartmentId { get; set; }
public string GetDisplayText() { string salaryText = Salary.HasValue ? $"{Salary.Value}円" : "未設定";
return $"{EmployeeId}:{EmployeeName}:部署ID {DepartmentId}:給与 {salaryText}"; }}nullになる可能性がある列
Section titled “nullになる可能性がある列”employees 表には、NULLになる可能性がある列があります。
この章では、次のように扱います。
| DB列 | C#の型 | 理由 |
|---|---|---|
employee_id | int | 主キーで必ず値がある |
employee_name | string | 社員名として扱う |
yomi | string? | NULLの可能性がある |
job_id | int | 職種IDとして扱う |
manager_id | int? | 上司がいない場合がある |
hiredate | DateTime | 入社日 |
salary | decimal? | NULLの可能性がある |
commission | decimal? | NULLの可能性がある |
department_id | int | 部署ID |
? が付いている型は、値がない状態、つまり null を持てる型です。
string? Yomiint? ManagerIddecimal? Salarydecimal? Commission表示用メソッドを持たせる
Section titled “表示用メソッドを持たせる”Employee クラスには、表示用の文字列を返す GetDisplayText メソッドを持たせています。
public string GetDisplayText(){ string salaryText = Salary.HasValue ? $"{Salary.Value}円" : "未設定";
return $"{EmployeeId}:{EmployeeName}:部署ID {DepartmentId}:給与 {salaryText}";}Salary は decimal? なので、値があるかどうかを確認してから表示しています。
Salary.HasValue値がある場合は Salary.Value を使い、値がない場合は "未設定" と表示します。
17-3 DataReaderからEmployeeを作る
Section titled “17-3 DataReaderからEmployeeを作る”SELECT結果をEmployeeに変換する
Section titled “SELECT結果をEmployeeに変換する”まず、employees 表から数件のデータを取得し、Employee オブジェクトに変換します。
using System;using Oracle.ManagedDataAccess.Client;
class Program{ static void Main() { string connectionString = "User Id=pingt;Password=oracle;Data Source=localhost:1521/XEPDB1;";
string sql = @" SELECT employee_id, employee_name, yomi, job_id, manager_id, hiredate, salary, commission, department_id FROM employees ORDER BY employee_id";
try { using (OracleConnection connection = new OracleConnection(connectionString)) { connection.Open();
using (OracleCommand command = new OracleCommand(sql, connection)) using (OracleDataReader reader = command.ExecuteReader()) { while (reader.Read()) { Employee employee = new Employee();
employee.EmployeeId = Convert.ToInt32(reader["employee_id"]); employee.EmployeeName = reader["employee_name"].ToString() ?? ""; employee.Yomi = reader["yomi"] == DBNull.Value ? null : reader["yomi"].ToString(); employee.JobId = Convert.ToInt32(reader["job_id"]);
if (reader["manager_id"] == DBNull.Value) { employee.ManagerId = null; } else { employee.ManagerId = Convert.ToInt32(reader["manager_id"]); }
employee.HireDate = Convert.ToDateTime(reader["hiredate"]);
if (reader["salary"] == DBNull.Value) { employee.Salary = null; } else { employee.Salary = Convert.ToDecimal(reader["salary"]); }
if (reader["commission"] == DBNull.Value) { employee.Commission = null; } else { employee.Commission = Convert.ToDecimal(reader["commission"]); }
employee.DepartmentId = Convert.ToInt32(reader["department_id"]);
Console.WriteLine(employee.GetDisplayText()); } } } } catch (OracleException ex) { Console.WriteLine("Oracle Database処理中にエラーが発生しました。"); Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine("予期しないエラーが発生しました。"); Console.WriteLine(ex.Message); } }}
class Employee{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } = ""; public string? Yomi { get; set; } public int JobId { get; set; } public int? ManagerId { get; set; } public DateTime HireDate { get; set; } public decimal? Salary { get; set; } public decimal? Commission { get; set; } public int DepartmentId { get; set; }
public string GetDisplayText() { string salaryText = Salary.HasValue ? $"{Salary.Value}円" : "未設定";
return $"{EmployeeId}:{EmployeeName}:部署ID {DepartmentId}:給与 {salaryText}"; }}コードが長くなっていることに注目する
Section titled “コードが長くなっていることに注目する”このコードでは、while (reader.Read()) の中がかなり長くなっています。
while (reader.Read()){ Employee employee = new Employee();
employee.EmployeeId = Convert.ToInt32(reader["employee_id"]); employee.EmployeeName = reader["employee_name"].ToString() ?? ""; ...}このままだと、次の問題があります。
Mainメソッドが長くなるDB接続処理と変換処理が混ざる別の場所で再利用しにくい修正する場所が分かりにくいそこで、次の節では、OracleDataReader の1行を Employee に変換する処理をメソッドに分けます。
17-4 変換処理をメソッドに分ける
Section titled “17-4 変換処理をメソッドに分ける”CreateEmployeeメソッドを作る
Section titled “CreateEmployeeメソッドを作る”OracleDataReader の現在行から Employee オブジェクトを作成するメソッドを作ります。
static Employee CreateEmployee(OracleDataReader reader){ Employee employee = new Employee();
employee.EmployeeId = Convert.ToInt32(reader["employee_id"]); employee.EmployeeName = reader["employee_name"].ToString() ?? ""; employee.Yomi = reader["yomi"] == DBNull.Value ? null : reader["yomi"].ToString(); employee.JobId = Convert.ToInt32(reader["job_id"]); employee.ManagerId = reader["manager_id"] == DBNull.Value ? null : Convert.ToInt32(reader["manager_id"]); employee.HireDate = Convert.ToDateTime(reader["hiredate"]); employee.Salary = reader["salary"] == DBNull.Value ? null : Convert.ToDecimal(reader["salary"]); employee.Commission = reader["commission"] == DBNull.Value ? null : Convert.ToDecimal(reader["commission"]); employee.DepartmentId = Convert.ToInt32(reader["department_id"]);
return employee;}このメソッドを使うと、while の中が短くなります。
while (reader.Read()){ Employee employee = CreateEmployee(reader);
Console.WriteLine(employee.GetDisplayText());}メソッド分割後のコード
Section titled “メソッド分割後のコード”using System;using Oracle.ManagedDataAccess.Client;
class Program{ static void Main() { string connectionString = GetConnectionString();
string sql = @" SELECT employee_id, employee_name, yomi, job_id, manager_id, hiredate, salary, commission, department_id FROM employees ORDER BY employee_id";
try { using (OracleConnection connection = new OracleConnection(connectionString)) { connection.Open();
using (OracleCommand command = new OracleCommand(sql, connection)) using (OracleDataReader reader = command.ExecuteReader()) { while (reader.Read()) { Employee employee = CreateEmployee(reader);
Console.WriteLine(employee.GetDisplayText()); } } } } catch (OracleException ex) { Console.WriteLine("Oracle Database処理中にエラーが発生しました。"); Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine("予期しないエラーが発生しました。"); Console.WriteLine(ex.Message); } }
static string GetConnectionString() { return "User Id=pingt;Password=oracle;Data Source=localhost:1521/XEPDB1;"; }
static Employee CreateEmployee(OracleDataReader reader) { Employee employee = new Employee();
employee.EmployeeId = Convert.ToInt32(reader["employee_id"]); employee.EmployeeName = reader["employee_name"].ToString() ?? ""; employee.Yomi = reader["yomi"] == DBNull.Value ? null : reader["yomi"].ToString(); employee.JobId = Convert.ToInt32(reader["job_id"]); employee.ManagerId = reader["manager_id"] == DBNull.Value ? null : Convert.ToInt32(reader["manager_id"]); employee.HireDate = Convert.ToDateTime(reader["hiredate"]); employee.Salary = reader["salary"] == DBNull.Value ? null : Convert.ToDecimal(reader["salary"]); employee.Commission = reader["commission"] == DBNull.Value ? null : Convert.ToDecimal(reader["commission"]); employee.DepartmentId = Convert.ToInt32(reader["department_id"]);
return employee; }}
class Employee{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } = ""; public string? Yomi { get; set; } public int JobId { get; set; } public int? ManagerId { get; set; } public DateTime HireDate { get; set; } public decimal? Salary { get; set; } public decimal? Commission { get; set; } public int DepartmentId { get; set; }
public string GetDisplayText() { string salaryText = Salary.HasValue ? $"{Salary.Value}円" : "未設定";
return $"{EmployeeId}:{EmployeeName}:部署ID {DepartmentId}:給与 {salaryText}"; }}メソッドに分けるメリット
Section titled “メソッドに分けるメリット”処理をメソッドに分けると、次のメリットがあります。
Mainメソッドが読みやすくなる変換処理に名前を付けられる同じ変換処理を再利用しやすい修正箇所が分かりやすいこのように、DBから取得した値をオブジェクトに変換する処理は、後のアプリ開発でも重要になります。
17-5 Listとして取得する
Section titled “17-5 Listとして取得する”複数件をListに格納する
Section titled “複数件をListに格納する”次に、SELECT結果を List<Employee> として取得します。
using System;using System.Collections.Generic;using Oracle.ManagedDataAccess.Client;
class Program{ static void Main() { try { List<Employee> employees = GetAllEmployees();
foreach (Employee employee in employees) { Console.WriteLine(employee.GetDisplayText()); }
Console.WriteLine($"取得件数:{employees.Count}件"); } catch (OracleException ex) { Console.WriteLine("Oracle Database処理中にエラーが発生しました。"); Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine("予期しないエラーが発生しました。"); Console.WriteLine(ex.Message); } }
static List<Employee> GetAllEmployees() { List<Employee> employees = new List<Employee>();
string connectionString = GetConnectionString();
string sql = @" SELECT employee_id, employee_name, yomi, job_id, manager_id, hiredate, salary, commission, department_id FROM employees ORDER BY employee_id";
using (OracleConnection connection = new OracleConnection(connectionString)) { connection.Open();
using (OracleCommand command = new OracleCommand(sql, connection)) using (OracleDataReader reader = command.ExecuteReader()) { while (reader.Read()) { Employee employee = CreateEmployee(reader);
employees.Add(employee); } } }
return employees; }
static string GetConnectionString() { return "User Id=pingt;Password=oracle;Data Source=localhost:1521/XEPDB1;"; }
static Employee CreateEmployee(OracleDataReader reader) { Employee employee = new Employee();
employee.EmployeeId = Convert.ToInt32(reader["employee_id"]); employee.EmployeeName = reader["employee_name"].ToString() ?? ""; employee.Yomi = reader["yomi"] == DBNull.Value ? null : reader["yomi"].ToString(); employee.JobId = Convert.ToInt32(reader["job_id"]); employee.ManagerId = reader["manager_id"] == DBNull.Value ? null : Convert.ToInt32(reader["manager_id"]); employee.HireDate = Convert.ToDateTime(reader["hiredate"]); employee.Salary = reader["salary"] == DBNull.Value ? null : Convert.ToDecimal(reader["salary"]); employee.Commission = reader["commission"] == DBNull.Value ? null : Convert.ToDecimal(reader["commission"]); employee.DepartmentId = Convert.ToInt32(reader["department_id"]);
return employee; }}
class Employee{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } = ""; public string? Yomi { get; set; } public int JobId { get; set; } public int? ManagerId { get; set; } public DateTime HireDate { get; set; } public decimal? Salary { get; set; } public decimal? Commission { get; set; } public int DepartmentId { get; set; }
public string GetDisplayText() { string salaryText = Salary.HasValue ? $"{Salary.Value}円" : "未設定";
return $"{EmployeeId}:{EmployeeName}:部署ID {DepartmentId}:給与 {salaryText}"; }}GetAllEmployeesメソッド
Section titled “GetAllEmployeesメソッド”次のメソッドに注目してください。
static List<Employee> GetAllEmployees()このメソッドは、DBから社員一覧を取得し、List<Employee> として返します。
GetAllEmployees → 全社員を取得する → List<Employee>として返すMain メソッド側では、DBの詳細を意識せず、次のように使えます。
List<Employee> employees = GetAllEmployees();この形は、後のWebアプリやデスクトップアプリでも重要です。
17-6 LINQで取得結果を扱う
Section titled “17-6 LINQで取得結果を扱う”DBから取得したListをLINQで絞り込む
Section titled “DBから取得したListをLINQで絞り込む”第12章で学習したLINQは、DBから取得した List<Employee> に対しても使えます。
using System;using System.Collections.Generic;using System.Linq;using Oracle.ManagedDataAccess.Client;
class Program{ static void Main() { try { List<Employee> employees = GetAllEmployees();
List<Employee> department1Employees = employees .Where(employee => employee.DepartmentId == 1) .ToList();
foreach (Employee employee in department1Employees) { Console.WriteLine(employee.GetDisplayText()); }
Console.WriteLine($"部署ID 1 の社員数:{department1Employees.Count}件"); } catch (OracleException ex) { Console.WriteLine("Oracle Database処理中にエラーが発生しました。"); Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine("予期しないエラーが発生しました。"); Console.WriteLine(ex.Message); } }
// GetAllEmployees、GetConnectionString、CreateEmployee、Employeeクラスは前節と同じ}DB側で絞り込むか、C#側で絞り込むか
Section titled “DB側で絞り込むか、C#側で絞り込むか”この例では、いったん全件を取得してからC#側で絞り込んでいます。
List<Employee> employees = GetAllEmployees();
List<Employee> results = employees .Where(employee => employee.DepartmentId == 1) .ToList();しかし、実務では件数が多い場合、DB側でWHERE句を使って絞り込むことが多いです。
SELECT ...FROM employeesWHERE department_id = :departmentIdこの章では、次のように考えてください。
少量のデータで学習する場合 → List<T>とLINQの練習としてC#側で絞り込んでもよい
実務で大量データを扱う場合 → できるだけDB側で絞り込む給与が未設定の社員を抽出する
Section titled “給与が未設定の社員を抽出する”Salary は decimal? なので、null かどうかで絞り込めます。
List<Employee> noSalaryEmployees = employees .Where(employee => employee.Salary == null) .ToList();給与が設定されている社員だけを取り出す場合は、次のように書けます。
List<Employee> hasSalaryEmployees = employees .Where(employee => employee.Salary.HasValue) .ToList();null許容型を使っておくと、DBのNULLをC#側でも自然に扱えます。
17-7 Departmentクラスを作成する
Section titled “17-7 Departmentクラスを作成する”Departmentクラス
Section titled “Departmentクラス”次に、departments 表に対応する Department クラスを作成します。
class Department{ public int DepartmentId { get; set; } public string DepartmentName { get; set; } = ""; public int ManagerId { get; set; }
public string GetDisplayText() { return $"{DepartmentId}:{DepartmentName}:管理者ID {ManagerId}"; }}departments表をListとして取得する
Section titled “departments表をListとして取得する”using System;using System.Collections.Generic;using Oracle.ManagedDataAccess.Client;
class Program{ static void Main() { try { List<Department> departments = GetAllDepartments();
foreach (Department department in departments) { Console.WriteLine(department.GetDisplayText()); } } catch (OracleException ex) { Console.WriteLine("Oracle Database処理中にエラーが発生しました。"); Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine("予期しないエラーが発生しました。"); Console.WriteLine(ex.Message); } }
static List<Department> GetAllDepartments() { List<Department> departments = new List<Department>();
string connectionString = GetConnectionString();
string sql = @" SELECT department_id, department_name, manager_id FROM departments ORDER BY department_id";
using (OracleConnection connection = new OracleConnection(connectionString)) { connection.Open();
using (OracleCommand command = new OracleCommand(sql, connection)) using (OracleDataReader reader = command.ExecuteReader()) { while (reader.Read()) { Department department = new Department();
department.DepartmentId = Convert.ToInt32(reader["department_id"]); department.DepartmentName = reader["department_name"].ToString() ?? ""; department.ManagerId = Convert.ToInt32(reader["manager_id"]);
departments.Add(department); } } }
return departments; }
static string GetConnectionString() { return "User Id=pingt;Password=oracle;Data Source=localhost:1521/XEPDB1;"; }}
class Department{ public int DepartmentId { get; set; } public string DepartmentName { get; set; } = ""; public int ManagerId { get; set; }
public string GetDisplayText() { return $"{DepartmentId}:{DepartmentName}:管理者ID {ManagerId}"; }}17-8 Repositoryクラスに分ける
Section titled “17-8 Repositoryクラスに分ける”DBアクセス処理をMainから分離する
Section titled “DBアクセス処理をMainから分離する”ここまで、Program クラスの中にDBアクセス用のメソッドを書いてきました。
しかし、後のWebアプリやデスクトップアプリでは、DBアクセス処理を専用のクラスに分けることが多いです。
この章では、社員データを取得するクラスとして EmployeeRepository を作成します。
Program 画面表示や実行の流れを担当する
EmployeeRepository employees表へのアクセスを担当する
Employee 社員1人分のデータを表すEmployeeRepositoryクラス
Section titled “EmployeeRepositoryクラス”using System;using System.Collections.Generic;using Oracle.ManagedDataAccess.Client;
class Program{ static void Main() { try { EmployeeRepository repository = new EmployeeRepository();
List<Employee> employees = repository.GetAll();
foreach (Employee employee in employees) { Console.WriteLine(employee.GetDisplayText()); } } catch (OracleException ex) { Console.WriteLine("Oracle Database処理中にエラーが発生しました。"); Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine("予期しないエラーが発生しました。"); Console.WriteLine(ex.Message); } }}
class EmployeeRepository{ private string _connectionString = "User Id=pingt;Password=oracle;Data Source=localhost:1521/XEPDB1;";
public List<Employee> GetAll() { List<Employee> employees = new List<Employee>();
string sql = @" SELECT employee_id, employee_name, yomi, job_id, manager_id, hiredate, salary, commission, department_id FROM employees ORDER BY employee_id";
using (OracleConnection connection = new OracleConnection(_connectionString)) { connection.Open();
using (OracleCommand command = new OracleCommand(sql, connection)) using (OracleDataReader reader = command.ExecuteReader()) { while (reader.Read()) { Employee employee = CreateEmployee(reader);
employees.Add(employee); } } }
return employees; }
private Employee CreateEmployee(OracleDataReader reader) { Employee employee = new Employee();
employee.EmployeeId = Convert.ToInt32(reader["employee_id"]); employee.EmployeeName = reader["employee_name"].ToString() ?? ""; employee.Yomi = reader["yomi"] == DBNull.Value ? null : reader["yomi"].ToString(); employee.JobId = Convert.ToInt32(reader["job_id"]); employee.ManagerId = reader["manager_id"] == DBNull.Value ? null : Convert.ToInt32(reader["manager_id"]); employee.HireDate = Convert.ToDateTime(reader["hiredate"]); employee.Salary = reader["salary"] == DBNull.Value ? null : Convert.ToDecimal(reader["salary"]); employee.Commission = reader["commission"] == DBNull.Value ? null : Convert.ToDecimal(reader["commission"]); employee.DepartmentId = Convert.ToInt32(reader["department_id"]);
return employee; }}
class Employee{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } = ""; public string? Yomi { get; set; } public int JobId { get; set; } public int? ManagerId { get; set; } public DateTime HireDate { get; set; } public decimal? Salary { get; set; } public decimal? Commission { get; set; } public int DepartmentId { get; set; }
public string GetDisplayText() { string salaryText = Salary.HasValue ? $"{Salary.Value}円" : "未設定";
return $"{EmployeeId}:{EmployeeName}:部署ID {DepartmentId}:給与 {salaryText}"; }}Repositoryとは
Section titled “Repositoryとは”Repositoryは、データの取得や保存を担当するクラスとして使われることがあります。
この章では、次のように理解してください。
Repository → DBアクセス処理をまとめるクラスEmployeeRepository は、employees 表へのアクセスを担当します。
EmployeeRepository repository = new EmployeeRepository();
List<Employee> employees = repository.GetAll();Main メソッド側は、SQL文や OracleDataReader の細かい処理を知らなくても、社員一覧を取得できます。
17-9 FindByIdメソッドを作成する
Section titled “17-9 FindByIdメソッドを作成する”社員IDで1件検索する
Section titled “社員IDで1件検索する”EmployeeRepository に、社員IDで1件検索する FindById メソッドを追加します。
public Employee? FindById(int employeeId){ string sql = @" SELECT employee_id, employee_name, yomi, job_id, manager_id, hiredate, salary, commission, department_id FROM employees WHERE employee_id = :employeeId";
using (OracleConnection connection = new OracleConnection(_connectionString)) { connection.Open();
using (OracleCommand command = new OracleCommand(sql, connection)) { command.Parameters.Add("employeeId", OracleDbType.Int32).Value = employeeId;
using (OracleDataReader reader = command.ExecuteReader()) { if (reader.Read()) { return CreateEmployee(reader); } } } }
return null;}該当する社員が見つかった場合は Employee オブジェクトを返します。
見つからなかった場合は null を返します。
FindByIdを呼び出す
Section titled “FindByIdを呼び出す”using System;using System.Collections.Generic;using Oracle.ManagedDataAccess.Client;
class Program{ static void Main() { try { EmployeeRepository repository = new EmployeeRepository();
Console.WriteLine("検索する社員IDを入力してください。"); string input = Console.ReadLine() ?? "";
if (!int.TryParse(input, out int employeeId)) { Console.WriteLine("社員IDは整数で入力してください。"); return; }
Employee? employee = repository.FindById(employeeId);
if (employee == null) { Console.WriteLine("該当する社員は見つかりませんでした。"); } else { Console.WriteLine(employee.GetDisplayText()); } } catch (OracleException ex) { Console.WriteLine("Oracle Database処理中にエラーが発生しました。"); Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine("予期しないエラーが発生しました。"); Console.WriteLine(ex.Message); } }}FindById の戻り値は Employee? です。
Employee? employee = repository.FindById(employeeId);見つからない場合は null になるため、必ず null チェックを行います。
if (employee == null){ Console.WriteLine("該当する社員は見つかりませんでした。");}17-10 JOIN結果を画面表示用オブジェクトにする
Section titled “17-10 JOIN結果を画面表示用オブジェクトにする”EmployeeとDepartmentを結合した結果
Section titled “EmployeeとDepartmentを結合した結果”一覧画面では、department_id ではなく部署名を表示したいことがあります。
その場合、SQLで employees と departments をJOINします。
SELECT e.employee_id, e.employee_name, d.department_name, e.hiredate, e.salaryFROM employees e JOIN departments d ON e.department_id = d.department_idORDER BY e.employee_idこの結果は、employees 表そのものの1行ではありません。
社員情報に部署名を加えた、画面表示用の形です。
そこで、画面表示用のクラスを作ります。
EmployeeListItemクラス
Section titled “EmployeeListItemクラス”class EmployeeListItem{ public int EmployeeId { get; set; } public string EmployeeName { get; set; } = ""; public string DepartmentName { get; set; } = ""; public DateTime HireDate { get; set; } public decimal? Salary { get; set; }
public string GetDisplayText() { string salaryText = Salary.HasValue ? $"{Salary.Value}円" : "未設定";
return $"{EmployeeId}:{EmployeeName}:{DepartmentName}:入社日 {HireDate.ToString("yyyy/MM/dd")}:給与 {salaryText}"; }}EmployeeListItem は、社員一覧表示に必要な情報だけを持つクラスです。
Employee → employees表の1行に対応するクラス
EmployeeListItem → 一覧表示用のデータを表すクラスJOIN結果をListにする
Section titled “JOIN結果をListにする”public List<EmployeeListItem> GetEmployeeListItems(){ List<EmployeeListItem> items = new List<EmployeeListItem>();
string sql = @" SELECT e.employee_id, e.employee_name, d.department_name, e.hiredate, e.salary FROM employees e JOIN departments d ON e.department_id = d.department_id ORDER BY e.employee_id";
using (OracleConnection connection = new OracleConnection(_connectionString)) { connection.Open();
using (OracleCommand command = new OracleCommand(sql, connection)) using (OracleDataReader reader = command.ExecuteReader()) { while (reader.Read()) { EmployeeListItem item = new EmployeeListItem();
item.EmployeeId = Convert.ToInt32(reader["employee_id"]); item.EmployeeName = reader["employee_name"].ToString() ?? ""; item.DepartmentName = reader["department_name"].ToString() ?? ""; item.HireDate = Convert.ToDateTime(reader["hiredate"]); item.Salary = reader["salary"] == DBNull.Value ? null : Convert.ToDecimal(reader["salary"]);
items.Add(item); } } }
return items;}EmployeeRepository repository = new EmployeeRepository();
List<EmployeeListItem> items = repository.GetEmployeeListItems();
foreach (EmployeeListItem item in items){ Console.WriteLine(item.GetDisplayText());}実行結果例:
1001:山田二郎:総務:入社日 2001/04/01:給与 500000円1002:佐藤昭夫:営業:入社日 2001/04/01:給与 500000円1003:山口洋子:開発:入社日 2001/10/01:給与 500000円画面表示用クラスを分ける理由
Section titled “画面表示用クラスを分ける理由”DBのテーブル構造と、画面に表示したい情報は、必ずしも一致しません。
たとえば、employees 表には department_id があります。
しかし、画面では部署IDよりも部署名を表示したいことがあります。
DB上の社員情報 department_id = 1
画面表示 部署名 = 総務このような場合、一覧表示用のクラスを別に作ると扱いやすくなります。
Employee DBのemployees表に近い形
EmployeeListItem 画面表示に必要な形後のMVCやWinFormsでも、この考え方は重要です。
17-11 小さな構成に整理する
Section titled “17-11 小さな構成に整理する”この章での構成
Section titled “この章での構成”この章の最終的な構成は、次のようになります。
Program 実行の流れを担当する
EmployeeRepository Oracle Databaseから社員データを取得する
Employee employees表の1行を表す
EmployeeListItem 社員一覧表示用のデータを表す
Department departments表の1行を表す後のWebアプリでの利用イメージ
Section titled “後のWebアプリでの利用イメージ”Webアプリでは、ControllerからRepositoryを呼び出し、取得した一覧をViewに渡すことになります。
Controller ↓EmployeeRepository ↓List<EmployeeListItem> ↓View後のデスクトップアプリでの利用イメージ
Section titled “後のデスクトップアプリでの利用イメージ”デスクトップアプリでは、ボタンをクリックしたときにRepositoryを呼び出し、取得した一覧をDataGridViewに表示することになります。
Button Click ↓EmployeeRepository ↓List<EmployeeListItem> ↓DataGridViewつまり、この章で作る「DBからデータを取得し、オブジェクトとして返す処理」は、後の章でそのまま土台になります。
よくあるつまずき
Section titled “よくあるつまずき”この章でよくあるつまずきを確認します。
| つまずき | 原因 | 対応 |
|---|---|---|
| DBの1行をオブジェクトにする意味が分からない | 直接表示する処理に慣れている | 後で画面に渡すためにオブジェクト化すると考える |
Employee クラスのプロパティが多くて難しい | DB列に合わせているため | まず主要項目だけでも理解する |
decimal? が分からない | DBのNULLとC#のnull許容型がつながっていない | NULLになる列は ? を付けると考える |
DBNull.Value を忘れる | DBのNULLを直接変換しようとしている | 変換前に必ずNULL確認する |
CreateEmployee の役割が分からない | メソッド分割に慣れていない | DataReaderの1行をEmployeeに変換する専用メソッド |
List<Employee> にする意味が分からない | 複数行データをまとめる感覚が弱い | SELECT結果全体をC#側で持つためと考える |
| Repositoryの意味が分からない | クラス分割に慣れていない | DBアクセスを担当するクラスと考える |
FindById の戻り値が Employee? になる理由が分からない | 見つからない可能性を考慮していない | 検索結果が0件ならnullを返す |
Employee と EmployeeListItem の違いが分からない | DB用と画面表示用の役割が混ざっている | DBの1行用か、画面表示用かで分ける |
学んだことチェック
Section titled “学んだことチェック”次の項目について、自分で説明できるか確認してください。
- DBの1行をC#のオブジェクトとして扱う意味を説明できる
-
Employeeクラスを作成できる - DBのNULLになる列に
string?、int?、decimal?を使える -
OracleDataReaderからEmployeeオブジェクトを作成できる -
DBNull.Valueを確認してから値を変換できる -
List<Employee>に複数行の結果を格納できる -
GetAllEmployeesのようなメソッドを作成できる -
EmployeeRepositoryの役割を説明できる -
FindByIdで1件検索できる - 見つからない場合に
nullを返す設計を理解できる - JOIN結果を
EmployeeListItemのような画面表示用クラスに変換できる - 後のWebアプリやデスクトップアプリで、この章の考え方が使われることを説明できる
研修の進め方によっては、隣の人または近くの人と説明確認を行います。
次の内容を、自分の言葉で説明してください。
- なぜDBのSELECT結果をそのまま表示せず、
Employeeオブジェクトに変換するのですか。 List<Employee>は何を表していますか。- DBのNULLをC#で扱うとき、何を確認する必要がありますか。
decimal? Salaryの?は何を意味していますか。CreateEmployeeメソッドは何をするメソッドですか。EmployeeRepositoryは何を担当するクラスですか。FindByIdの戻り値がEmployee?になる理由は何ですか。EmployeeとEmployeeListItemの違いは何ですか。
説明するときは、完全な答えでなくても構いません。
自分の言葉で説明しようとすることが大切です。
この章の演習課題に取り組みます。
制限時間は 90分 です。
時間内にすべて完成しなくても構いません。
できたところまでを保存し、Gitに提出してください。
まずは、全員が必須課題に取り組んでください。
課題17-1 Employeeクラスを作成する
Section titled “課題17-1 Employeeクラスを作成する”employees 表に対応する Employee クラスを作成してください。
プロパティ:
| プロパティ名 | 型 |
|---|---|
EmployeeId | int |
EmployeeName | string |
Yomi | string? |
JobId | int |
ManagerId | int? |
HireDate | DateTime |
Salary | decimal? |
Commission | decimal? |
DepartmentId | int |
条件:
- NULLになる可能性がある列には
?を付ける - 表示用の
GetDisplayTextメソッドを作成する
課題17-2 DataReaderからEmployeeを作成する
Section titled “課題17-2 DataReaderからEmployeeを作成する”OracleDataReader の現在行から Employee オブジェクトを作成する CreateEmployee メソッドを作成してください。
仕様:
static Employee CreateEmployee(OracleDataReader reader)条件:
reader["employee_id"]などから値を取り出すDBNull.Valueを確認するSalary、Commission、ManagerIdなどのNULLに対応する
課題17-3 Listで全社員を取得する
Section titled “課題17-3 Listで全社員を取得する”employees 表から全社員を取得し、List<Employee> として返す GetAllEmployees メソッドを作成してください。
仕様:
static List<Employee> GetAllEmployees()条件:
List<Employee>を作成するreader.Read()で1行ずつ読み取るCreateEmployee(reader)でEmployeeを作成するemployees.Add(employee)でリストに追加する- 最後にリストを返す
課題17-4 取得した社員一覧を表示する
Section titled “課題17-4 取得した社員一覧を表示する”課題17-3で作成した GetAllEmployees を呼び出し、全社員を表示してください。
実行結果例:
1001:山田二郎:部署ID 1:給与 500000円1002:佐藤昭夫:部署ID 2:給与 500000円...条件:
foreachで表示するGetDisplayTextメソッドを使ってもよい- 取得件数も表示する
必須課題が終わった人は、発展課題に取り組んでください。
課題17-5 LINQで部署IDを絞り込む
Section titled “課題17-5 LINQで部署IDを絞り込む”GetAllEmployees で取得した List<Employee> から、部署IDが 1 の社員だけを表示してください。
条件:
using System.Linq;を使うWhereを使うToList()を使う- 件数を表示する
課題17-6 EmployeeRepositoryクラスを作成する
Section titled “課題17-6 EmployeeRepositoryクラスを作成する”DBアクセス処理を EmployeeRepository クラスに分離してください。
条件:
EmployeeRepositoryクラスを作成するGetAllメソッドを作成するCreateEmployeeメソッドはEmployeeRepositoryの中に移動するProgramクラスからrepository.GetAll()を呼び出す
課題17-7 FindByIdメソッドを作成する
Section titled “課題17-7 FindByIdメソッドを作成する”EmployeeRepository に、社員IDで1件検索する FindById メソッドを追加してください。
仕様:
public Employee? FindById(int employeeId)条件:
- SQLパラメーターを使う
- 見つかった場合は
Employeeを返す - 見つからなかった場合は
nullを返す - 呼び出し側で
nullチェックを行う
課題17-8 Departmentクラスを作成する
Section titled “課題17-8 Departmentクラスを作成する”departments 表に対応する Department クラスを作成してください。
プロパティ:
| プロパティ名 | 型 |
|---|---|
DepartmentId | int |
DepartmentName | string |
ManagerId | int |
条件:
GetDisplayTextメソッドを作成するdepartments表から一覧を取得して表示する
課題17-9 EmployeeListItemクラスを作成する
Section titled “課題17-9 EmployeeListItemクラスを作成する”社員一覧表示用の EmployeeListItem クラスを作成してください。
プロパティ:
| プロパティ名 | 型 |
|---|---|
EmployeeId | int |
EmployeeName | string |
DepartmentName | string |
HireDate | DateTime |
Salary | decimal? |
条件:
employeesとdepartmentsをJOINする- JOIN結果を
EmployeeListItemに変換する List<EmployeeListItem>として返す- コンソールに一覧表示する
課題17-10 検索メニューを作成する
Section titled “課題17-10 検索メニューを作成する”コンソール上で、次のメニューを表示してください。
1:全社員表示2:社員ID検索3:部署ID検索0:終了番号を選択してください。条件:
while文でメニューを繰り返すswitch文を使うEmployeeRepositoryのメソッドを呼び出す- まずはSELECTのみでよい
- 入力値が不正な場合はエラーメッセージを表示する
Gitへの提出
Section titled “Gitへの提出”課題が終わったら、できたところまでをGitに提出します。
まず、現在の状態を確認します。
git status変更されたファイルを追加します。
git add .コミットします。
git commit -m "Chapter17 DBデータをオブジェクトとして扱う"ファイルサーバー上のリポジトリへpushします。
git pushGitの操作でエラーが出た場合は、自己判断で同じ操作を繰り返さず、講師に確認してください。
提出前チェックリスト
Section titled “提出前チェックリスト”提出前に、次の項目を確認してください。
-
Employeeクラスを作成できている - DBのNULLに対応するプロパティをnull許容型にしている
-
OracleDataReaderからEmployeeを作成できている -
DBNull.Valueを確認してから変換している -
List<Employee>に複数行を格納できている -
foreachで社員一覧を表示できている -
EmployeeRepositoryにDBアクセス処理を分離できている -
FindByIdで1件検索できている - 見つからない場合の
nullチェックができている - JOIN結果を
EmployeeListItemに変換できている -
using文でDB接続を安全に閉じている -
try-catchで例外処理を行っている - インデントが整っている
- Gitにcommitしている
- Gitにpushしている
この章のまとめ
Section titled “この章のまとめ”この章では、DBから取得したデータをC#のオブジェクトとして扱う方法を学習しました。
この章で学んだ主な内容は次の通りです。
- DBの1行分のデータは、C#の1つのオブジェクトとして扱える
employees表の1行は、Employeeオブジェクトとして表せる- SELECT結果の複数行は、
List<Employee>として扱える OracleDataReaderの現在行からオブジェクトを作成する処理をメソッドに分けると読みやすくなる- DBのNULLは
DBNull.Valueとして返されるため、変換前に確認する必要がある string?、int?、decimal?を使うと、DBのNULLをC#のnullとして扱いやすいEmployeeRepositoryのようなクラスにDBアクセス処理をまとめると、呼び出し側のコードが読みやすくなるFindByIdのような検索メソッドでは、見つからない場合にnullを返す設計ができる- JOIN結果は、画面表示用の
EmployeeListItemのようなクラスに変換できる - DBのテーブル構造と画面表示用のデータ構造は、必ずしも同じでなくてよい
次章からは、ASP.NET Core MVCによるWebアプリの基本 に入ります。
この章で作成したような Employee、EmployeeRepository、EmployeeListItem の考え方を使いながら、Webアプリで社員一覧や社員詳細を表示する流れを学習します。