.Net 6.0 の ASP.NET Core Web APIから、Dapper+Npgsql NuGetパッケージを使い、PostgreSQLデータベースへ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/Npgsql 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="Npgsql" Version="7.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> </ItemGroup> </Project> |
/appsettings.Development.json
PostgreSQLへ接続するコネクションストリングを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "PostgreSQLDB": "User ID=testuser;Password=testuser;Host=localhost;Port=5432;Database=testdb;Pooling=true;CommandTimeout=600;" } } |
/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> /// PostgreSQL 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")["PostgreSQLDB"]; // 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
・PostgreSQLから取得したデータに対応する、データモデルクラスを追加。
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace WebApplication1.Model { public class m_table_a { 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
・PostgreSQLデータベースに対して、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(m_table_a insert_m_table_a) { SQL_m_table_a.Transaction1(insert_m_table_a); return Ok(); } } } |
/SQL/SQL_m_table_a.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 Npgsql; namespace WebApplication1.SQL { public class SQL_m_table_a { public static void Transaction1(m_table_a insert_m_table_a) { using (var conn = new NpgsqlConnection(SharedData.ConnectionString)) { conn.Open(); using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) { try { var List_m_table_a1 = Select(conn); Insert(conn, insert_m_table_a); var List_m_table_a2 = Select(conn); tran.Commit(); } catch { tran.Rollback(); throw; } } } } public static IEnumerable<m_table_a> Select(NpgsqlConnection conn) { const string sql = " SELECT " + " id, " + " code, " + " value_string, " + " value_date " + " FROM " + " test_schema.m_table_a "; return conn.Query<m_table_a>(sql); } public static void Insert(NpgsqlConnection conn, m_table_a _m_table_a) { const string sql = " INSERT INTO test_schema.m_table_a( " + " id, " + " value_string, " + " value_date, " + " code " + " ) VALUES ( " + " @id, " + " @value_string, " + " @value_date, " + " @code " + " ); "; var sqlParam = new { id = _m_table_a.id, code = _m_table_a.code, value_string = _m_table_a.value_string, value_date = _m_table_a.value_date }; conn.Execute(sql, sqlParam); } } } |
Select対象のDBテーブル構成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
-- Table: test_schema.m_table_a -- DROP TABLE IF EXISTS test_schema.m_table_a; CREATE TABLE IF NOT EXISTS test_schema.m_table_a ( id bigint NOT NULL, value_string text COLLATE pg_catalog."default", value_date date, code text COLLATE pg_catalog."default", CONSTRAINT m_table_a_pkey PRIMARY KEY (id) ) TABLESPACE pg_default; ALTER TABLE IF EXISTS test_schema.m_table_a OWNER to testuser; |
コメント