星期四, 12月 28, 2023

[.NET] 在 BackgroundService 內使用 Quartz

在 BackgroundService 內使用 Quartz 3.8 來處理排程需求,Quartz 相關請參考之前筆記

安裝 Quartz 套件

必須安裝兩個套件

建立 Quartz Job

把 Log DI 注入後,使用 LogWarming 在畫面上輸出文字
using Quartz;

namespace QuartzWorkerSample
{
    [DisallowConcurrentExecution]
    public class HelloWorldJob : IJob
    {
        private readonly ILogger<HelloWorldJob> _logger;
        public HelloWorldJob(ILogger<HelloWorldJob> logger)
        {
            _logger = logger;
        }

        public Task Execute(IJobExecutionContext context)
        {
            _logger.LogWarning($"Hello World {DateTime.Now} and Jobtype: {context.JobDetail.JobType}");
            return Task.CompletedTask;
        }
    }
}  

在 Program 中使用 Quartz 

會有兩個文字輸出,分別為
  • QuartzHostedService:每 5 秒輸出
  • BackgroundService:即為 .NET 8 預設 Worker,每 1 秒輸出
QuartzHostedService 有實作 IHostService,可以變成背景服務來使用
C# 語法
using Quartz;

namespace QuartzWorkerSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = Host.CreateApplicationBuilder(args);

            builder.Services.AddQuartz(config =>
            {
                config.AddJob<HelloWorldJob>(config => config
                    .WithIdentity(nameof(HelloWorldJob)));

                config.AddTrigger(config => config
                    .ForJob(nameof(HelloWorldJob))
                    .WithIdentity("HelloWorldJob-Trigger")
                    .WithSimpleSchedule(x => x
                        .WithIntervalInSeconds(5) // 每五秒觸發一次
                        .RepeatForever()));
            });

            // QuartzHostedService 實作 IHostService
            builder.Services.AddQuartzHostedService(options => options
                .WaitForJobsToComplete = true);

            // 該 Worker 為 .NET 8 預設範本
            builder.Services.AddHostedService<Worker>();

            var host = builder.Build();
            host.Run();
        }
    }
}
執行結果
網路教學常見語法
builder.Services.AddQuartz(config =>
{
    config.UseMicrosoftDependencyInjectionScopedJobFactory();
    config.UseMicrosoftDependencyInjectionJobFactory();
});
UseMicrosoftDependencyInjectionScopedJobFactory 在 V3.3.2 開始 Job Factory 為 Scoped Jobs,不在需要使用它,詳見官方文章 - Microsoft DI Integration
UseMicrosoftDependencyInjectionJobFactory 在 V3.7 時 MS DI 變成預設 DI,不需要去設定,詳見 V3.7 release討論

看到這兩個語法就把它 pass 掉吧

星期三, 12月 27, 2023

[VFP] 報表預覽在 Form 後方

使用者回報,為什麼報表列印預覽,只有列印控制跳出來而已,沒有看見報表預覽,然後程式就像當機一樣都無法操作,只能強制關閉後重開系統,去操作發現報表預覽竟然在 Form 後方,如下圖


確認 Form 屬性 AlwaysOnTop 有被開啟,關閉後就恢復正常報表預覽效果

星期一, 12月 25, 2023

[SQL] ANSI_WARNINGS

收到某隻排程商業邏輯維護執行失敗通知,錯誤訊息如下
異質性查詢需要為連線設定 ANSI_NULLS 和 ANSI_WARNINGS 選項。這樣才能確保一致的查詢語意。請啟用這些選項再重新發出您的查詢。 [SQLSTATE 42000] (錯誤 7405). 步驟失敗。

公司維護 sp 都是放在 master 底下,script 會指定 dbname,但沒有使用到 Linked Server 才是,且是某天突然收到執行失敗,打開 sp 來觀察,發現有明確關閉 ANSI_WARNINGS,該設定效果如下
  • When set to ON, if null values appear in aggregate functions, such as SUM, AVG, MAX, MIN, STDEV, STDEVP, VAR, VARP, or COUNT, a warning message is generated. When set to OFF, no warning is issued.
  • When set to ON, the divide-by-zero and arithmetic overflow errors cause the statement to be rolled back and an error message is generated. When set to OFF, the divide-by-zero and arithmetic overflow errors cause null values to be returned. The behavior in which a divide-by-zero or arithmetic overflow error causes null values to be returned occurs
  • if an INSERT or UPDATE is tried on a character, Unicode, or binary column in which the length of a new value exceeds the maximum size of the column. If SET ANSI_WARNINGS is ON, the INSERT or UPDATE is canceled as specified by the ISO standard. Trailing blanks are ignored for character columns and trailing nulls are ignored for binary columns. When OFF, data is truncated to the size of the column and the statement succeeds.
