Skip to content

第17章 DBデータをオブジェクトとして扱う

この章では、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アプリ・デスクトップアプリで利用するデータ取得処理の土台を作れる

項目内容
開発環境Visual Studio 2022
プロジェクト種類コンソール アプリ
対象フレームワーク.NET 8
プロジェクト名Chapter17_DbObjects
DBOracle Database
接続ユーザーpingt
主に使用する表employeesdepartments
使用するNuGetパッケージOracle.ManagedDataAccess.Core

作業を始める前に、次の内容を確認してください。

  • 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行をオブジェクトとして考える”

第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_id
employee_name
yomi
job_id
manager_id
hiredate
salary
commission
department_id

C#側では、この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つのオブジェクトとして扱えます。


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>へ追加

まず、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}";
}
}

employees 表には、NULLになる可能性がある列があります。

この章では、次のように扱います。

DB列C#の型理由
employee_idint主キーで必ず値がある
employee_namestring社員名として扱う
yomistring?NULLの可能性がある
job_idint職種IDとして扱う
manager_idint?上司がいない場合がある
hiredateDateTime入社日
salarydecimal?NULLの可能性がある
commissiondecimal?NULLの可能性がある
department_idint部署ID

? が付いている型は、値がない状態、つまり null を持てる型です。

string? Yomi
int? ManagerId
decimal? Salary
decimal? Commission

Employee クラスには、表示用の文字列を返す GetDisplayText メソッドを持たせています。

public string GetDisplayText()
{
string salaryText = Salary.HasValue ? $"{Salary.Value}" : "未設定";
return $"{EmployeeId}{EmployeeName}:部署ID {DepartmentId}:給与 {salaryText}";
}

Salarydecimal? なので、値があるかどうかを確認してから表示しています。

Salary.HasValue

値がある場合は Salary.Value を使い、値がない場合は "未設定" と表示します。


まず、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 変換処理をメソッドに分ける”

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());
}

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}";
}
}

処理をメソッドに分けると、次のメリットがあります。

Mainメソッドが読みやすくなる
変換処理に名前を付けられる
同じ変換処理を再利用しやすい
修正箇所が分かりやすい

このように、DBから取得した値をオブジェクトに変換する処理は、後のアプリ開発でも重要になります。


次に、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}";
}
}

次のメソッドに注目してください。

static List<Employee> GetAllEmployees()

このメソッドは、DBから社員一覧を取得し、List<Employee> として返します。

GetAllEmployees
→ 全社員を取得する
→ List<Employee>として返す

Main メソッド側では、DBの詳細を意識せず、次のように使えます。

List<Employee> employees = GetAllEmployees();

この形は、後のWebアプリやデスクトップアプリでも重要です。


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 employees
WHERE department_id = :departmentId

この章では、次のように考えてください。

少量のデータで学習する場合
→ List<T>とLINQの練習としてC#側で絞り込んでもよい
実務で大量データを扱う場合
→ できるだけDB側で絞り込む

給与が未設定の社員を抽出する

Section titled “給与が未設定の社員を抽出する”

Salarydecimal? なので、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#側でも自然に扱えます。


次に、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}";
}
}

DBアクセス処理をMainから分離する

Section titled “DBアクセス処理をMainから分離する”

ここまで、Program クラスの中にDBアクセス用のメソッドを書いてきました。

しかし、後のWebアプリやデスクトップアプリでは、DBアクセス処理を専用のクラスに分けることが多いです。

この章では、社員データを取得するクラスとして EmployeeRepository を作成します。

Program
画面表示や実行の流れを担当する
EmployeeRepository
employees表へのアクセスを担当する
Employee
社員1人分のデータを表す

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は、データの取得や保存を担当するクラスとして使われることがあります。

この章では、次のように理解してください。

Repository
→ DBアクセス処理をまとめるクラス

EmployeeRepository は、employees 表へのアクセスを担当します。

EmployeeRepository repository = new EmployeeRepository();
List<Employee> employees = repository.GetAll();

Main メソッド側は、SQL文や OracleDataReader の細かい処理を知らなくても、社員一覧を取得できます。


17-9 FindByIdメソッドを作成する

Section titled “17-9 FindByIdメソッドを作成する”

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 を返します。


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で employeesdepartments をJOINします。

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

この結果は、employees 表そのものの1行ではありません。

社員情報に部署名を加えた、画面表示用の形です。

そこで、画面表示用のクラスを作ります。


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
→ 一覧表示用のデータを表すクラス

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でも、この考え方は重要です。


