きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

Effective C# 6.0/7.0 × 5章:× 自分なりの要点まとめ

目的

流し読みにならないように、1項目毎要約をする。
今回は、最後の5章:例外処理まとめ。

項目45:契約違反を例外として報告すること

例外がスローされた場合でも堅牢なコードを作成するスキルは、すべてのC#開発者にとって重要です。

  • 例外を発生させる可能性があるPublicメソッドを開発する場合、その処理の事前チェック用のメソッドも提供するようにするとよい。
  • 例外処理をフロー制御に利用してはいけない。・・・そういえば、以前、例外処理をGOTO文のようにして実装されたシステムを見たことがあった・・・例外処理は通常のメソッド呼び出しよりも多くの時間がかかるため、パフォーマンスの観点からも、可読性の観点からもアンチパターンであるということだと思う。

項目47:アプリケーション固有の例外クラスを作成する

- すべての例外処理をExceptionクラスを利用して行うことに固執せず、独自の例外クラスを作成し、適切なタイミングで、適切な例外処理を行えるようにするのがよい。 - むやみやたらに独自例外クラスを作成するのではなく、発生した例外を無視したとき、長期間にわたって例外が発生することになる場面(データの整合性がとれなくなったなど)かどうかを一つの指標として作るようにするのが良い。

項目49:catchからの再スローよりも例外フィルタを使用すること

項目50:例外フィルタの副作用を活用する

  • 例外フィルタを使用すると、ランタイムが適切なcatch句を見つける前にれ外フィルタを実行することができる。そのため、発生した例外を全て確認したい場合は、常にfalseを返すような例外フィルタを実装すると良い。
  • 以下のようにすることで、既存のCatch句に影響を及ぼさずにログ出力機能を追加することができる。
Func<Exception, bool> errorhandling = (exception) =>
 {
     var oldColor = Console.ForegroundColor;
     Console.ForegroundColor = ConsoleColor.Red;
     Console.WriteLine(exception.ToString());
     Console.ForegroundColor = oldColor;
     return false;
 };
try
{
    throw new ArgumentException();
}
catch (SystemException ex) when (errorhandling(ex))
{
    //ここは実行されない
    Console.WriteLine(ex.ToString());
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.ToString());
}
catch (Exception ex)
{
    Console.WriteLine(ex.ToString());
}

所感

例外処理については、わかった気になっていたが、例外フィルタのように、初めて知ったテクニックもあった。今一度例外処理の考え方を見直す必要がありそう。

PC操作 × キャプチャー × Excel保存

概要

システムのテストをする時、一つ一つの操作をキャプチャ(スクリーンショット)し、それらをExcelに貼り付けエビデンスとすることが多々ある。
また、環境構築や、システムの操作手順書を作成する場面でも、一つ一つの操作をキャプチャし、Excelに貼り付けるという作業が発生する。
ただただ面倒くさい。
こういった作業を少しでも効率よくするためのツールを作ってみた。

環境

Windows環境のみ。 .NET Framework 4.8が必要。

Download

github.com

使い方

インストール

ZIP展開→Setupを実行。

画面説明

f:id:kinakomotitti:20210526223337p:plain
Main page
①:キャプチャーを開始するためのボタン。これをクリックすると、操作の記録を開始する。キャプチャーされたファイルは、JPEGファイルとして後述の設定画面で指定されたフォルダに保存される。
②:キャプチャーされたすべてのファイルを操作順に張り付けられたExcelファイルを作成するためのボタン。
③:このアプリケーションを終了するためのボタン。
④:設定画面を開くためのボタン。
設定画面では、 以下の項目を設定することができる。
A)キャプチャーされたファイルの出力先フォルダパス
B)出力されるExcelのファイル名
C)スクリーンショットをとるとき、ActiveなWindowだけをキャプチャーするか、画面全体をキャプチャするか

出力仕様

指定されたフォルダパス
│  output.xlsx ←②ボタン処理で作成されたExcelはここに作られる。
│
└─Screenshot←キャプチャされたスクリーンショットはここに保存される。
        20210523_091138.4597508.jpeg
        20210523_091141.4437308.jpeg

キャプチャー処理に関する仕様

マウス操作は、左右クリック、ホイールクリックに対応。クリックする度にスクリーンショットを実行する。
キーダウン操作は、Enterにのみ対応。Enterを押下したタイミングでスクリーンショットを実行する。

今後

リファクタリングをした後、スクリーンショットJpegファイルを使ってGIFファイルを生成する機能を追加したい。

忘備録

これまでやってきたことの経緯のメモ。同じことをやらないようにするため。
Start:キャプチャツールを作ろう。

