6 мин

Наследование в 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, сейчас схема была немного усложнена:

  1. Я создал таблицу BlogPost. Смысл этого в том, что в моей системе Post будет являться не только постом блога, но и комментарием, сообщением и вообще всем, что один пользователь может передать другому. Такая схема сделана исключительно для целей обучения, потому что в большинстве систем комментарии — это очень критичная таблица, в которой постоянно много записей и которая является самой нагруженной таблицей в системе. Соответственно, BlogPost должен унаследовать все данные из Post и нести в себе дополнительную информацию. Я хочу, чтобы в моей системе под каждым постом блога пользователи сами писали название ссылки на комментарии. Например: «Жгут здесь», «Об этом думают». Это внесёт некое разнообразие. Тут я применю первый тип наследования — каждая Entity имеет свою таблицу (Table per Type).

  2. Также мне было бы удобно, чтобы в моей модели БД были и простые 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 не самым зверским образом урезал свои продукты.

Читать дальше

Похожие посты