この章の最終的な構成は、次のようになります。

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からデータを取得し、オブジェクトとして返す処理」は、後の章でそのまま土台になります。


この章でよくあるつまずきを確認します。

つまずき原因対応
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を返す
EmployeeEmployeeListItem の違いが分からないDB用と画面表示用の役割が混ざっているDBの1行用か、画面表示用かで分ける

次の項目について、自分で説明できるか確認してください。

  • DBの1行をC#のオブジェクトとして扱う意味を説明できる
  • Employee クラスを作成できる
  • DBのNULLになる列に string?int?decimal? を使える
  • OracleDataReader から Employee オブジェクトを作成できる
  • DBNull.Value を確認してから値を変換できる
  • List<Employee> に複数行の結果を格納できる
  • GetAllEmployees のようなメソッドを作成できる
  • EmployeeRepository の役割を説明できる
  • FindById で1件検索できる
  • 見つからない場合に null を返す設計を理解できる
  • JOIN結果を EmployeeListItem のような画面表示用クラスに変換できる
  • 後のWebアプリやデスクトップアプリで、この章の考え方が使われることを説明できる

研修の進め方によっては、隣の人または近くの人と説明確認を行います。

次の内容を、自分の言葉で説明してください。

  1. なぜDBのSELECT結果をそのまま表示せず、Employee オブジェクトに変換するのですか。
  2. List<Employee> は何を表していますか。
  3. DBのNULLをC#で扱うとき、何を確認する必要がありますか。
  4. decimal? Salary? は何を意味していますか。
  5. CreateEmployee メソッドは何をするメソッドですか。
  6. EmployeeRepository は何を担当するクラスですか。
  7. FindById の戻り値が Employee? になる理由は何ですか。
  8. EmployeeEmployeeListItem の違いは何ですか。

説明するときは、完全な答えでなくても構いません。
自分の言葉で説明しようとすることが大切です。


この章の演習課題に取り組みます。

制限時間は 90分 です。

時間内にすべて完成しなくても構いません。
できたところまでを保存し、Gitに提出してください。


まずは、全員が必須課題に取り組んでください。


課題17-1 Employeeクラスを作成する

Section titled “課題17-1 Employeeクラスを作成する”

employees 表に対応する Employee クラスを作成してください。

プロパティ:

プロパティ名
EmployeeIdint
EmployeeNamestring
Yomistring?
JobIdint
ManagerIdint?
HireDateDateTime
Salarydecimal?
Commissiondecimal?
DepartmentIdint

条件:

  • 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 を確認する
  • SalaryCommissionManagerId などの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 クラスを作成してください。

プロパティ:

プロパティ名
DepartmentIdint
DepartmentNamestring
ManagerIdint

条件:

  • GetDisplayText メソッドを作成する
  • departments 表から一覧を取得して表示する

課題17-9 EmployeeListItemクラスを作成する

Section titled “課題17-9 EmployeeListItemクラスを作成する”

社員一覧表示用の EmployeeListItem クラスを作成してください。

プロパティ:

プロパティ名
EmployeeIdint
EmployeeNamestring
DepartmentNamestring
HireDateDateTime
Salarydecimal?

条件:

  • employeesdepartments をJOINする
  • JOIN結果を EmployeeListItem に変換する
  • List<EmployeeListItem> として返す
  • コンソールに一覧表示する

課題17-10 検索メニューを作成する

Section titled “課題17-10 検索メニューを作成する”

コンソール上で、次のメニューを表示してください。

1:全社員表示
2:社員ID検索
3:部署ID検索
0:終了
番号を選択してください。

条件:

  • while 文でメニューを繰り返す
  • switch 文を使う
  • EmployeeRepository のメソッドを呼び出す
  • まずはSELECTのみでよい
  • 入力値が不正な場合はエラーメッセージを表示する

課題が終わったら、できたところまでをGitに提出します。

まず、現在の状態を確認します。

Terminal window
git status

変更されたファイルを追加します。

Terminal window
git add .

コミットします。

Terminal window
git commit -m "Chapter17 DBデータをオブジェクトとして扱う"

ファイルサーバー上のリポジトリへpushします。

Terminal window
git push

Gitの操作でエラーが出た場合は、自己判断で同じ操作を繰り返さず、講師に確認してください。


提出前に、次の項目を確認してください。

  • 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している

この章では、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アプリの基本 に入ります。

この章で作成したような EmployeeEmployeeRepositoryEmployeeListItem の考え方を使いながら、Webアプリで社員一覧や社員詳細を表示する流れを学習します。