Наследование в ADO.NET Entity Framework
Практический разбор двух типов наследования в ADO.NET Entity Framework: Table per Type и Table per Hierarchy — с типичными ошибками и их решениями.
О чём вы, Морфеус?
Приветствую всех! Моя первая статья на Хабре была оценена достаточно высоко. Спасибо всем, кто оставил своё мнение — мне было приятно вас почитать. Продолжаю.
В этой статье хотелось бы поговорить о наследовании. Признаться честно, до изучения ADO.NET Entity Framework я вообще даже не задумывался о том, чтобы вводить в свои проекты наследование сущностей в объектно-ориентированных обёртках для БД. Обычно базу строили так, чтобы максимально избегать наследования. Хотя порой оно и маячило на горизонте, но обходилось. Сейчас я опишу, как я добавил в свой проект два очень простых класса, которые были отнаследованы от уже имеющихся таблиц.
Опять же замечу, что я не буду углубляться в теорию — всё показываю на практике. Для теории я сделаю отдельный цикл статей.
Подготовка
Скажу сразу: я над кодом просидел час, разбирая те ошибки, которые у меня возникли во время создания этой статьи. И моя цель сейчас — не только показать, как реализуется наследование в ANEF, но и показать вам типовые ошибки, которых можно избежать.
Да, кстати, неявно договорились вот о чём: ANEF = ADO.NET Entity Framework.
Структура базы данных
Со времён первой статьи структура БД немного изменилась, но всё же осталась очень простой. Изначально в БД присутствовали только две таблицы: Post и User, сейчас схема была немного усложнена:
-
Я создал таблицу BlogPost. Смысл этого в том, что в моей системе
Postбудет являться не только постом блога, но и комментарием, сообщением и вообще всем, что один пользователь может передать другому. Такая схема сделана исключительно для целей обучения, потому что в большинстве систем комментарии — это очень критичная таблица, в которой постоянно много записей и которая является самой нагруженной таблицей в системе. Соответственно,BlogPostдолжен унаследовать все данные изPostи нести в себе дополнительную информацию. Я хочу, чтобы в моей системе под каждым постом блога пользователи сами писали название ссылки на комментарии. Например: «Жгут здесь», «Об этом думают». Это внесёт некое разнообразие. Тут я применю первый тип наследования — каждая Entity имеет свою таблицу (Table per Type). -
Также мне было бы удобно, чтобы в моей модели БД были и простые User’ы и Admin’ы. Посему, используя поле
IsAdmin, я буду делать второй тип наследования — одна таблица на несколько Entity (Table per Hierarchy). По значению в этом поле будет происходить отсеивание entity.
Table per Type: BlogPost наследует Post
Сразу оговорюсь — здесь я сделал самую большую свою ошибку, которая стоила мне 20 минут работы с форумами. А в общем-то она была очень глупой и была сделана по недосмотру.
Важно правильно расставить связи в БД. В первый раз я по глупости развернул их в другую сторону, то есть сделал таблицу BlogPost Primary в связи. Ладно, бывает, поехали дальше.
Заходим в студию, в наш проект базы данных и обновляем нашу схему из БД. При этом должна появиться новая Entity BlogPost. Всё отлично, появилась. Даже со связью. Удаляйте эту связь первым делом. В наследованных Entity она нам не нужна. После этого — в контекстном меню Entity Post мы добавляем новое наследование.
И вот тут я в первый раз словил разочарование от визуальной среды разработки. Запускаем валидацию проекта (я рекомендую запускать эту валидацию чуть ли не после каждого чиха, если вы ещё не совсем освоились в работе с наследованием ANEF). И валидация с треском падает.
Неприятность заключается в том, что нам надо ещё немного пошаманить и кое-что понять, прежде чем нам удастся нормально организовать наследование. У нас есть две таблицы: BlogPost и Post. В таблицах есть два ключа — BlogPost.PostId и Post.Id, так считает SQL. ANEF достаточно верно считает, что на самом деле у таблицы BlogPost нет ключа BlogPost.PostId, а есть ключ Post.Id. В принципе это более чем логично — у нас есть связь 1:1, так зачем нам заморачиваться с ещё одним ключом?
Тоже верно. Удаляем из Entity BlogPost параметр PostId. После этого валидация снова рушится — тоже правильно. У нас в таблице есть значение PostId, но оно не имеет никаких мэппингов. Исправляем этот недочёт: выставляем мэппинг для поля таблицы PostId в переменную Id.
Сразу возникает вопрос — а где это мы успели определить переменную Id для таблицы BlogPost? Как я говорил раньше — ANEF считает, что у Entity Post и BlogPost есть только один ключ, поэтому неявно пририсовала его к таблице BlogPost.
Кажется, всё — теперь это можно использовать. Пытайтесь, компилируйте.
Table per Hierarchy: Admin наследует User
Это был первый тип наследования — каждая Entity привязана к своей таблице в БД. Теперь перейдём ко второму типу наследования, где у нас есть только одна таблица и несколько типов Entity.
Как я уже говорил — я хотел отделить Entity Admin от User. С помощью ANEF это достаточно просто сделать. Через контекстное меню добавляйте новую Entity, при этом укажите, что она отнаследована от Entity User. Сразу же можете удалять свойство IsAdmin из entity User и из Admin — у меня оно оставлено для наглядности, но с ним вы не пройдёте валидацию.
Опять надо немного поколдовать. Для начала надо настроить условия мэппингов. В частности, сейчас у нас все User и все Admin одинаковы — их ничего не разделяет. Переходим к мэппингам entity и выбираем условия, при которых должен проходить мэппинг.
ANEF не даёт возможности вынести поле IsAdmin в переменную, так как оно является условием мэппинга.
И вот ещё одна крутая ошибка. Я лазил по форумам ещё пять минут, пока до меня не дошёл достаточно простой факт: если у нас по какому-либо условию идёт отсеивание записей в таблицу Admin, то это не значит, что все остальные записи автоматом уйдут в таблицу пользователей. Поэтому необходимо в мэппингах таблицы User тоже включить условие и отсеивать остальные записи в эту таблицу.
Сразу оговорюсь — у меня поле IsAdmin, несмотря на название, не Boolean, а Int16, так что отсеивание проводилось по принципу IsAdmin=1 и IsAdmin=0. В дальнейшем у нас была идея расширить возможности по администрированию, так что взято с заделом на будущее.
Итоги
У нас образовались две очень простых, но всё же отнаследованных entity. Какие именно свойства и как наследовать — не столь важно. Вы сами можете поэкспериментировать с этим. Моей целью было показать, как именно проводить это наследование.
Пока я копался в наследовании ANEF, я прикинул несколько схем, реализация которых действительно была бы облегчена с использованием наследования. Например, в одной из наших систем существовала система работы с портфолио, где человек мог вводить о себе различные данные: место работы, образование, научные публикации и так далее. Для каждого из типов записи в портфолио была отдельная таблица, и могу вам сказать точно — SQL-запрос, который выбирал эти данные, был просто адским. Всё было очень неудобно и криво. Если бы я тогда использовал наследование, то имел бы List<> базовых классов, например PortfolioEntry, с которыми мог бы быстро и удобно работать. Это пример из жизни — я думаю, покопавшись во внутренностях ANEF, вы сами найдёте множество таких примеров в своём коде.
В следующей статье я постараюсь привести побольше программного кода, который покажет, как правильно работать с наследованными entity.
P.S. Всё это было проделано на бесплатных версиях Visual Studio и Management Studio. Microsoft не самым зверским образом урезал свои продукты.
Читать дальше
Похожие посты
ADO.NET Entity Framework: практическое введение
Практическое введение в ADO.NET Entity Framework — от создания модели данных до выполнения базовых операций CRUD в ASP.NET.
.NET Interop на примере работы с сокетами
Как скрестить ежа с ужом: используем P/Invoke и DllImport для работы с нативными Windows Sockets из .NET-приложения.
Код всегда ничего не стоил
Сорок лет мы делали вид, что софт — это товар. SaaS сдвинул цену на сервис. ИИ сдвинул её ниже нуля. Артефакт бесплатен. Платят теперь за понимание, какой именно код стоит писать.