ORM使用時のテスト中にOOPとDBの間の概念的なギャップを埋めるのに役立つライブラリ-LinqTestable

ご存知のように、オブジェクト指向モデルとリレーショナルモデルの間には概念的なギャップがあり、ORMでさえブリッジすることはできません。 基本的に、このギャップは、リレーショナルデータベースを使用する場合、特定のオブジェクトではなくセットでの作業を強制されるという事実に影響します。 しかし、もう1つの要因があります。データベースでのNULLの動作は、オブジェクト指向言語でのNULLの動作とは異なります。 これは、2つの状況で同じクエリを使用する場合に問題になる可能性があります。1)データベースをクエリするとき2)データベースのテーブルの代わりにRAMの配列を使用する単体テスト中。 さらに、データベースのみを参照している場合、これは問題になりますが、リレーショナルデータベースではなく、OOPの観点からNULLを考えてください!



画像



例1


外部キーによってリンクされた3つのテーブルがあります:車、ドア、ドアハンドル。 すべての外部キーはnull値を許可しません。 各ドアとハンドルには、正確に取り付けられているもの(特定の機械またはドア)を示す必要があります。



テーブルソースコード
(Oracleはデータベース、ORM-EntityFramework、言語-C#として使用されました。)



create table CAR ( CAR_ID NUMBER(10) not null ); alter table CAR add constraint CAR_PK primary key (CAR_ID); create table DOOR ( DOOR_ID NUMBER(10) not null, CAR_ID NUMBER(10) not null ); alter table DOOR add constraint DOOR_PK primary key (DOOR_ID); alter table DOOR add constraint DOOR_CAR_FK foreign key (CAR_ID) references CAR (CAR_ID); create index DOOR_CAR_ID_I on DOOR (CAR_ID) tablespace INDX_S; create table DOOR_HANDLE ( DOOR_HANDLE_ID NUMBER(10) not null, DOOR_ID NUMBER(10) not null, COLOR NVARCHAR2(15) null ); alter table DOOR_HANDLE add constraint DOOR_HANDLE_PK primary key (DOOR_HANDLE_ID); alter table DOOR_HANDLE add constraint DOOR_HANDLE_DOOR_FK foreign key (DOOR_ID) references DOOR (DOOR_ID); create index DOOR_HANDLE_DOOR_ID_I on DOOR_HANDLE (DOOR_ID) tablespace INDX_S;
      
      









データベースにマシンを1つ作成してみましょう。残りのテーブルは空のままです。 次に、ORMを使用してマシンとドアの間に左結合を行います。



 var cars = (from car in dataModel.CAR join door in dataModel.DOOR on car.CAR_ID equals door.CAR_ID into joinedDoor from door in joinedDoor.DefaultIfEmpty() //left join select new { car.CAR_ID, door.DOOR_ID }).ToList();
      
      







このクエリを返すと思いますか?



そうです、ORMはあなたに例外を投げ、あなたに森を送ります。 なんで? Bdは文字列を返します

データベースとマッピングの両方でdoor.DOOR_IDをNULLにすることはできないことが示されているため、CAR_ID = 1、DOOR_ID = NULL、およびORMはそれを処理できません。 NULLは、左結合のみが原因で表示されました。 おそらく、ORMの「曲線」が原因でしょうか? いいえ、ORMの動作はまったく正しいです。nullを0に置き換えるか、空の文字列を返すということは、ユーザーを欺くということです。 マッピングの変更もオプションではありません。コードでは、フィールドを空白のままにすることができると言われ、ビジネスロジックではその逆が必要になります。 解決策は、ORMがフィールドがnullになる可能性があることを理解できるようにリクエストを変更することです。



 var cars = (from car in dataModel.CAR join door in dataModel.DOOR on car.CAR_ID equals door.CAR_ID into joinedDoor from door in joinedDoor.DefaultIfEmpty() select new { car.CAR_ID, DOOR_ID = door != null ? door.DOOR_ID : (int?) null }).ToList();
      
      





要求を手動で変更することも、実行時にそのようなすべての要求を自動的に変更するコードを作成することもできます(後で詳しく説明します)。



例2


2つの左結合を持つ要求があります。

 var carsWithoutRedHandle = (from car in dataModel.CAR join door in dataModel.DOOR on car.CAR_ID equals door.CAR_ID into joinedDoor from door in joinedDoor.DefaultIfEmpty() join doorHandle in dataModel.DOOR_HANDLE on door.DOOR_ID equals doorHandle.DOOR_ID into joinedDoorHandle from doorHandle in joinedDoorHandle.DefaultIfEmpty() where doorHandle.Color != “RED” || doorHandle == null select car).ToList();
      
      







データベースにアクセスすると、このリクエストは完全に処理されます。 ただし、単体テストで使用する価値があります。2番目の結合でdoor.DOOR_IDにアクセスしようとすると、上部が開いているために車が必要ない場合、NullReferenceExceptionが発生します。 さて、リクエストを変更する時間:



 var carsWithoutRedHandle = (from car in dataModel.CAR join door in dataModel.DOOR on car.CAR_ID equals door.CAR_ID into joinedDoor from door in joinedDoor.DefaultIfEmpty() join doorHandle in dataModel.DOOR_HANDLE on (door != null ? door.DOOR_ID : (int?)null) equals doorHandle.DOOR_ID into joinedDoorHandle from doorHandle in joinedDoorHandle.DefaultIfEmpty() where doorHandle.Color != “RED” || doorHandle == null select car).ToList();
      
      







ただし、1つの「しかし」があります。 linqクエリを変更すると、実行プランがはるかに遅いSQLクエリを取得できます。 そのような例を見てみましょう。



 using System.Linq; using System.Linq.Expressions; using LinqKit; IEnumerable<CAR> GetCars(IDataModel dataModel, Expression<Func<DOOR, bool>> doorSpecification = null, Expression<Func<DOOR_HANDLE, bool>> doorHandleSpecification = null) { if (doorSpecification == null) doorSpecification = door => true; if (doorHandleSpecification == null) doorHandleSpecification = handle => true; var cars = (from car in dataModel.CAR.AsExpandable() join door in dataModel.DOOR on car.CAR_ID equals door.CAR_ID into joinedDoor from door in joinedDoor.DefaultIfEmpty() join doorHandle in dataModel.DOOR_HANDLE on /*(door != null ? door.DOOR_ID : (int?)null)*/door.DOOR_ID equals doorHandle.DOOR_ID into joinedDoorHandle from doorHandle in joinedDoorHandle.DefaultIfEmpty() where doorSpecification.Invoke(door) && doorHandleSpecification.Invoke(doorHandle) select car); return cars; } var carsWithRedHandle = GetCars(dataModel, doorHandleSpecification: doorHandle => doorHandle.COLOR == "RED").ToList();
      
      





結合が次のように発生する場合のSQLクエリとその実行計画を次に示します。door.DOOR_IDがdoorHandle.DOOR_IDに等しい



画像



そして、これは(door!= Null?Door.DOOR_ID:(int?)Null)がdoorHandle.DOOR_IDと等しい場合の実行計画です。



画像



ご覧のとおり、実行計画は完全に異なり、そのコストは1.5倍です。

この問題を解決するには、#if DEBUGを使用してデバッグでテストを実行しますが、信じられますが、コードの可読性と信頼性はこれからまったく向上しません。 芽の問題に対処する方がはるかに良いです-ユニットテストを書くときに、左結合のこの機能をまったく心配する必要がないようにするために。 この目的のために、 https://github.com/FiresShadow/LinqTestableに投稿されたライブラリを作成しました。

ライブラリを使用するには、プロジェクトをダウンロードして接続し、プロジェクトのMockObjectSetを変更する必要があります。つまり、この部分を置き換えます。

  public System.Linq.Expressions.Expression Expression { get { return _collection.AsQueryable<T>().Expression; } } public IQueryProvider Provider { get { return _collection.AsQueryable<T>().Provider; } }
      
      





に:

 public System.Linq.Expressions.Expression Expression { get { return _collection.AsQueryable<T>().ToTestable().Expression; } } public IQueryProvider Provider { get { return _collection.AsQueryable<T>().ToTestable().Provider; } }
      
      





その後、上記の単体テストの問題は自動的に解消されます。



ところで、 ここで Entity Frameworkの単体テストの作成方法を読むことができます



ライブラリは少し湿っていて、1つの問題だけを解決します。2つの左結合を使用したNullReferenceExceptionです。 この問題を解決するだけでは概念的なギャップは解消されません。他にも多くの問題があります。たとえば、nullとnullが等しいかどうかを比較すると、リレーショナルモデルとオブジェクト指向モデルで異なる結果が生じます。 しかし、この問題も解決可能です。



All Articles