該 sp 是彙總統計整理相關,可能是為了避免彙總時有 null 出現發出警告才把 ANSI_WARNINGS 關閉。

仔細閱讀後發現到真正原因在於一個 view,它最近變成 Linked Server 必須跨 Server 去抓資料,所以才導致該錯誤訊息發生,現況去檢查也看不出會彙總時會有 null 發生,把 ANSI_WARNINGS ON 起來結案

星期三, 12月 20, 2023

SATA 線

維修 PC 時看電源偵測器說明書時發現,才發現 SATA 線有 4 條和 5 條的差異

說明書上的文字說明
有個別客戶反應:插上 SATA 介面 測試器的 +3.3V 燈不亮,因為現在市場上的電源 SATA 介面有 4 根線和 5 根線的哦!首先要確認您的電源 SATA 介面是 4 根線還是 5 根線,如果是 4 根線 那是沒有 +3.3V 輸出的,要 5 根線的才有 +3.3V 輸出的。

SATA 4 條線
SATA 5 條線
看說明書長知識

星期二, 12月 19, 2023

[.NET] 在 BackgroundService 內使用 NLog

在 BackgroundService 內使用 NLog 5.2.7 來處理 Log,主要進行三種輸出,分別為 txt 檔案、DB 和 Console,NLog 相關請參考之前筆記
NLog 5.2.7 在筆記時官方說明還沒有 .NET 8 在上面,但可以進行安裝,DB 連線部分則是安裝 Microsoft.Data.SqlClinet 來使用,而非 System.Data.SqlClient,下圖為安裝套件



Programes
using NLog.Config;
using NLog.Extensions.Logging;
using NLog.Targets;

namespace NLogWorkerSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
            builder.Services.AddHostedService<Worker>();

            builder.Logging.ClearProviders();
            builder.Logging.AddNLog(NLogConfigurationInit());

            var host = builder.Build();
            host.Run();
        }

        private static LoggingConfiguration NLogConfigurationInit()
        {
            LoggingConfiguration config = new LoggingConfiguration();

            // Target 1:把錯誤訊息輸出至 txt 檔案 
            FileTarget errorFile = new FileTarget("errorfile") { FileName = @"D:\ErrorFile.txt" };
            config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, errorFile);

            // Target 2:把錯誤訊息 insert 至 DB 
            DatabaseTarget target = new DatabaseTarget();
            target.ConnectionString = "Data Source=.;Initial Catalog=AdventureWorks2022;Integrated Security=True;TrustServerCertificate=True";
            target.CommandText = "insert into LogTable(LogTime,Message) values(@LogTime, @Message);";
            target.Parameters.Add(new DatabaseParameterInfo() { Name = "@LogTime", Layout = "${date}" });
            config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, target);

            // Target 3:把錯誤訊息輸出至 Console
            ColoredConsoleTarget coloredConsole = new ColoredConsoleTarget("ColoredConsole");
            config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, coloredConsole);

            return config;
        }
    }
}

.NET 8 預設 Worker 範本
namespace NLogWorkerSample
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                if (_logger.IsEnabled(LogLevel.Information))
                {
                    _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                }
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

執行結果

寫進 txt 檔案


  DB insert
 


Console 輸出

星期二, 12月 12, 2023

[EF] 此憑證鏈結是由不受信任的授權單位發出的

操作 edmx 時連線 local SQL Server 時突然出現這個錯誤訊息


了解該錯誤訊息相關資訊後發現,原來 Microsoft.Data.SqlClient 4.0 開始會強制加密,官方文件 - Introduction to Microsoft.Data.SqlClient namespace 
進 VS 伺服器總管內觀察連線,發現 [加密選項] 已經被設定為 [Mandatory (True)],把該選項設定為 [Optional (False)] 或是勾選下方 [信任伺服器憑證] 都可以正常連線


詳細查該連線使用的 provider 發現沒有使用到  Microsoft.Data.SqlClient,看起來就是加密被啟用,不知道為什麼突然被啟用就是


星期六, 12月 09, 2023

[.NET] 在 BackgroundService 內讀取 Windows 事件

