.Net 6.0 の ASP.NET Core Web APIから、Dapper+Oracle.EntityFrameworkCore NuGetパッケージを使い、ORACLEデータベースへDBトランザクション処理を行うサンプルを作成しました。
ORマッピングの主流はDapperになって来てる。
ソースコードはGitHubで公開しています。
ビジネスロジックやデータベース処理を、Repogitoryや Interfaceを駆使して実装すると、ソースコードが複雑になり保守性が下がります。
staticクラスで実装するとソースコードがシンプルになり保守性が高くなります。
staticクラスで実装したソースコードは、ユニットテストの実装も容易です。
Repogitoryや Interfaceを駆使して無駄な処理を大量に実装し、工数が爆発して開発が終わらなくなっているプロジェクトが多過ぎる。駆使するべきはstatic。
いまだに、C#のマルチスレッド処理が分からず「staticを使うのは不安だ」と言っている人がいて、その人の意見が採用され「static禁止」になると、開発工数が爆発します。
ソースコード構成
今回使った Visual Studio プロジェクト テンプレートは、Visual Studio 2022 + .Net 6 + ASP.NET Core Web API アプリ の HTTPS無し。
テンプレートに対して、下記の処理を加えました。
ソースコード変更内容を解説
/WebApplication1.csproj
Dapper/Oracle.EntityFrameworkCore NuGetパッケージをインストール。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Oracle.EntityFrameworkCore" Version="7.21.9" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> </ItemGroup> </Project> |
/appsettings.Development.json
ORACLEへ接続するコネクションストリングを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "OracleDB": "User Id = TESTUSER1; Password = testuser1password; Data Source = (DESCRIPTION = (ADDRESS = (PROTOCOL = tcp)(HOST = LOCALHOST)(PORT = 1521))(CONNECT_DATA = (SERVICE_NAME = ORCL1)));" } } |
/Shared/SharedData.cs
・staticメンバ変数で実装した、アプリ全体で使えるコネクションストリング変数を追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
namespace WebApplication1.Shared { public static class SharedData { /// <summary> /// ORACLE DB 接続文字列 /// </summary> public static string ConnectionString; } } |
/Program.cs
・アプリ起動時に環境設定ファイルを読み込み、アプリ全体で使えるコネクションストリング変数にセットする処理を追加。
環境設定ファイルの読み込み処理をアプリ起動時1回だけにし、アプリ起動後は staticメンバ変数を直接参照することで、処理コストを減らせる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using WebApplication1.Shared; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; SharedData.ConnectionString = configuration.GetSection("ConnectionStrings")["OracleDB"]; // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run(); |
/Model/TableModels.cs
・ORACLEから取得したデータに対応する、データモデルクラスを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
namespace WebApplication1.Model { public class TABLE1 { public long ID { get; set; } public string CODE { get; set; } public string? VALUE_STRING { get; set; } public DateTime? VALUE_DATE { get; set; } } } |
/Controllers/WeatherForecastController.cs
・ORACLEデータベースに対して、DBトランザクション処理を実行する、HTTP POSTリクエストWEBAPIを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
using Microsoft.AspNetCore.Mvc; using WebApplication1.Model; using WebApplication1.SQL; namespace WebApplication1.Controllers { [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } [HttpGet(Name = "GetWeatherForecast")] public IEnumerable<WeatherForecast> Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } [HttpPost(Name = "PostWeatherForecast")] public IActionResult PostWeatherForecast(TABLE1 insertTABLE1) { SQL_TABLE1.Transaction1(insertTABLE1); return Ok(); } } } |
/SQL/SQL_TABLE1.cs
・DBトランザクション処理を行う Transaction1メソッドから、Insert SQLを発行する Insertメソッド、Select SQLを発行する Selectメソッドを呼び出しています。
・BeginTransactionでトランザクションを開始したDBコネクション変数(conn)を各メソッドに渡し、Dapperを通してSQLを発行することで、とてもシンプルで高速なDBトランザクション処理を実装しています。
・razorページには画面処理のみ、SQLフォルダ配下の staticクラスにはDB処理と、レイヤー分けをしています。
今回は簡単なロジックなので2レイヤーにしています。業務システムだとビジネスロジックレイヤーのソースコードをDataフォルダ配下に纏め、「画面レイヤー(Pagesフォルダ) => ビジネスロジックレイヤー(Dataフォルダ) => データベースレイヤー(Sqlフォルダ)」と繋げていくイメージです。
ビジネスロジックレイヤーを設ける場合、トランザクション処理の開始処理(BeginTransaction)はビジネスロジックレイヤーに実装します。
・データベース処理を行う、SQL文を伴うソースコードはSQLフォルダに纏まっていると分かり易い。
・Insert文のパラメータは匿名クラスにし、DapperにSQLとパラメータを渡して、Insert SQLを発行しています。
・データベースとC#側のデータ型が一致していれば、あとは Dapperがバインドしてくれる。
・SQL文は定数化しておくことで処理コストを減らしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
using System.Data; using WebApplication1.Model; using WebApplication1.Shared; using Dapper; using Microsoft.Extensions.Logging; using Oracle.ManagedDataAccess.Client; namespace WebApplication1.SQL { public class SQL_TABLE1 { public static void Transaction1(TABLE1 insertTABLE1) { using (var conn = new OracleConnection(SharedData.ConnectionString)) { conn.Open(); using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) { try { var table1List = Select(conn); Insert(conn, tran, insertTABLE1); var table1List2 = Select(conn); tran.Commit(); } catch { tran.Rollback(); throw; } } } } public static IEnumerable<TABLE1> Select(OracleConnection conn) { const string sql = " SELECT " + " id, " + " code, " + " value_string, " + " value_date " + " FROM " + " testuser1.table1 "; return conn.Query<TABLE1>(sql); } public static void Insert(OracleConnection conn, OracleTransaction tran, TABLE1 insertTABLE1) { const string sql = " INSERT INTO testuser1.table1( " + " id, " + " value_string, " + " value_date, " + " code " + " ) VALUES ( " + " :id, " + " :value_string, " + " :value_date, " + " :code " + " ) "; var sqlParam = new { id = insertTABLE1.ID, code = insertTABLE1.CODE, value_string = insertTABLE1.VALUE_STRING, value_date = insertTABLE1.VALUE_DATE }; conn.Execute(sql, sqlParam, tran); } } } |
Select対象のDBテーブル構成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
-------------------------------------------------------- -- DDL for Table TABLE1 -------------------------------------------------------- CREATE TABLE "TESTUSER1"."TABLE1" ( "ID" NUMBER, "CODE" VARCHAR2(20 BYTE), "VALUE_STRING" NVARCHAR2(20), "VALUE_DATE" DATE ) SEGMENT CREATION IMMEDIATE PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) TABLESPACE "TABLESPACE1" ; -------------------------------------------------------- -- DDL for Index TABLE1_PK -------------------------------------------------------- CREATE UNIQUE INDEX "TESTUSER1"."TABLE1_PK" ON "TESTUSER1"."TABLE1" ("ID") PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) TABLESPACE "TABLESPACE1" ; -------------------------------------------------------- -- Constraints for Table TABLE1 -------------------------------------------------------- ALTER TABLE "TESTUSER1"."TABLE1" ADD CONSTRAINT "TABLE1_PK" PRIMARY KEY ("ID") USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT) TABLESPACE "TABLESPACE1" ENABLE; ALTER TABLE "TESTUSER1"."TABLE1" MODIFY ("ID" NOT NULL ENABLE); |
コメント