Consoleアプリでは常時起動できないから、Windows Service or WPF or UWP or WIn Formにしよう。

Windows Storeで公開したいから、UWPにしよう。
試してみたら、ツールがアクティブな時しかキャプチャができないことに気が付いた。
これではだめだ。Win Form or WPFにしよう。

せっかくなので、使ったことのないWPFにしよう。

.NET CoreでWPFがサポートされていたような気がする。それでやってみよう。

ClosedXml(ツールで利用しているライブラリ)が.NET coreサポートしてなかった・・・元に戻そう。

Ver.1 リリース。

Effective C# 6.0/7.0 × 4章:LINQを扱う処理 × 自分なりの要点まとめ

目的

流し読みにならないように、1項目毎要約をする。
今回は、4章:LINQを扱う処理の項目に対してのまとめとなる。

項目29:コレクションを返すメソッドではなくイテレータを返すメソッドとすること

イテレータメソッド とは、 yield return を 使用 し て 必要 に 応じ て シーケンス の 要素 を 生成 する よう な メソッド の こと です。BillWagner. Effective C# 6.0/7.0 (p.121). 株式会社翔泳社. Kindle 版.

  • ある配列(List<string>など)を作成して処理を行う場面があった場合、作成した配列を返すことがある。
public static IEnumerable<char> GenerateAlphabetCache()
{
    var collection = new List<char>();
    var letter = 'a';
    while (letter <= 'z')
    {
        collection.Add(letter);
        letter++;
    }
    return collection;
}
  • イテレータを利用することで、配列を返すのではなく、配列の要素が呼び出されたときに1つ1つの要素を呼び出し元に返すようにすることができる。
public static IEnumerable<char> GenerateAlphabet()
{
    var letter = 'a';
    while (letter <= 'z')
    {
        yield return letter;
        letter++;
    }
}
  • これにより、呼び出し元で、生成した配列を丸ごとキャッシュして処理を進めるか、必要になった要素を取り出し、それ毎に処理を進めるかという選択をさせることができる。また、後者の場合、呼び出し元で配列を管理する手間を省くことができる。
  • 処理を進めるうえで、必要な要素だけを使って処理を進めることができる場合は、必要な分だけ要素を生成するということができる場面では、イテレータを利用したほうが良い。

項目30:ループよりもクエリ構文を使用すること

  • メモ:誤植?クエリよりもパフォーマンスの出るループを手書きすることもできるけど、常にそうなるとは限らない。といいたいのでは?

ループ よりも クエリ の 方 が パフォーマンス が 低い という 意見 や データ も あり ます。 クエリ よりも パフォーマンス の 出 ない ループ を 手書き する こと も 確か に でき ます が、 常に そう だ という わけ では あり ませ ん。BillWagner. Effective C# 6.0/7.0 (p.130). 株式会社翔泳社. Kindle 版.

  • 上記の通り、パフォーマンスに関する課題は完全に解決されていないのかもしれない。しかし、可読性の観点からすると、ForやWhileなどの命令形式のループを使った処理より、はるかに可読性の高いコードが実装できる。
  • クエリ構文にするか、メソッドチェーンにするか迷う場面も出てくる。メソッドチェーンだけでサポートされている拡張メソッドもあるため、メソッドチェーンに行きがちだが、クエリ構文のほうが可読性が高い場面もある。可読性の観点から、どちらを使うべきか判断するようにするのがよさそう。
  • 明瞭な違いがないシンプルなサンプル。ここに特定の条件だけ値を返す処理や、ソート順序を変えたりするなどの追加要件が発生したとたん、可読性の観点で明瞭な違いが出てくる。
public static IEnumerable<Tuple<int,int>> ProduceIndices()
{
    return from x in Enumerable.Range(0, 10)
           from y in Enumerable.Range(0, 10)
           select Tuple.Create(x, y);
}
public static IEnumerable<Tuple<int, int>> ProduceIndicesOld()
{
    foreach (var x in Enumerable.Range(0, 10))
    {
        foreach (var y in Enumerable.Range(0, 10))
        {
            yield return Tuple.Create(x,y);
        }
    }
}

項目31:シーケンス用の組み合わせ可能なAPIを作成する

  • ポイントは、イテレータメソッドの利点を理解すること。

