Библиотека, Которая Помогает Преодолеть Концептуальный Разрыв Между Ооп И Бд Во Время Тестирования При Использовании Orm — Linqtestable

Как известно, между объектно-ориентированной и реляционной моделями существует различие.

концептуальный разрыв , который не способен преодолеть даже ORM. Этот разрыв в основном влияет на то, что при использовании реляционной базы данных мы вынуждены работать с множествами, а не с конкретными объектами.

Но есть еще один фактор: поведение NULL в базе данных отличается от поведения NULL в объектно-ориентированных языках.

Это может стать проблемой, если вы используете один и тот же запрос в двух ситуациях: 1) при запросе к базе данных 2) во время модульного тестирования, когда вместо таблицы из базы данных используется массив в памяти.

Более того, это может стать проблемой, если вы обращаетесь только к базе данных, но думаете о NULL в терминах ООП, а не о реляционной базе данных!

Библиотека, которая помогает преодолеть концептуальный разрыв между ООП и БД во время тестирования при использовании ORM — LinqTestable



Пример 1
Есть три таблицы, связанные внешним ключом: машина, дверь, дверная ручка.

Все внешние ключи не являются обнуляемыми, т.е.

на каждой двери и ручке должно быть указано, к чему именно они прикреплены (конкретному автомобилю или двери).

Исходный код для создания таблиц (В качестве базы данных использовался 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;

Создадим в базе одну машину, остальные таблицы останутся пустыми.

Затем мы просто создадим левое соединение между автомобилем и дверями, используя 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 выкинет вам исключение и отправит вас куда подальше.

Почему? База данных вернет строку CAR_ID=1, DOOR_ID=NULL, и ORM не сможет его обработать, поскольку и база данных, и сопоставление указывают, что Door.DOOR_ID не может быть NULL. NULL появился исключительно из-за левого соединения.

Может быть, виновата «кривая» ORM? Нет, поведение ORM вполне корректное: замена null на 0 или возврат пустой строки означает обман пользователя.

Изменить маппинг тоже не вариант: код скажет, что поле можно оставить пустым, но бизнес-логика потребует обратного.

Решение состоит в том, чтобы изменить запрос, чтобы 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() select new { car.CAR_ID, DOOR_ID = door != null ? door.DOOR_ID : (int?) null }).

ToList();

Вы можете изменить запрос вручную или написать код, который будет автоматически изменять все такие запросы во время выполнения (но об этом позже).



Пример 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();

Этот запрос будет отлично обработан при доступе к базе данных.

Но используйте его в модульном тесте, и вы получите исключение NullReferenceException при попытке доступа к Door.DOOR_ID во втором соединении, если какой-либо из машин не нужны двери, потому что верх открыт. Что ж, пора изменить запрос:

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();

Однако здесь есть одно «но».

Изменив запрос 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.

Библиотека, которая помогает преодолеть концептуальный разрыв между ООП и БД во время тестирования при использовании ORM — LinqTestable

А вот план выполнения, когда (door != null? Door.DOOR_ID: (int?)null) равно DoorHandle.DOOR_ID

Библиотека, которая помогает преодолеть концептуальный разрыв между ООП и БД во время тестирования при использовании ORM — LinqTestable

Как видите, план выполнения совершенно другой, а Стоимость его в полтора раза больше.

Для решения этой проблемы можно использовать #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. Здесь .

Библиотека немного сыроватая и решает только одну проблему: исключение NullReferenceException с двумя левыми соединениями.

Решение этой проблемы само по себе не устраняет концептуальный разрыв, существует множество других проблем, например: Сравнение нуля с нулем на предмет равенства дает разные результаты в реляционных и объектно-ориентированных моделях.

Но и эту проблему можно решить.

Теги: #тестирование приложений #оптимизация запросов #философия программирования #.

NET #sql #tdd

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.