管理 Windows 事件檢視器 內有紀錄使用 .NET Framework Windows Service 來進行管理,該筆記要改為 .NET BackgroundService 來達到,原以為 [C#] EventLog 在 .NET 上會有所變化必須重新學習,但發現是無縫接軌
操作時一直會有綠蚯蚓的提示訊息,說明只能用 Windows 系統上而已
改寫 Worker

抓取各 EventLog 的最新錯誤訊息來顯示,該 ExecuteAsync 執行後,只會 run 一次而已
using System.Diagnostics;

namespace EventWorkerSample
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            foreach (EventLog el in EventLog.GetEventLogs())
            {
                EventLogEntry ele = el.Entries
                    .OfType<EventLogEntry>()
                    .Where(entry => entry.EntryType == EventLogEntryType.Error)
                    .OrderByDescending(o => o.TimeWritten)
                    .Take(1)
                    .SingleOrDefault();

                if (ele == null)
                    continue;

                string errorMessage = $@"
                    Log 名稱:{el.Log} - {el.LogDisplayName}
                    錯誤訊息:{ele.Message.Substring(0, 50)}
                    ";

                _logger.LogError(errorMessage);
            }

            await Task.CompletedTask;
        }
    }
}

星期五, 12月 08, 2023

[.NET] 在 BackgroundService 內使用 EFCore

延續 [.NET] 使用 BackgroundService 建立 Windows Service 筆記,該文章內的 JokeService 隨機抓取資料顯示行為改用 EFCore 讀取資料庫來取代,EF Core 安裝和連線字串調整請參考該筆記 - [EFCore] 在 Console 專案上安裝設定

因為 AddHostedService 和 DbContext 生命週期因素,一併把 .NET DI 知識補齊,參考文章如下
理解 DI 生命週期後就知道為什麼要透過 IServiceProvider 來存取 DbContext 囉

JobService

透過 IServiceProvider 來抓取 DbContext,而非直接把 DbContext DI 進 JokeService 內
using EFCoreWorkerSample.Models;
using Microsoft.EntityFrameworkCore;

namespace EFCoreWorkerSample
{
    public class JokeService
    {
        private readonly IServiceProvider _serviceProvider;

        public JokeService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public string GetJoke()
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<EfcoreDbContext>();

            // 從 DB 隨機抓取一個 Joke 出來顯示
            string tsql = @"
                    SELECT TOP 1 *
                    FROM Jokes
                    ORDER BY NEWID()
                    ";

            var joke = dbContext.Jokes.FromSqlRaw(tsql).Single();

            return $@"
                輸出時間:{DateTime.Now}
                問題:{joke.Q}
                答案:{joke.A}";
        }

    }
}

改寫 Worker 內容

namespace EFCoreWorkerSample
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly JokeService _jokeService;

        public Worker(ILogger<Worker> logger, JokeService jokeService)
        {
            _logger = logger;
            _jokeService = jokeService;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string message = _jokeService.GetJoke();

                _logger.LogInformation(message);

                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

Program.cs 

Service 擴充方法預設生命週期
  • AddHostedService:singletion
  • AddDbContext:scoped
using EFCoreWorkerSample.Models;

namespace EFCoreWorkerSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = Host.CreateApplicationBuilder(args);

            // 使用 AddHostedService 來註冊實作 IHostedService 類別,其生命週期為 Singleton
            builder.Services.AddHostedService<Worker>();

            builder.Services.AddScoped<EfcoreDbContext>();

            builder.Services.AddSingleton<JokeService>();

            var host = builder.Build();
            host.Run();
        }
    }
}

執行結果

DI 生命週期驗證

因為 AddHostedService 註冊 Worker 生命週期為 singletion,所以 JokeService DI 進去時也只能使用 singletion,換成其他生命週期 (scoped 或 transient),就會拋出 exception 啦
Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: EFCoreWorkerSample.Worker': Cannot consume scoped service 'EFCoreWorkerSample.JokeService' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.)

星期四, 12月 07, 2023

[EFCore] 在 Console 專案上安裝設定

在 .NET 8 Console 專案上安裝 EFCore 並調整資料庫連線,該筆記以 DBFirst 為主

EFCore 套件安裝

使用 EFCore 相關 nuget 套件,分別為
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tool
安裝 Microsoft.EntityFrameworkCore.Tool 才能使用 Scaffold-DbContext 
套件管理器主控台內進行安裝
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools


使用 Scaffold-DbContext 來建立 entity

在 [套件管理器主控台] 內輸入下列語法
Scaffold-DbContext "Server=.;Database=AdventureWorks2022;Trusted_Connection=True;TrustServerCertificate=true" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Force
參數說明
  • -OutputDir:產生的 Model 要放在哪一個資料夾內
  • -Force :強制覆蓋