単一 の 項目 を 処理 する よりも、 シーケンス を 処理 する プログラム の 方 が 多い はず で、 foreach や for、 while といった キーワード を 使用 し て 処理 する こと に なるでしょう。 つまり コレクション を 入力 として 受け取り、 項目 を 確認 または 変更 し、 別 の コレクション を 返す よう な メソッド を 作成 する こと に なる でしょ う。 BillWagner. Effective C# 6.0/7.0 (p.130). 株式会社翔泳社. Kindle 版. - なぜばれた( ^ω^)・・・

  • 一つの配列を受け取り、新しい配列を呼び出し元に返す。イテレータを使えば、一つ一つの要素毎に処理を行うことができる。そのため、従来の配列全部を処理するループで実装するより、効率的に処理を実行することができる。また、他のイテレータメソッドと組み合わせることで、可読性を損なうことなく複雑な処理を、効率的に実行できるようになる。

  • イテレータを効果的に利用するためにも、データの種類ごとにメソッドを定義する代わりに、任意の配列に対して処理を行えるようなメソッドを定義するという考え方を持つ。

項目37:クエリを即時評価ではなく遅延評価すること

  • クエリを実行したとき、つど新しい要素を取得する挙動を遅延評価(lazy evakuation)といい、結果を一括で取得する挙動を即時評価(eager evaluetion)という。
  • 遅延評価をすることで、一度定義したクエリに対して、追加のクエリを追加することができる。同じことを即時評価すると、一度定義したクエリからすべての結果を受け取ったのち、追加のクエリを実行することになるため、配列の保存に利用されるメモリやループ処理を実行する処理コストの面からパフォーマンスの低下を招く。
    • クエリ式で定義すれば、期待通りの遅延評価になるかというと、そうではない。その例は↓。以下のようにすべてのintの値を出力するメソッドがあった場合、CaseAでは、10回だけアイテムを取得するように動くのに対し、CaseBでは、配列をすべて生成し、すべての項目に対してwhere句で指定した条件を検査する。そのため、int.MaxValue-11回のループ処理が実行されることになる。
public static IEnumerable<int> AllNumbers()
{
    var number = 0;
    while (number <= int.MaxValue)
    {
        yield return number;
        number++;
    }
}

//呼び出し元。CaseA
 var test = Sample.AllNumbers();
 foreach (var item in test.Take(10))
{
    Console.WriteLine(item);
}

//呼び出し元。CaseB
var test2 = from item in Sample.AllNumbers()
            where item <= 10
            select item;
foreach (var item in test2)
{
    Console.WriteLine(item);
}

一方、 一部 の クエリ 式 では 答え を 返す 前 に シーケンス 全体 を 必要 と する 場合 が あり ます。 この ボトルネック が 発生 する タイミング を 把握 する こと によって、 パフォーマンス の 劣化 を 起こさ ない よう な クエリ を 作成 できる よう になり ます。BillWagner. Effective C# 6.0/7.0 (p.163). 株式会社翔泳社. Kindle 版.

  • 同じように、配列の要素すべてが必要になるケースとして、OrderBy、Max、Minがある。
  • まずは、遅延実行で処理が定義できないか考え、どうしても難しい場合は、即時評価(ToList, ToArray)を利用するという思考でいるのが推奨されている。

項目43:クエリに期待する意味をSingle()やFirst()を使用して表現すること

  • 特定の1つの要素を配列から取得しようとする場合、Signleメソッドを利用して取得するようにすると、そのクエリの実装に明確な意図を表現することができる。
  • 複数の要素を取得し、その中のどれでもいいから1つの要素を取得したいという考えで、Firstメソッドを利用すると、そのコードの明確な意図が表現しきれない。

所感

項目44、41あたりのクロージャが絡んでくるところが十分に理解できていない。これまで使ってこなかった機能なので、これを起点に深掘りしていきたい。

Effective C# 6.0/7.0 × 3章:ジェネリックによる処理 × 自分なりの要点まとめ

目的

流し読みにならないように、1項目毎要約をする。
今回は、3章:ジェネリックによる処理の項目に対してのまとめ。 今回も特に心に残った項目をピックアップした。

項目20:IConparableIComparerにより順序関係を実装する

  • IConparableインターフェースは後方互換の為に実装する必要があるが、実装する際、インターフェースを明示的に指定する形で実装することで、意図せず呼び出されることを防ぐことができる。
  • IConparable.CompareToメソッドは、Object型の引数を受け付ける。そのため、引数に値型の変数が指定された場合Boxing、Unboxingの処理が発生するため、IComparable<T>のCompareToメソッドの処理に比べてパフォーマンスの低下が懸念される。
  • IComparerの実装と呼び出し方
