Интересные моменты работы LINQ to SQL. Опять
С моего предыдущего поста прошёл месяц, по-моему самое время продолжить. В этот раз поговорим об Inheritance Mapping«е, ну, а особо интересующихся в конце статьи ждёт сюрприз.Итак, начнём.
Проблемы с дискриминаторомРазумеется, мы храним в нашей базе данных полиморфные сущности. Например, есть сущность CustomerOperation, которая отражает некоторую операцию, которую можно совершать над потребителем. Операции совершаются в основном через сервисы, поэтому есть наследник CustomerServiceOperation, а так же у нас есть механизм WebTracking«а, для которого есть WebTrackingOperation. Но довольно слов, лучше покажу код: [Table (Name = «directcrm.CustomerOperations»)] [InheritanceMapping (Code = », Type = typeof (CustomerOperation), IsDefault = true)] [InheritanceMapping (Code = «Service», Type = typeof (CustomerServiceOperation))] [InheritanceMapping (Code = «WebTracking», Type = typeof (WebTrackingOperation))] public class CustomerOperation: CampaignItemBase, ICampaignItem { // тут что-то происходит
[Column (Storage = «discriminator», CanBeNull = false, IsDiscriminator = true)]
public string Discriminator
{
get
{
return discriminator;
}
private set
{
if (discriminator!= value)
{
SendPropertyChanging ();
discriminator = value;
SendPropertyChanged ();
}
}
}
// тут происходит что-то другое
}
Попробуем получить какую-нибудь WebTracking-операцию. А вот и код:
modelContext.Repositories.Get
SELECT TOP (1) NULL AS [EMPTY]
FROM [directcrm].[CustomerOperations] AS [t0]
WHERE 0 = 1
Умный LINQ to SQL! Но исключение было бы лучшим выбором, на мой скромный взгляд. Хорошо, а если мы забыли зарегистрировать тип, но прошлись по таблице этих операций и некоторым тип поменяли прямо в базе скриптом (например, раньше все операции были CustomerService, а теперь появились WebTracking, и некоторые старые нужно обновить). Попробуем вычитать операцию с незарегистрированным дискриминатором из базы:
var test = modelContext.Repositories.Get
Вот код:
var test = mc.Repositories.Get
Отлично, давайте теперь попробуем записать именно WebTrackingOperation в базу (при рефакторинге, который я описал выше, код, создающий такие сущности, обязательно бы появился). Попытка эта провалится с забавным NullReference, пруф:
Пока не обращайте внимания, что ошибка падает в загадочной Mindbox.Data.Linq.dll, она падает точно так же и в классическом LINQ to SQL. Из ошибки, как вы может заметить, вообще ни разу не видно, где искать проблему, так что баг не очень приятный. Будьте внимательнее и не забывайте указывать все типы полиморфных сущностей в атрибутах Inheritance Mapping«а.
Ну и напоследок о забавном свойстве дискриминатора. Попробуем создать новую сущность базового класса CustomerOperation, присвоить дискриминатор самостоятельно и сохранить:
var newOp = new CustomerOperation ();
newOp.NormalizeAndSetName («TestForHabr»);
newOp.NormalizeAndSetSystemName («TestForHabr»);
newOp.NormalizeAndSetDescription («TestForHabr»);
newOp.Discriminator = «WebTracking»;
newOp.Campaign = test.Campaign;
mc.Repositories.Get
mc.SubmitChanges (); При этом будет сгенерирован insert со значением поля Discriminator = WebTracking, однако после вставки LINQ to SQL перевыставит дискриминатор сам — то есть вызовет его setter с пустой строкой (потому что это значение по-умолчанию для базового типа было указано в Inheritance Mapping атрибуте):
Если это поведение вас не устраивает, то есть простой workaround: в setter«е дискриминатора игнорировать выставление пустой строки.
Непрошеная энумерация У LINQ to SQL есть один (нет, ну разумеется ни разу не один, но сейчас речь о конкретном) очень неприятный момент. Почти всегда, если запрос на linq был построен таким образом, что его не удаётся смаппить на sql, linq при вызове энумератора кидает исключение. Текст таких исключений всем известен, это может быть что-то типа (вытащил парочку из багтрекера): «Member access 'System.DateTime DateTimeUtc' of 'Itc.DirectCrm.Model.CustomerAction' not legal on type 'System.Linq.IQueryable`1[Itc.DirectCrm.Model.CustomerAction]» или «Method 'Boolean Evaluate[CustomerLotteryTicket, Boolean](System.Linq.Expressions.Expression`1[System.Func`2[Itc.DirectCrm.Promo.CustomerLotteryTicket, System.Boolean]], Itc.DirectCrm.Promo.CustomerLotteryTicket)' has no supported translation to SQL». Ключевое слово тут почти. Иногда LINQ to SQL может посчитать, что вместо того, чтобы кинуть подобное исключение, лучше вычитать побольше сущностей в память, и в памяти уже произвести какие-то преобразования. Это очень печально по нескольким причинам: это не документировано никак (на сколько мне известно), из-за этого иногда падают OutOfMemory (так как вычитанные сущности уже никогда не покинут контекст, хотя код будет выглядеть так, как будто ты вычитываешь анонимные объекты, которые будут быстро собираться GC), а так же из-за багов с Inheritance Mapping. Собственно, давайте посмотрим на такой баг.Есть сущность «Шаблон действия», она отражает различные виды действий людей в системе. Есть простые шаблоны: человек совершил авторизацию, зашёл на какой-то конкретный раздел сайта, выиграл приз. Так же есть всякого рода рассылки, которые у нас реализованы через другие типы шаблонов действий — то есть через inheritance mapping. Кусочек кода, чтобы всё было хорошо:
[Table (Name = «directcrm.ActionTemplates»)]
[InheritanceMapping (Code = », Type = typeof (ActionTemplate), IsDefault = true)]
[InheritanceMapping (Code = «Hierarchical», Type = typeof (HierarchicalActionTemplate))]
[InheritanceMapping (Code = «CustomerToCustomer», Type = typeof (CustomerToCustomerActionTemplate))]
[InheritanceMapping (Code = «EmailMailing», Type = typeof (EmailMailingActionTemplate))]
[InheritanceMapping (Code = «SmsMailing», Type = typeof (SmsMailingActionTemplate))]
[InheritanceMapping (Code = «BannerCampaign», Type = typeof (BannerCampaignActionTemplate))]
public class ActionTemplate: INotifyPropertyChanging, INotifyPropertyChanged, IValidatable, IEntityWithSystemName
{
// тут что-то есть
}
Шаблон действия с id = 20 является EmailMailingActionTemplate — Email-рассылкой (я проверил), давайте вычитаем его:
var actualTemplate = modelContext.Repositories.Get
var test = modelContext.Repositories.Get
SELECT TOP (1) [t0].[Id], [t0].[CategoryId], [t0].[Name], [t0].[Type], [t0].[Discriminator], [t0].[RowVersion], [t0].[SystemName], [t0].[CreationCondition], [t0].[UsageDescription], [t0].[StartDateTimeUtcOverride], [t0].[EndDateTimeUtcOverride], [t0].[DateCreatedUtc], [t0].[DateChangedUtc], [t0].[CreatedById], [t0].[LastChangedById], [t0].[CampaignId] FROM [directcrm].[ActionTemplates] AS [t0] WHERE [t0].[Id] = @p0 После такого запроса предыдущий (вытаскивающий конкретный шаблон по Id) возвращает шаблон действия базового типа: Автоматическая энумерация, как показывает такой результат, просто не обращает внимания на Inheritance Mapping, всегда создавая сущности базового класса. Бойтесь автоматической энумерации! Комментарий от IharBury:
Боюсь, это не совсем отражает, то что на самом деле происходит. LINQ не трактует такой код, как AsEnumerable. Он выполняет маппинг для того, что мапится, а остальное выполняет в памяти. Например, если ты сделаешь не просто Select свойства сущности, а Select свойства у связанной сущности, то будет создана в памяти только связанная сущность.Хорошим советом было бы не делать у сущностей свойств, которые не мапятся на SQL — делать их методами, чтобы сразу была видна проблема.
FixedItems
Вы могли заметить, что в части, где я рассказывал про проблемы с дискриминатором, у меня был представлен следующий код:
modelContext.Repositories.Get
Как можно догадаться, весь доступ к СУБД из кода осуществляется через репозитории. У каждого репозитория есть методы-хелперы для получения сущностей по каким-то признакам (у репозитория потребителей метод GetByName и т.п.), но, так как не практично на каждый чих создавать свой метод, то у каждого нашего репозитория есть свойство Items — это просто IQueryable нужных сущностей. Но что же такое FixedItems на CustomerOperationRepository? Самое забавное — свойство FixedItems в коде выглядит так:
public IQueryable
CustomerOperationRepository даёт доступ к сущностям CustomerOperation, но сама сущность CustomerOperation является элементом кампании (нашего большого агрегата, к которому привязывается довольно много других сущностей). У этих сущностей похожая валидация, есть множество общих свойств, так что они наследуются от одного класса и репозитории их тоже наследуются. Так же все элементы кампании наследуются от интерфейса ICampaignItem, а базовый класс репозитория принимает тип сущности в качестве первого параметра generic«а:
public abstract class CampaignItemRepositoryBase
Вот как это происходит. Обратите внимание на значения CanBeNull и тип колонки в разных сущностях:
public class IdentificationTrackingOperation: CustomerOperation { private int? operationStepGroupId;
[Column (Storage = «operationStepGroupId», CanBeNull = true)] public int? OperationStepGroupId { get { return operationStepGroupId; } set { if (operationStepGroupId!= value) { SendPropertyChanging (); operationStepGroupId = value; SendPropertyChanged (); } } } }
public class PerformActionCustomerServiceOperation: CustomerServiceOperation, IPerformActionCustomerServiceOperation
{
private int operationStepGroupId;
private EntityRef
[Column (Storage = «operationStepGroupId», CanBeNull = false)] public int OperationStepGroupId { get { return operationStepGroupId; } set { if (operationStepGroupId!= value) { SendPropertyChanging (); operationStepGroupId = value; SendPropertyChanged (); } } } } Ну, отлично, а теперь попробуем прочитать из базы список операций. Одна из этих операций — IdentificationTrackingOperation, у которой отсутствует OperationStepGroupId.Попытка ожидаемо проваливается, потому что мы получаем InvalidOperationException: CannotAssignNull: System.Int32.
Как с этим бороться? Мы сделали просто — для LINQ разрешили в PerformActionCustomerServiceOperation отсутствие значений для OperationStepGroupId, и в своей валидации это отдельно проверяем.
Тем, кто дочитал, сюрприз Вы могли заметить, что ошибки в LINQ у меня на скриншотах падают в сборке с названием Mindbox.Data.Linq.dll. Да, мы форкнули LINQ to SQL. Например, теперь для регистрации наследников сущностей можно использовать не только InheritanceMappingAttribute, но и метод void AddInheritance (object code) на Mindbox.Data.Linq.Mapping.MindboxMappingConfiguration, что позволяет регистрировать наследников сущностей в разных сборках.Наш форк можно поставить через Nuget: Mindbox.Data.Linq.
Может быть, вы захотите чем-то помочь или воспользоваться — удачи с этим.