Scaffold-DbContext  建立 entity 後,可以在 DbContext OnConfiguring() 內發現連線字串警告
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
=> optionsBuilder.UseSqlServer("Server=.;Database=AdventureWorks2022;Trusted_Connection=True;TrustServerCertificate=true");

使用 appsetings.json 來儲存資料庫連線


原本是想找 [JSON 檔案] 範本來新增 appsettings.json 檔案,但是發現在 Console Project 內不會有它,最後是選擇 [JavaScript JSON 組態檔] 來新增並進行修改,參考資料 - can't add appsettings.json

把 appsettings.json 檔案屬性 [複製到輸出目錄] 修改為 [永遠複製]
把連線字串從 OnConfiguring 內移至 appsettings.json 內
{
  "ConnectionStrings": {
    "SqlServer": "Server=.;Database=AdventureWorks2022;Trusted_Connection=True;TrustServerCertificate=true"
  }
}

主程式

在 Console Project 內安裝
  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.Json
下述程式會從 appsettings.json 內抓出連線字串,並把連線字串塞進 DbContext 內
using CRUDSimple.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CRUDSimple
{
    internal class Program
    {
        static void Main(string[] args)
        {

            string connectionString = GetConnectionString();

            var options = new DbContextOptionsBuilder<AdventureWorks2022Context>()
                .UseSqlServer(connectionString)
                .Options;

            using (AdventureWorks2022Context dbContext = new AdventureWorks2022Context(options))
            {
                var source = dbContext.Employees
                    .OrderBy(e => e.HireDate)
                    .Take(10)
                    .ToList();

                foreach (Employee e in source)
                    Console.WriteLine(e.JobTitle);
            }
        }

        private static string GetConnectionString()
        {
            IConfiguration configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .Build();

            // 從 appsettings 取得連線字串
            // 寫法一
            // string connectionString = configuration.GetSection("ConnectionString").GetSection("SqlServer").Value;
            // 寫法二
            string connectionString = configuration.GetConnectionString("SqlServer");

            return connectionString;
        }
    }
} 

星期三, 12月 06, 2023

GodexEZ 1300 Plus 本機分享

在使用者 PC 上有台用 USB 連接的標籤機 - GodexEZ 1300 Plus,因為機器本身沒有網路功能,一直被定位在單機使用,但現況是需要分享出來給其他 PC 來進行列印

在本機上設定標籤機分享後,連進來 PC 上安裝標籤機時出現下列該錯誤訊息
完全不知所云的錯誤訊息,反正就是沒有成功連線,把本機上的 Godex Driver 重新安裝至最新版本,其他 PC 就可以正常連線進分享標籤機了

星期五, 12月 01, 2023

[.NET] 使用 BackgroundService 建立 Windows Service

根據官方文章 - Create Windows Service using BackgroundService 的筆記,該內容為使用 BackgroundService 建立 Windows Service,並從 JobService 內隨機取出資料塞進 Windows 事件內

建立背景工作服務 (Worker Service Template) 專案

輸入關鍵字 Worker 就可以找到 [背景工作服務範本]
輸入專案名稱並決定檔案位置
架構為 [.NET 7.0] 和 [不要使用最上層陳述式] 為預設值
nuget 上安裝 Microsoft.Extensions.Hosting.WindowsServices

星期三, 11月 29, 2023

[C#] DataGridView - CellFormatting 事件

使用 DataGridView 時,常常在CellFormatting Event 內使用 DataGridViewCellFormattingEventArgs 來抓取相關資料,如同該篇筆記 -  [C#] 格式化 DataGridView 內資料,但這次踩到坑,原來 DataGrdiViewColumn.Visible = false 情況下,DataGridViewCellFormattingEventArgs 內就抓不到該欄位的 ColumnIndex

該範例很簡易,在 CellFormatting 內把 DataGridViewCellFormattingEventArgs.ColumnIndex 往 TextBox 內塞資料而已,當 Form 一啟動會觸發,Code 如下

namespace CellFormatingVisible
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
        {
            textBox1.Text += e.ColumnIndex.ToString() + Environment.NewLine;
        }
    }
}

實際執行,有五個欄位
把 Column3 隱藏起來,ColumnIndex 2 就沒有抓到

星期一, 10月 30, 2023

[SQL] 限定資料資料表

有一個 Table 記錄著部門內每台機器最後一筆完工資料,該部門 Table 最多也只有四部機器,只有四筆資料的意思,有一個 Query 依賴該 Table 來限定資料,執行計畫只要從該 Table 開始跑就沒有問題啦

