.Net 6.0 の ASP.NET Core Web APIから、Dapper+Microsoft.Data.SqlClient NuGetパッケージを使い、SQL ServerデータベースへDBトランザクション処理を行うサンプルを作成しました。
ORマッピングの主流はDapperになって来てる。
ソースコードはGitHubで公開しています。
Youtube動画だと、DBトランザクション継続中に SQL Server Management Studio側からの接続に対してレスポンスが返って来ないのは、SQL Server側でロックが発生している為です。
ビジネスロジックやデータベース処理を、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/Microsoft.Data.SqlClient 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="Microsoft.Data.SqlClient" Version="5.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> </ItemGroup> </Project> |
/appsettings.Development.json
SQLServerへ接続するコネクションストリングを追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "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 WebApplication1.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 |
using WebApplication1.Shared; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; SharedData.ConnectionString = configuration.GetSection("ConnectionStrings")["SQLServerDB"]; // 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
・SQLServerから取得したデータに対応する、データモデルクラスを追加。
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace WebApplication1.Model { public class TableA { public long Id { get; set; } public string Code { get; set; } public string? ValueString { get; set; } public DateTime? ValueDate { get; set; } } } |
/Controllers/WeatherForecastController.cs
・SQLServerデータベースに対して、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(TableA insertTableA) { SQL_TableA.Transaction1(insertTableA); return Ok(); } } } |
/SQL/SQL_TableA.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 82 |
using System.Data; using WebApplication1.Model; using WebApplication1.Shared; using Dapper; using Microsoft.Extensions.Logging; using Microsoft.Data.SqlClient; namespace WebApplication1.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); } } } |
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 |
コメント