public struct Customer : IComparable<Customer>, IComparable
{
    public string Name { get; }
    public double Revenue { get; }
    public Customer(string name, double revenue)
    {
        Name = name;
        Revenue = revenue;
    }
    public int CompareTo([AllowNull] Customer other)
    {
        return this.Name.CompareTo(other.Name);
    }
    int IComparable.CompareTo(object obj)
    {
        if ((obj is Customer) == false)
        {
            throw new ArgumentException();
        }
        return this.CompareTo((Customer)obj);
    }
    public static Comparison<Customer> CompareByRevenue => (left, right) => left.Revenue.CompareTo(right.Revenue);
    private class RevenueComparer : IComparer<Customer>
    { 
        int IComparer<Customer>.Compare(Customer left, Customer right) => left.Revenue.CompareTo(right.Revenue);
    }
}
static void Main(string[] args)
{
    var a = new Customer("BBB",10);
    var b = new Customer("AAA",9);
    var c = new Customer("DDD",1);
    var d = new Customer("CCC",7);
    List<Customer> customers = new List<Customer>() { a, b, c,d};

    //IComparable<T>
    customers.Sort();
    foreach (var item in customers)
    {
        Console.WriteLine(item.Name);
    }

    //IComparer<T>
    customers.Sort(Customer.CompareByRevenue);
    foreach (var item in customers)
    {
        Console.WriteLine(item.Name);
    }
}    

IComparer を 使用 する と、 一般的 な 順序 とは 異なる 順序 を 定義 し たり、 外部 から 提供 さ れ た 型 に対する 順序 を 定義 し たり する こと が でき ます。BillWagner. Effective C# 6.0/7.0 (p.89). 株式会社翔泳社. Kindle 版.

項目22:ジェネリックの共変性と反変性をサポートする

  • 基底クラスを戻り値とするデリゲートに対して、 派生クラスを戻り値とするメソッドを代入できることを covariance といいます。
  • 派生クラスを引数とするデリゲートに対して、 基底クラスを引数とするデリゲートを代入できることを contravariance といいます
  • Memo:誤植?out修飾子がついているので、共変性では?C#リファレンスが間違っているのか・・・?

    Func < out T > は 反 変性 を 持つ もの として 宣言 さ れ て い ます が、 用途 から 見れ ば IContravariantDelegate インター フェイス を 実装 する オブジェクト を 入力 に 取る という こと を 表し て い ます。 しかし IContravariantDelegate インター フェイス の 観点 では 反 変性 で 正しい の です。 BillWagner. Effective C# 6.0/7.0 (p.97). 株式会社翔泳社. Kindle 版.

項目23:型パラメータにおけるメソッドの制約をデリゲートとして定義する

ジェネリック型では、型引数に具体的な型が指定されているかどうかで型情報が変わります。 たとえばListクラスを例にとった場合、型引数Tの型が具体的に定まっていないListを特にオープンジェネリック型(あるいはジェネリック型定義)、対してListやListなど型引数Tの型が具体的な型が定まっているものをクローズジェネリック型(あるいは構築ジェネリック型、構築された型)と呼びます。 - ジェネリッククラス<T>で、TクラスにAddメソッドが必要な処理を定義する場合、ジェネリッククラスを提供する側は、IAddインタフェースを制約としたジェネリッククラスを作成する。一方、利用する側は、IAddインターフェースを実装したクラスを用意する必要がある。そして、提供されたじぇねりくクラスをクローズジェネリック型として利用することが求められる。 - 提供側、利用側で↑の実装をより柔軟にんするため、ジェネリッククラスで必要だったAddメソッドをデリゲートとして定義し、利用側が呼び出すときに指定できるようにする。

//提供側(IAddインターフェースを定義しなくてOK、制約をかけなくてOK)
public static T Add<T>(T left, T right, Func<T, T, T> AddFunc)
{
    return AddFunc(left, right);
}
//同じこと
//public static T Add<T>(T left, T right, Func<T, T, T> AddFunc) =>
//    AddFunc(left, right);

//呼び出し側(IAddの実装をしなくてOK、クローズジェネリッククラスを用意しなくてOK)
Example.Add(1, 1, (x, y) => x + y);

ほとんど の 場合、 ジェネリッククラス で 呼び出す 必要 の ある メソッド は 特定 の デリゲート として 置き換える こと が でき ます。 BillWagner. Effective C# 6.0/7.0 (p.100). 株式会社翔泳社. Kindle 版.

定義してあっても、使わなければいいかと無視していたが、制約やデリゲートを利用することで、使わせないようにすることや、その制約にかかる実装コストを減らすことができる・・・ということなのか・・・

項目25:型引数がインスタンスのフィールドでない場合には、ジェネリックメソッドとして定義すること

  • 項目23の例を使うと、以下のようになる。