但最近改版時產生異常,變成一部機器有多筆資料,導致執行計畫沒有從該 Table 起跑,兩個重點 Table 資料量大約是 150 萬筆和 50 萬筆,通通都被拉出來篩選,CPU Time 超過 1 秒

整理 Table 內資料,執行計畫就恢復正常啦

改善前後比較

改善前改善後
CPU Time1,28216
50 萬 Table Logical Read14,857284
150 萬 Table Logical Read16,484294

星期六, 10月 21, 2023

[SQL] 遺漏索引 - equality、inequality

延續 [SQL] 遺漏索引建議 該篇筆記,xml missing index 內還有其他發現,是關於 equality 和 inequality 條件判斷,在 [SQL] 遺失索引案例 內有紀錄過 <>、> 或 NOT IN 是屬於 inequality,網路上也可以找到詳盡列出屬於 equality、inequality 的操作

但最後我是以 sys.dm_db_missing_index_details 內的 inequality_columns 文字說明為判斷依據
Any comparison operator other than "=" expresses inequality. 
白話翻譯就是任何非等於的操作就是 inequality,但是在該 missing index 內,IN 有出現在不同的建議內,且一個是歸類在 equality,一個是歸類在 inequality 內
WHERE ColumnId5 IN ('資料1', '資料2', '資料3', '資料4')

星期五, 10月 20, 2023

[SQL] 遺漏索引建議

Turning 平行處理時發現前兩個 TSQL 執行次數特別高,特別抓出來處理
打開 xml missing index 發現特別之處,TSQL 語法內的欄位資訊以 missing index 內的 ColumnID 來示意
SELECT
    M.ColumnId5
   ,M.ColumnId11
   ,M.ColumnId12
   ,M.ColumnId27
FROM TableName AS M
	JOIN 
		(
			SELECT
				ColumnId5 ,
				MAX(ColumnId29) AS ColumnId29
			FROM TableName
			WHERE ColumnId5 IN ('資料1', '資料2', '資料3', '資料4')
				AND ColumnId23 > 0
			GROUP BY ColumnId5
		) AS T ON M.ColumnId5 = T.ColumnId5
		AND M.ColumnId29 = T.ColumnId29
WHERE M.ColumnId23 > 0
重點欄位在於 where 條件
WHERE ColumnId5 IN ('資料1', '資料2', '資料3', '資料4')
    AND ColumnId23 > 0
missing index 建議

在舊文章 - Using Missing Index Information to Write CREATE INDEX Statements 內有條列這四點說明
  • List the equality columns first (leftmost in the column list). 
  • List the inequality columns after the equality columns (to the right of equality columns listed).
  • List the include columns in the INCLUDE clause of the CREATE INDEX statement. 
  • To determine an effective order for the equality columns, order them based on their selectivity; that is, list the most selective columns first.
在新文章 - Tune nonclustered indexes with missing index suggestions 內則是改為文字說明
Review the missing index recommendations for a table as a group, along with the definitions of existing indexes on the table. Remember that when defining indexes, generally equality columns should be put before the inequality columns, and together they should form the key of the index. To determine an effective order for the equality columns, order them based on their selectivity: list the most selective columns first (leftmost in the column list). Unique columns are most selective, while columns with many repeating values are less selective.

Included columns should be added to the CREATE INDEX statement using the INCLUDE clause. The order of included columns doesn't affect query performance. Therefore, when combining indexes, included columns may be combined without worrying about order
打開 XML 執行計畫找到最佳建議,基本上有依循上述原則建議,先是建議 equality 欄位後,才是 inequality 欄位
但該 case 的 ColumnId5 在商業邏輯上類似分類資料,ColumnId23 類似未結案資料,ColumnId23 欄位是比較 selective,確認另外兩個建議都是以 ColumnId5 為主,建議完全沒有幫助,最後單純建立 ColumnId23 Index 來消除平行處理
CREATE INDEX IX_TableName ON TableName( ColumnId5 )
這兩個 case 重點 table 剛好都是同一個,建立一支 index 解決兩個 case 的平行處理,該 case 效能前後比較

改善前改善後
Logical Read11,7662,098


星期四, 10月 19, 2023

[SQL] 隱含轉換 - CASE WHEN

Turning 時遇見新的隱含轉換案例,該 case 是兩個 table 透過 left join 後,要在 select 內去進行統計轉換,但該欄位原本是字串欄位,卻轉成整數 (理論上是手誤),導致隱含轉換發生
 
模擬案例
USE tempdb
GO

DROP TABLE IF EXISTS tblMaster
DROP TABLE IF EXISTS tblDetail

