.Net 6.0 の Blazor Serverから、Dapper+Microsoft.Data.SqlClient NuGetパッケージを使い、SQLServerデータベースへDBトランザクション処理を行うサンプルを作成しました。
ORマッピングの主流はDapperになって来てる。
ソースコードはGitHubで公開しています。
Youtube動画だと、DBトランザクション継続中に SQLServer Management Studio側からの接続に対してレスポンスが返って来ないのは、SQLServer側でロックが発生している為です。
ビジネスロジックやデータベース処理を、Repogitoryや Interfaceを駆使して実装すると、ソースコードが複雑になり保守性が下がります。
staticクラスで実装するとソースコードがシンプルになり保守性が高くなります。
staticクラスで実装したソースコードは、ユニットテストの実装も容易です。
Repogitoryや Interfaceを駆使して無駄な処理を大量に実装し、工数が爆発して開発が終わらなくなっているプロジェクトが多過ぎる。駆使するべきはstatic。
いまだに、C#のマルチスレッド処理が分からず「staticを使うのは不安だ」と言っている人がいて、その人の意見が採用され「static禁止」になると、開発工数が爆発します。
ソースコード構成
今回使った Visual Studio プロジェクト テンプレートは、Visual Studio 2022 + .Net 6 + Blazor Server アプリ の HTTPS無し。
テンプレートに対して、下記の処理を加えました。
ソースコード変更内容を解説
/BlazorApp1.csproj
Dapper/Microsoft.Data.SqlClient NuGetパッケージをインストール。
SQLSeverへ接続するライブラリは、System.Data.SqlClientではなく Microsoft.Data.SqlClientが推奨です。
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="Microsoft.Data.SqlClient" Version="5.0.1" /> </ItemGroup> </Project> |
/appsettings.Development.json
SQLServerへ接続するコネクションストリングを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "SQLServerDB": "Data Source=localhost;Initial Catalog=TestDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False" } } |
/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> /// SQLServer 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")["SQLServerDB"]; // 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
・SQLServerから取得したデータに対応する、データモデルクラスを追加。
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 TableA { public long Id; public string Code; public string? ValueString; public DateTime? ValueDate; } } |
/Pages/Counter.razor
・SQLServerデータベースに対して、DBトランザクション処理を実行する「DB Transaction SQLServer」ボタンを追加。
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">DB Transaction SQLServer</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } private void Transaction1() { try { var insertTableA = new TableA() { Id = 1, Code = "1", ValueString = "1", ValueDate = DateTime.Now }; SQL_TableA.Transaction1(insertTableA); } catch (Exception ex) { throw; } } } |
/SQL/SQL_TableA.cs
・DBトランザクション処理を行う Transaction1メソッドから、Insert SQLを発行する Insertメソッド、Select SQLを発行する Selectメソッドを呼び出しています。
・BeginTransactionでトランザクションを開始し、DBコネクション変数(conn)とトランザクション変数(tran)を各メソッドへ渡し、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 82 |
using System.Data; using BlazorApp1.Model; using BlazorApp1.Shared; using Dapper; using Microsoft.Extensions.Logging; using Microsoft.Data.SqlClient; namespace BlazorApp1.SQL { public class SQL_TableA { public static void Transaction1(TableA insertTableA) { using (var conn = new SqlConnection(SharedData.ConnectionString)) { conn.Open(); using (var tran = conn.BeginTransaction(IsolationLevel.ReadCommitted)) { try { var tableAList = Select(conn, tran); Insert(conn, tran, insertTableA); var tableAList2 = Select(conn, tran); tran.Commit(); } catch(Exception ex) { tran.Rollback(); throw; } } } } public static IEnumerable<TableA> Select(SqlConnection conn, SqlTransaction tran) { const string sql = " SELECT " + " Id, " + " Code, " + " ValueString, " + " ValueDate " + " FROM " + " dbo.TableA "; return conn.Query<TableA>(sql, null, tran); } public static void Insert(SqlConnection conn, SqlTransaction tran, TableA _TableA) { const string sql = " INSERT INTO dbo.TableA( " + " Id, " + " Code, " + " ValueString, " + " ValueDate " + " ) VALUES ( " + " @id, " + " @code, " + " @valueString, " + " @valueDate " + " ); "; var sqlParam = new { id = _TableA.Id, code = _TableA.Code, valueString = _TableA.ValueString, valueDate = _TableA.ValueDate }; conn.Execute(sql, sqlParam, tran); } } } |
/_Imports.razor
・追加した classを、各Razorページで使用できるように Usingを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@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 |
Select対象のDBテーブル構成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
USE [TestDb] GO /****** Object: Table [dbo].[TableA] Script Date: 2023/01/22 14:14:39 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[TableA]( [Id] [bigint] NULL, [ValueString] [nvarchar](50) NULL, [Code] [nvarchar](50) NULL, [ValueDate] [datetime] NULL ) ON [PRIMARY] GO |
コメント