//ジェネリッククラスとして定義(型引数がインスタンスのフィールドではない)
public static class Example<T>
{
    public static T Add(T left, T right, Func<T, T, T> AddFunc)
    {
        return AddFunc(left, right);
    }
}

Example<int>.Add(1, 1, (x, y) => x + y);
Example<double>.Add(1, 1, (x, y) => x + y);
//非ジェネリッククラスのジェネリックメソッドとして定義
public static class Example
{
    public static T Add<T>(T left, T right, Func<T, T, T> AddFunc)
    {
        return AddFunc(left, right);
    }
    //こうして最適化もできる
    public static int Add(int left, int right, Func<int,int,int> AddFunc)
    {
        return AddFunc(left, right);
    }
}

//呼び出し側が、Exampleクラスの型引数を意識しなくてよくなった。振り分けは、Exampleクラスが責任をもって行う。
Example.Add(1, 1, (x, y) => x + y);  //Integer
Example.Add(1.1, 1.1, (x, y) => x + y); //Double

所感

機能 は する かも しれ ませ ん が、 実行 時 の 型 が IComparer < T > を 実装 し て いる かを 実行 時 に 確認 し、 適切 な メソッド を 呼び出す という 余計 な 手間 が かかり ます。BillWagner. Effective C# 6.0/7.0 (p.107). 株式会社翔泳社. Kindle 版.

紹介されていたtipsも勉強になったが、これまでの開発において、いかに↑の意識がうすかったかという点がよくわかったし、勉強になった。動かないとそもそもダメだが、動けばいいということでもない。

Effective C# 6.0/7.0 × 1章:C#言語イディオム&2章:リソース管理 × 自分なりの要点まとめ

目的

Effective C# 6.0/7.0を読んだ。 流し読みにならないように、1項目毎に要約してみた。
まとめてみた中で、心に残ったところを抜粋。 今回は1章:C#言語イディオム2章:リソース管理の項目を対象とする。

項目3:キャストにはisまたはasを使用すること

  • 安全にキャストをするための方法。1)as 演算子を利用する。2)is演算子で変換可能性を確認した後、asまたは、キャストを利用して変換を行う。
  • as はNull許容型もしくは参照型のインスタンスにしか利用できない。また、指定された型から派生した型への変換できない。変換に失敗したときは、nullを返す。
  • キャストは、指定の型への変換演算子を利用できる。参考)Implicit演算子
  • サンプルコードで出てきたImplicit演算子についてはこちら

項目5:カルチャ固有の文字列よりもFormattableStringを使用すること

  • 補完文字列は、暗黙的な型変換では、String型の文字列インスタンスとして生成されるが、FormattableStringを明示的に指定することも可能。
Func<FormattableString, string> ToGerman = (source) =>
 {
     return string.Format(System.Globalization.CultureInfo.CreateSpecificCulture("de-de"),
         source.Format,
         source.GetArguments());
 };
var test = $"Test{DateTime.Now}";
FormattableString test2 = $"Test{DateTime.Now}";
//Console.WriteLine(ToGerman(test)); //コンパイルエラー
Console.WriteLine(test2);
Console.WriteLine(ToGerman(test2));
Console.WriteLine(ToGerman($"Test{DateTime.Now}"));

項目7:デリゲートを使用してコールバックを表現する

  • シンプルな例
var numbers = Enumerable.Range(1, 200).ToList();
var oddNumbers = numbers.Find(n => n % 2 == 1);
  • 有用な場面1:イベントと組み合わせて使用。
  • 有用な場面2:特定のクラス間でデータをやり取りする必要があるが、互いのインターフェースを利用するほどの連携をさせたくない場合。

項目8:イベントの呼び出し時にNull条件演算子を使用すること

public class EventSource
{
    private int counter;
    private EventHandler<int> Updated;
    Public void RaiseUpdates()
    {
        counter++;
        if (updated != null)
            Updated(this,counter);
    }
}
  • Null条件演算子を利用することで、↑の従来の書き方を↓のようにすることができる。コンパイラがすべてのデリゲートもしくは、イベントの定義に対してタイプセーフなInvokeメソッドを生成する。上下で同じ挙動をするため、下のコードもスレッドセーフで実行できる。(Updatedを実行しようとしたタイミングで、別スレッドの処理でデリゲートがイベントハンドラから削除された場合でも、元のスレッドではデリゲートへの参照をなくさずに実行することができる。)
public class EventSource
{
    private int counter;
    private EventHandler<int> Updated;
    Public void RaiseUpdates()
    {
        counter++;
        Updated?.Invoke(this,counter);
    }
}   