CREATE TABLE tblMaster (ColPK CHAR(11))
CREATE TABLE tblDetail (ID INT IDENTITY(1,1) , ColPK char(11))

SELECT
	CASE
		WHEN D.ColPK IS NOT NULL THEN 1 -- 字串欄位轉為整數
		ELSE D.ColPK
	END AS Test1 ,
	IIF(D.ColPK IS NOT NULL , 1 , D.ColPK) AS Test2
FROM tblMaster AS M
	LEFT JOIN tblDetail AS D ON M.ColPK = D.ColPK 

星期五, 10月 13, 2023

[DP] 責任鍊

以資料驗證為例子,使用責任鍊 (Chain of Responsibility,簡稱 cor) 來消除 IfElse 的驗證流程

概念示意圖 (第一次畫)
Legacy Code:IfElse
using System;

namespace ConsoleApp2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string data = "AB-20231013";
            bool isValid = false;

            if (data.Length == 11)
            {
                if (data.Substring(0, 2) == "AB")
                {
                    if (data.Substring(2, 1) == "-") ;
                    {
                        string date = data.Substring(3, 8);

                        isValid = DateTime.TryParseExact(
                            date,
                            "yyyyMMdd",
                            System.Globalization.CultureInfo.InvariantCulture,
                            System.Globalization.DateTimeStyles.None,
                            out DateTime result);
                    }
                }
            }

            Console.WriteLine($"驗證結果為 {isValid}");
        }
    }
}
Designe Pattern:責任鍊來消除 IfElse
using System;

namespace ConsoleApp2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string data = "AB-20231013";

            AbstractHandler chain = new 總長度Handler(
                new 前兩碼Hanlde(
                new 符號Handler(
                new 日期Handle(null))));

            bool isValid = chain.HandleRequest(data);
            Console.WriteLine($"驗證結果為 {isValid}");
        }
    }

    public class 日期Handle : AbstractHandler
    {
        public 日期Handle(AbstractHandler successor) : base(successor)
        {
        }

        protected override bool Handle(string request)
        {
            string date = request.Substring(3, 8);

            return DateTime.TryParseExact(
                date,
                "yyyyMMdd",
                System.Globalization.CultureInfo.InvariantCulture,
                System.Globalization.DateTimeStyles.None,
                out DateTime result);
        }
    }

    public class 符號Handler : AbstractHandler
    {
        public 符號Handler(AbstractHandler successor) : base(successor)
        {
        }

        protected override bool Handle(string request)
        {
            return (request.Substring(2, 1) == "-");
        }
    }

    public class 前兩碼Hanlde : AbstractHandler
    {
        public 前兩碼Hanlde(AbstractHandler successor) : base(successor)
        {
        }

        protected override bool Handle(string request)
        {
            return (request.Substring(0, 2) == "AB");
        }
    }

    public class 總長度Handler : AbstractHandler
    {
        public 總長度Handler(AbstractHandler successor) : base(successor)
        {
        }

        protected override bool Handle(string request)
        {
            return (request.Trim().Length == 11);
        }
    }

    public abstract class AbstractHandler
    {
        private AbstractHandler _successor;

        public AbstractHandler(AbstractHandler successor)
        {
            _successor = successor;
        }

        protected abstract bool Handle(string request);

        public bool HandleRequest(string request)
        {
            if (Handle(request) == false)
                return false;

            if (_successor == null)
                return true;

            return _successor.HandleRequest(request);
        }
    }
}

星期二, 10月 10, 2023

[SSMS] 指令碼精靈

[SSMS] 產生 Script 以 Alter 為目標,紀錄單一物件和多物件產生 Script 方式,該篇會以指令碼精靈為主,指令碼精靈除了產生物件 Script 之外,還可以把資料一併轉出

DB => 工作 => 產生指令碼



這邊可以選擇 [編寫整個資料庫和所有資料庫物件的指令碼] 或 [選擇特定的資料庫物件],該筆記就單選轉出 Person.Person Table 而已


把物件匯出成 Script 並點選右上角的 [進階] 選項 


進階選項內的 [要編寫指令碼的資料類型] 有三種選項,分別為
  • 結構描述和資料
  • 僅限結構描述
  • 僅限資料
該筆記以 [結構描述和資料] 為主,要同時匯出 Table Scheam 和資料






