.Net 6.0 の Blazor Serverから、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 + Blazor Server アプリ の HTTPS無し。
テンプレートに対して、下記の処理を加えました。
ソースコード変更内容を解説
/BlazorApp1.csproj
Dapper/Npgsql NuGetパッケージをインストール。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<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.0" /> </ItemGroup> </Project> |
/appsettings.Development.json
PostgreSQLへ接続するコネクションストリングを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "DetailedErrors": true, "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 BlazorApp1.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 31 32 33 34 |
using BlazorApp1.Data; using BlazorApp1.Shared; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; SharedData.ConnectionString = configuration.GetSection("ConnectionStrings")["PostgreSQLDB"]; // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddSingleton<WeatherForecastService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); app.Run(); |
/Model/TableModels.cs
・PostgreSQLから取得したデータに対応する、データモデルクラスを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using static System.Net.Mime.MediaTypeNames; namespace BlazorApp1.Model { public class m_table_a { public long id; public string code; public string? value_string; public DateTime? value_date; } } |
/Pages/Counter.razor
・PostgreSQLデータベースに対して、DBトランザクション処理を実行する「Select from PostgreSQL」ボタンを追加。
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 |
@page "/counter" <PageTitle>Counter</PageTitle> <h1>Counter</h1> <p role="status">Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> <button class="btn btn-primary" @onclick="Transaction1">Select from PostgreSQL</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } private void Transaction1() { try { var insert_m_table_a = new m_table_a() { id = 1, code = "1", value_string = "1", value_date = DateTime.Now }; SQL_m_table_a.Transaction1(insert_m_table_a); } catch (Exception ex) { throw; } } } |
/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 BlazorApp1.Model; using BlazorApp1.Shared; using Dapper; using Microsoft.Extensions.Logging; using Npgsql; namespace BlazorApp1.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); } } } |
/_Imports.razor
・追加した classを、各Razorページで使用できるように Usingを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@using System.Net.Http @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using BlazorApp1 @using BlazorApp1.Model @using BlazorApp1.Shared @using BlazorApp1.SQL @using Npgsql |
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; |
コメント