項目12:メンバには割り当て演算子よりもオブジェクト初期化子を使用すること

  • オブジェクト初期化子は、コンストラクタの初期化処理より前に実行される。オブジェクト初期化子でも初期化し、コンストラクタでも初期化すると、無駄にインスタンスを作成することとなる。どちらかに統一するため、初期化子を利用するようにしたほうが良い。
  • 例外もある。オブジェクトのメンバ変数の中に、初期化のタイミングで例外を発生させる可能性のある者がいれば、コンストラクタ内で初期化するようにしたほうが良い。例外処理ができるため。
  • そのほかに、コンストラクタの引数に応じて初期化条件が変わる場合もコンストラクタで初期化したほうが良い。
  • オブジェクト初期化子にまとめることで、後々追加されるコンストラクタで、初期化処理を忘れるメンバー変数が出てくることを防ぐこともできる。

項目13:Staticメンバを最適に初期化すること

  • 項目12の初期化のタイミングで例外を発生させる可能性がある場合と同じ理由。Staticメンバが初期化に失敗すると、アプリケーションが終了していまう。 - Staticコンストラクタの例(シングルトンパターン)
public class SampleItem
{
    //単純な領域確保だけであれば、初期菓子を使う。
    private static readonly SampleItem theoneAndOnly = new SampleItem();
    public static SampleItem MyProperty 
    {
        get { return SampleItem.theoneAndOnly; }
    }
    private SampleItem() { }
}
  • staticの場合も、非Staticのと同じで、メンバ初期化子が実行された後に、コンストラクタが実行される。どちらも同時に初期化する必要はない。
public class SampleItem
{
    //複雑な初期化処理が必要な場合には、初期化子より処理は後に実行されるが、Staticコンストラクタを利用するようにする。
    public string Name { get; set; }
    private static readonly SampleItem theoneAndOnly;
    public static SampleItem MyProperty
    {
        get { return SampleItem.theoneAndOnly; }
    }
    static SampleItem()
    {
        try
        {
            theoneAndOnly = new SampleItem();
        }
        catch (Exception)
        {
            throw;
        }
    }
    private SampleItem() { }
}

項目16:コンストラクタ内では仮想メソッドを呼ばない

  • あるBaseクラスを継承した派生クラスがある場合、派生クラスのフィールド変数の初期化→Baseクラスのコンストラクタの実行→派生クラスのコンストラクタの実行の流れで処理が進むことになる。
  • この時、Baseクラスのコンストラクタ内でBaseクラスで定義されたVirtualメソッドを呼び出す処理がある場合、派生クラスで実装されたVirtualメソッドが、派生クラスのコンストラクタ実行前に実行されてしまうケースがある。
  • 期待していない順序での処理の実行を防ぐためにも、コンストラクタ内でのVirtualメソッドの呼び出しは避けるべきである。

EF Core 3.1 × Postgres × 実際に出力されるSQLの検証を続けてみた

この記事の目的

この記事は、
EF Coreが出力する実際のSQLの検証を行うこと
を目的としています。
今回は、Selectに焦点を絞って、いろいろな条件でクエリを投げた結果をまとめていきます。
前回は、クエリ式、メソッド式どちらもやりましたが、今回からしばらくは、クエリ式中心でまとめていきます。
実行環境などは、前回のブログと同じものを利用しています。

変更点

AccountテーブルにNote列を追加しました。
f:id:kinakomotitti:20200607215550p:plain

関連ブログ

www.kinakomotitti.net


本題

WHERE句 Null検索

NotNull制約が付いたEmail列と、制約が付いていないNote列に対して、以下のようなクエリを投げてみます。

(from account in context.Account
 where account.Email != null &&
            account.Email != string.Empty &&
            account.Note != null &&
            account.Note != string.Empty
 select account).ToList();


結果・・・

SELECT a.user_id, a.created_on, a.email, a.last_login, a.note, a.password, a.sex_id, a.username
FROM account AS a
WHERE((a.email <> '') AND
              (a.note IS NOT NULL)) AND(a.note <> '')
  • 1つの区切りごとに括弧がつけられるので、どこまでが一つの条件かわかりやすくなっています。条件が3つ(A、B、C)ある場合、 (A and B) and C のような形式で付与されているようでしす。条件が4つ(A、B、C、D)ある場合、 ((A and B) and C ) and D になるイメージですね。こうしている理由は何だろう・・・こうしたほうがパフォーマンスがあがるのかなぁ。
  • Email、Noteどちらの列にも、Nullに関するチェック処理を追加しましたが、Emailのほうの条件だけ無視されています。
  • から文字列でないことの確認は、どちらの列も同じように出力されました。