轉出的 Script 語法,資料部分只記錄 5 筆資料
USE [AdventureWorks2022]
GO
/****** Object:  Table [Person].[Person]    Script Date: 2023/10/10 下午 10:32:11 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [Person].[Person](
	[BusinessEntityID] [int] NOT NULL,
	[PersonType] [nchar](2) NOT NULL,
	[NameStyle] [dbo].[NameStyle] NOT NULL,
	[Title] [nvarchar](8) NULL,
	[FirstName] [dbo].[Name] NOT NULL,
	[MiddleName] [dbo].[Name] NULL,
	[LastName] [dbo].[Name] NOT NULL,
	[Suffix] [nvarchar](10) NULL,
	[EmailPromotion] [int] NOT NULL,
	[AdditionalContactInfo] [xml](CONTENT [Person].[AdditionalContactInfoSchemaCollection]) NULL,
	[Demographics] [xml](CONTENT [Person].[IndividualSurveySchemaCollection]) NULL,
	[rowguid] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
	[ModifiedDate] [datetime] NOT NULL,
 CONSTRAINT [PK_Person_BusinessEntityID] PRIMARY KEY CLUSTERED 
(
	[BusinessEntityID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
INSERT [Person].[Person] ([BusinessEntityID], [PersonType], [NameStyle], [Title], [FirstName], [MiddleName], [LastName], [Suffix], [EmailPromotion], [AdditionalContactInfo], [Demographics], [rowguid], [ModifiedDate]) VALUES (1, N'EM', 0, NULL, N'Ken', N'J', N'Sánchez', NULL, 0, NULL, N'<IndividualSurvey xmlns="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/IndividualSurvey"><TotalPurchaseYTD>0</TotalPurchaseYTD></IndividualSurvey>', N'92c4279f-1207-48a3-8448-4636514eb7e2', CAST(N'2009-01-07T00:00:00.000' AS DateTime))
INSERT [Person].[Person] ([BusinessEntityID], [PersonType], [NameStyle], [Title], [FirstName], [MiddleName], [LastName], [Suffix], [EmailPromotion], [AdditionalContactInfo], [Demographics], [rowguid], [ModifiedDate]) VALUES (2, N'EM', 0, NULL, N'Terri', N'Lee', N'Duffy', NULL, 1, NULL, N'<IndividualSurvey xmlns="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/IndividualSurvey"><TotalPurchaseYTD>0</TotalPurchaseYTD></IndividualSurvey>', N'd8763459-8aa8-47cc-aff7-c9079af79033', CAST(N'2008-01-24T00:00:00.000' AS DateTime))
INSERT [Person].[Person] ([BusinessEntityID], [PersonType], [NameStyle], [Title], [FirstName], [MiddleName], [LastName], [Suffix], [EmailPromotion], [AdditionalContactInfo], [Demographics], [rowguid], [ModifiedDate]) VALUES (3, N'EM', 0, NULL, N'Roberto', NULL, N'Tamburello', NULL, 0, NULL, N'<IndividualSurvey xmlns="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/IndividualSurvey"><TotalPurchaseYTD>0</TotalPurchaseYTD></IndividualSurvey>', N'e1a2555e-0828-434b-a33b-6f38136a37de', CAST(N'2007-11-04T00:00:00.000' AS DateTime))
INSERT [Person].[Person] ([BusinessEntityID], [PersonType], [NameStyle], [Title], [FirstName], [MiddleName], [LastName], [Suffix], [EmailPromotion], [AdditionalContactInfo], [Demographics], [rowguid], [ModifiedDate]) VALUES (4, N'EM', 0, NULL, N'Rob', NULL, N'Walters', NULL, 0, NULL, N'<IndividualSurvey xmlns="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/IndividualSurvey"><TotalPurchaseYTD>0</TotalPurchaseYTD></IndividualSurvey>', N'f2d7ce06-38b3-4357-805b-f4b6b71c01ff', CAST(N'2007-11-28T00:00:00.000' AS DateTime))
INSERT [Person].[Person] ([BusinessEntityID], [PersonType], [NameStyle], [Title], [FirstName], [MiddleName], [LastName], [Suffix], [EmailPromotion], [AdditionalContactInfo], [Demographics], [rowguid], [ModifiedDate]) VALUES (5, N'EM', 0, N'Ms.', N'Gail', N'A', N'Erickson', NULL, 0, NULL, N'<IndividualSurvey xmlns="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/IndividualSurvey"><TotalPurchaseYTD>0</TotalPurchaseYTD></IndividualSurvey>', N'f3a3f6b4-ae3b-430c-a754-9f2231ba6fef', CAST(N'2007-12-30T00:00:00.000' AS DateTime))
指令碼精雖然方便把資料轉出成 Script 來使用,但不建議使用就是,之前有過一萬筆左右 Table,在 Local 端透過指令碼精靈轉出,在離峰時間拿到 Product 環境上去進行 insert,花了快 10 分鐘才完成,有點出乎意外之外的慢,之後就乖乖的使用 bcp out 把資料倒出,bcp in 或 bulk insert 匯入資料比較快速

星期六, 9月 30, 2023

[SSMS] 產生變更指令碼

對於不熟悉 ddl 語法的人來說,要使用 ddl 語法來新增或變更 table 會花上不少時間尋找語法資料,在 SSMS 上有提供功能,可以取得變更的 ddl Script 語法,可以直接拿來應用或快速取的關鍵字

在 AdventureWorks2022 Person.Person Table 上新增 NewColumn 


新增 NewColumn 後,先不要進行儲存,會有下圖 - [產生變更指令碼] 功能出現


點選後就會出現這段 Script 可以複製出來使用
/* 為了避免任何可能發生資料遺失的問題,您應該先詳細檢視此指令碼,然後才能在資料庫設計工具環境以外的位置執行。*/
BEGIN TRANSACTION
SET QUOTED_IDENTIFIER ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
SET CONCAT_NULL_YIELDS_NULL ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE Person.Person ADD
	NewColumn nchar(10) NULL