Null が無視される件については、DBContextクラスのRequire設定が影響を与えていました。自動生成されたDBContextクラスの設定を以下のように変更することで、Null条件が無視されずに出力されることを確認しました。※無理やり出す必要もないですね(´▽`)

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Account>(entity =>
            {
                entity.Property(e => e.Email)
                    //.IsRequired()   <- Here
                    .HasColumnName("email")
                    .HasMaxLength(355);
                entity.Property(e => e.Note)
                    .HasColumnName("note")
                    .HasColumnType("character varying");
            });
        }
SELECT a.user_id, a.created_on, a.email, a.last_login, a.note, a.password, a.sex_id, a.username
FROM account AS a
WHERE (((a.email IS NOT NULL) AND (a.email <> '')) AND 
            (a.note IS NOT NULL)) AND (a.note <> '')

WHERE句 Null検索 with String メソッド

文字列の値が、Nullであること、空文字列であることを確認するときにお世話になるメソッド・・・そう、String.IsNullOrEmpty。これを使って、同じようにEmailとNote列の比較を行ったらどうなるか、調べてみました。

//IsNullOrEmptyを使った場合
SELECT a.user_id, a.created_on, a.email, a.last_login, a.note, a.password, a.sex_id, a.username
FROM account AS a
WHERE a.email = ''

//IsNullOrEmptyを使った場合
SELECT a.user_id, a.created_on, a.email, a.last_login, a.note, a.password, a.sex_id, a.username
FROM account AS a
WHERE (a.note IS NULL) OR (a.note = '')


//IsNullOrWhiteSpaceを使った場合
SELECT a.user_id, a.created_on, a.email, a.last_login, a.note, a.password, a.sex_id, a.username
FROM account AS a
WHERE BTRIM(a.email, E' \t\n\r') = ''

//IsNullOrWhiteSpaceを使った場合
SELECT a.user_id, a.created_on, a.email, a.last_login, a.note, a.password, a.sex_id, a.username
FROM account AS a
WHERE (a.note IS NULL) OR (BTRIM(a.note, E' \t\n\r') = '')
  • Stringクラスのメソッドを使った場合も、NotNull制約がある列に対しては、Nullの検査が無視されました。
  • String.IsNullOrWhiteSpaceを使った場合、文字列から、特定の文字列を削除した後の結果に対して、空文字検索が行われました。特定の文字列は、半角スペース、タブ、改行、キャリッジリターンの4つが含まれています。全角スペースは、含まれていません。ちなみに、以下のように、C#でのString.IsNullOrWhiteSpaceでは、全角スペースも含まれています。ここは、気を付けないといけないポイントですね!
string.IsNullOrWhiteSpace(null)
// →true
string.IsNullOrWhiteSpace(string.Empty)
// →true
string.IsNullOrWhiteSpace(" ")
// →true
string.IsNullOrWhiteSpace(" ")
// →true
string.IsNullOrWhiteSpace("あ")
// →false

まとめ

EF Coreのクエリ条件で、string.IsNullOrWhiteSpaceを使うと、半角スペース、タブ、改行、キャリッジリターンを考慮して判定をするSQLが実行される。ただし、C#の仕様と違い、全角スペースは含まれない。


つづく!





EF Core 3.1 × Postgres × 実際に出力されるSQLの検証を始めてみた

この記事の目的

この記事は、
EF Coreで実際に実行されるSQLの確認をしてみること
を目的としています。

本題

準備

0ベースで準備するのに結構手間がかかったので、どうやって準備したかメモしておきます。

1. Porjectの準備

新規で作成したコンソールアプリケーションのプロジェクトに、NugetでEF CoreのPostgresをインストールします。
www.npgsql.org
Npgsql.EntityFrameworkCore.PostgreSQL

2. DBの準備

DBはDocker Composeを使って、Dockerの中に構築しました。

  • Docker-composrで起動する

www.kinakomotitti.net

  • Postgresのログ出力設定を変更する

↑のYAMLのCommandに渡すオプションを変更したらできました。

command: postgres -c log_destination=stderr -c log_statement=all -c log_connections=on -c log_disconnections=on

参考)qiita.com

  • Sample Tableの作成

いい感じのテーブル定義があったのでこちらを利用させて頂きました。
www.postgresqltutorial.com

  • Logを見る

docker のコマンドに、Tailオプションがあるので、それをつかって、Logの監視をします。これをすることで、コンソールアプリを実行したとき、そのまま出力されたログを確認することができます。
docs.docker.jp


シンプルなCRUD

Insert

まずは、Insertについて確認しました。
登録するテーブルの情報はこんな感じです↓。

f:id:kinakomotitti:20200530163054p:plain
Accountテーブル

一部省略していますが、AccountテーブルにSampleデータを登録するコードは以下のものを利用しました。

context.Account.Add(new Account()
{
   Username = $"Sample0",
   Password = "Sample",
   Email ="0Sample@email.com"
});

この時、出力されたSQLは・・・

BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED
INSERT INTO account (created_on, email, last_login, password, sex_id, username)
        VALUES ($1, $2, $3, $4, $5, $6)
        RETURNING user_id
parameters: 
       $1 = '0001-01-01 00:00:00', 
       $2 = 'Sample.User@email.com', 
       $3 = NULL, 
       $4 = 'Sample', 
       $5 = NULL, 
       $6 = 'Sample_User'
COMMIT

ポイント

既定では、データベース プロバイダーがトランザクションをサポートしている場合は、SaveChanges() への 1 回の呼び出しに含まれるすべての変更がトランザクションに適用されます。 いずれかの変更が失敗した場合、トランザクションロールバックされ、変更は、データベースにまったく適用されません。
docs.microsoft.com

  • すべての列が明示的に登録される。ただし、ID列のように、Serial型の場合は、明示的に登録されない。
  • Returning 句がついている・・・ついているのに、Returningされたuser_idは、SaveChangesメソッドの戻り値として取得することができない。※戻ってくるのは、今回のクエリで影響を受けた行数。Serial(シーケンス)を使った列の自動採番された値を取得したい場合の解決方法は、前にまとめた気がするので、そっちも見てみて下さい。参考)www.kinakomotitti.net



Select

次に、Read(SELECT)を試してみました。SELECTは、JoinとかWHERE句とかいろいろな検証をしてみたいですが、基礎的なところから順番に・・・ということで、簡単なSELECTを実行しました。

Linqで検索処理を記述するとき、クエリ式を使った書き方と、メソッドチェーンで繋いでいく書き方の2つの方法があります。自分は、クエリ式のほうをよく使います(見た目がSQLっぽくてわかりやすいからです。)が、検証のため、2パターンの実行を試してみました。

//メソッド
context.Account.Select(row => row.Email).ToList();

//クエリ式
(from account in context.Account select account.Email).ToList();


出力結果は・・・

--2パターンとも、同じ結果でした。
SELECT a.user_id, 
            a.created_on, 
            a.email, 
            a.last_login, 
            a.password, 
            a.username
        FROM account AS a


Joinした場合についても一例だけ検証しました。

 //メソッド
context.Account.Join(
       context.AccountRole,
       p => p.UserId,
       t => t.UserId,
       (p, t) => new
       {
              UserName = p.Username,
              GrantDate = t.GrantDate
       }).ToList();

//クエリ式
(from account in context.Account
       join accountRole in context.AccountRole on account.UserId equals accountRole.UserId
       select new
       {
              UserName  = account.Username,
              GrantDate = accountRole.GrantDate
        }
).ToList();


出力結果は・・・

//こちらも同じ出力結果になりました。
SELECT a.username AS "UserName", 
            a0.grant_date AS "GrantDate"
FROM account AS a
INNER JOIN account_role AS a0 ON a.user_id = a0.user_id


ポイント

  • テーブルに別名がつけられている・・・a, b, c, となるかと思いきや、Joinの場合は、a, a0…となるようです。それがどうしたということでもないですが・・・
  • クエリ式でも、メソッド式でも、同じSQLが生成されました。コード的には、圧倒的にクエリ式が見やすいと感じます。特にJoinの構文とか・・・
Update

次はUpdateです。

//Update対象の行を適当に決めます。
var updateUser = context.Account.First();

//E-mailだけ更新します。
updateUser.Email = "Kinakomotitti@email.com";

//成功時には、resultに1が入ります。。。
var result = context.SaveChanges();


出力結果は・・・

BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED
UPDATE account SET email = $1
        WHERE user_id = $2
parameters: 
         $1 = 'Kinakomotitti@email.com', 
         $2 = '5'
COMMIT

そんなかんじですね。

Delete

最後はDeleteです。

//削除対象のユーザーをサクッと決ます。
var deleteUser = context.Account.First();
//削除します。
context.Remove(deleteUser);
//以下同文。
var result = context.SaveChanges();


出力結果は・・・

BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED
DELETE FROM account
        WHERE user_id = $1
parameters: $1 = '6'
COMMIT

でした!


続く。。。

その他リファレンス

qiita.com
docs.microsoft.com