GO
ALTER TABLE Person.Person SET (LOCK_ESCALATION = TABLE)
GO
COMMIT

星期一, 9月 18, 2023

[C#] InitializeComponent()

以往看見 InitializeComponent() 在 Form.cs 內而不是在 designer.cs 的 Code,都是些較早期的網路範例 Code,這次在公司內部發現 WinForm Project 發現,紀錄該情況

標準 Form

標準 Form 應該會附帶一個 designer.cs 檔案,建構子內會有 InitializeComponent(),是指向 designer.cs 檔案


designer.cs 檔案內會有 Form 控件相關設定值
namespace WindowsFormsApp1
{
    partial class Form1
    {
        /// <summary>
        /// 設計工具所需的變數。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 清除任何使用中的資源。
        /// </summary>
        /// <param name="disposing">如果應該處置受控資源則為 true,否則為 false。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form 設計工具產生的程式碼

        /// <summary>
        /// 此為設計工具支援所需的方法 - 請勿使用程式碼編輯器修改
        /// 這個方法的內容。
        /// </summary>
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(800, 450);
            this.Text = "Form1";
        }

        #endregion
    }
}
非標準的 Form

如下圖,Form 內沒有 designer.cs 檔案,InitializeComponent() 則是都在 Form.cs 檔案內,說實話也不知道要如何稱呼該 Form


重現該 Form

反覆建立 Form 後發現,只要新增一個類別檔案,再去繼承 System.Windows.Forms.Form 後,等同於把 class 轉成 Form 後就會出現該情況,但該情況也可以正常運行,公司內的 Code 已經存在很久,還是 FormBase 設計,每個 Form  都要去繼承它,沒有因為該情況而產生異常過

不肯定是否還有其他方式也會發生,只試出該情況,VS 環境為 VS2022

星期一, 9月 11, 2023

[SQL] 查詢 SQL Server 版本

根據官方文章 - 判斷 SQL Server Database Engine 所執行的版本和版次 並整合舊文章,重新整理該筆記

SSMS 連線資訊


錯誤記錄檔案 (ErrorLog)


@@version
SELECT @@VERSION
Microsoft SQL Server 2022 (RTM) - 16.0.1000.6 (X64)   
Oct  8 2022 05:58:25   
Copyright (C) 2022 Microsoft Corporation  Developer Edition (64-bit) 
on Windows 10 Pro 10.0 <X64> (Build 19045: ) (Hypervisor) 
SERVERPROPERTY 和相關語法
SELECT
  @@SERVERNAME AS [SERVER_NAME] ,
  @@SERVICENAME AS [SERVICE_NAME] ,
  RIGHT(LEFT(@@VERSION,25),4) AS 'SQL Server' ,
  SERVERPROPERTY('ProductVersion') AS ProductVersion,
  SERVERPROPERTY('ProductLevel') AS ProductLevel,
  SERVERPROPERTY('Edition') AS Edition
SERVERPROPERTY() 的 ProductLevel 屬性回傳值
  1. RTM = 原始發行版本
  2. SPn = Service Pack 版本
  3. CTP = Community Technology Preview 版本
SQL Server 2017 開始就沒有 SP,只有 Cumulative Update (CU) 而已,詳見 No More Service Packs for SQL Server

已安裝的 SQL Server 功能探索報告