Одним из основных преимуществ использования Object-Relational Mapper (ORM) является возможность взаимодействия и запросов к стандартной реляционной базе данных, такой как MSSQL, при использовании объектно-ориентированных парадигм в бэкенде. В C# .NET Core самым популярным ORM на сегодняшний день является Entity Framework Core, который позволяет выполнять простые операции CRUD с высокой производительностью. Тем не менее, одна проблема, с которой я недавно столкнулся, заключалась в том, как сопоставить шаблон иерархии классов с реляционной базой данных?

На самом деле существует несколько подходов к представлению паттернов наследования. Это включает в себя:

  • Таблица на бетон (TPC)
  • Таблица по типу (TPT)
  • Таблица на иерархию (TPH)

В чем разница между каждым? Как узнать, какой из них выбрать? И как каждый подход влияет на базовую реляционную базу данных?

Присоединяйтесь ко мне, когда мы обсудим внутреннюю работу, плюсы и минусы, включая варианты использования, для каждого подхода. Затем присоединяйтесь ко мне во второй части, где мы разработаем некоторые базовые операции CRUD с использованием шаблона TPH и увидим их в действии!

Наш мотивирующий пример

Давайте сначала рассмотрим следующую иерархию классов, используемую в кабинете врача для отслеживания людей в их системе. Каждый Person имеет имя и фамилию и связанную строку Id для их уникальной идентификации. В зависимости от нашего варианта использования класс Person также может быть помечен как abstract, если все лица в нашей системе относятся либо к Doctor, либо к Patient.

Кроме того, класс Doctor наследуется от Person и хранит идентификатор регистрации врача в местном руководящем медицинском органе. Вместо этого для каждого Patient мы сохраняем количество раз, которое они ранее посещали в нашей медицинской практике.

Теперь давайте посмотрим, как мы будем представлять этот шаблон наследования при каждом подходе.

Таблица на бетон (TPC)

Подход «таблица по концентрату» является наиболее простым для понимания и может быть резюмирован следующим образом:

Для каждого конкретного класса (не абстрактного) используется ровно одна таблица, так что все свойства, включая унаследованные, сопоставляются с одним отдельным столбцом в СУБД.

В приведенном выше примере это означает, что у нас будет три отдельные таблицы в нашей базе данных, по одной для классов Person, Doctor и Patient. Это по существу устраняет отношения наследования между классами и рассматривает каждый из них как отдельный объект.

Преимущества использования шаблона TPC для моделирования нашей иерархии включают:

  • Простота понимания. Шаблон TPC кажется интуитивно понятным и более простым для понимания, чем другие методы. Обращение к каждому объекту как к отдельной сущности позволяет использовать стандартные шаблоны с минимальными трудностями для программиста.
  • Высокая производительность для неполиморфных запросов.Представление каждого объекта в нашей иерархии одной таблицей позволяет быстро выполнять операторы SQL. Обратите внимание, что это относится только к неполиморфным запросам, то есть напрямую к подклассам, а не к его базовому классу.
  • Соответствует третьей нормальной форме (3NF):Преимущество использования TPC заключается в том, что ваша база данных соответствует 3NF, а данные нормализованы, что снижает вероятность логических несоответствий.

Как и в любом другом случае, существует множество недостатков, о которых следует помнить:

  • Большое количество сгенерированных таблиц: поскольку мы создаем уникальную таблицу для каждого конкретного класса, существующего в нашей иерархии, количество таблиц может резко увеличиться в зависимости от количества классов, которые у нас есть. Больше конкретных классов = больше таблиц!
  • Изменения в базовых классах сложны:любые изменения в базовом классе должны распространяться на все связанные таблицы во всех производных классах. Это означает, что простое изменение базового класса, например добавление нового поля, приводит к сложной миграции, влияющей на несколько таблиц.
  • Полиморфные запросы выполняются медленно.Поскольку каждый производный класс имеет свою собственную таблицу, полиморфный запрос из базового класса должен полагаться на UNION операторов SQL для получения необходимых данных. Это создает большой и неэффективный SQL-запрос.

В общем, TPC лучше всего подходит для иерархий, которые хорошо определены заранее, когда маловероятно, что базовый класс изменится. Кроме того, TPC следует использовать, когда иерархия не слишком сложна, а полиморфные запросы маловероятны.

Таблица по типу (TPT)

Шаблон таблицы для каждого типа аналогичен подходу TPC, но с существенным отличием:

Для каждого класса, абстрактного или иного, используется отдельная таблица, а столбцы для подклассов содержат только те свойства, которые не унаследованы от базового класса. Первичный ключ для каждого подкласса — это внешний ключ таблицы базового класса.

Давайте посмотрим, как это будет выглядеть с нашей иерархией выше. Обратите внимание, что у нас снова есть три таблицы для каждого из наших классов. Однако на этот раз столбцы в каждой таблице немного отличаются от предыдущих. Только свойства, которых не было в базовом классе Person, появляются в столбцах таблиц производных типов. Для класса Doctor это свойство RegistrationId, а для класса Patient это свойство PreviousVisits. Мы также отмечаем, что первичный ключ для каждого подкласса фактически является внешним ключом для базового базового класса.

Итак, каковы основные преимущества использования конфигурации TPT?

  • Ограниченная избыточность данных. Производимые таблицы для подклассов содержат только столбцы, отсутствующие в базовом классе, что приводит к созданию более простых таблиц.
  • Эволюция модели проста: изменение, добавление и удаление классов из нашей иерархии более просто с конфигурацией TPT. Изменения в классе влияют только на соответствующую таблицу, что означает, что миграция упрощается и ее легче выполнять. Например, добавление нового свойства в класс Person требует только добавления нового столбца в таблицу Person и не влияет на другие производные объекты.
  • Соответствует третьей нормальной форме (3NF). Как и в случае с TPC, использование конфигурации TPT также соответствует 3NF и гарантирует, что ваша база данных нормализована.

Есть и некоторые недостатки, которые могут повлиять на наш вариант использования:

  • Большое количество сгенерированных таблиц: поскольку TPT требует соответствующей таблицы для всех классов, количество сгенерированных таблиц больше или равно при использовании подхода TPC.
  • Низкая производительность запросов. Операторы SQL, сгенерированные из запросов с использованием конфигурации TPT, используют INNER JOIN и становятся все более сложными в зависимости от количества уровней в иерархии классов. Это связано с использованием внешних ключей в качестве первичного ключа подклассов. Чтобы получить все необходимые свойства, мы должны INNER JOIN выполнить INNER JOIN по всем родительским объектам. В многоуровневых иерархиях эта проблема усугубляется. Кроме того, конфигурация TPT значительно усложняет создание любого рукописного SQL.
  • Специальные отчеты сложны:поскольку свойства распределены по разным уровням иерархии наследования, специальные отчеты становятся более сложными. Это можно несколько облегчить за счет использования представления базы данных, но распределенная структура затрудняет построение базового запроса.

Учитывая все плюсы и минусы, подход TPT лучше всего подходит для иерархий, которые используют полиморфные запросы и имеют подклассы с множеством различных свойств. Это использует сильные стороны TPT в сохранении только неунаследованных свойств в соответствующих таблицах. Однако для более сложных многоуровневых структур наследования, где важна производительность, TPT менее подходит.

Таблица на иерархию (TPH)

И последнее, но не менее важное — это шаблон таблицы для каждой иерархии, который явно отличается как от TPC, так и от TPT:

Вся иерархия сопоставляется с одной таблицей, столбцы которой содержат все свойства как базового класса, так и любых производных классов. Столбец дискриминатор используется для определения того, к какому конкретному подклассу принадлежит каждая строка.

Учитывая наш пример выше, использование конфигурации TPH создаст одну таблицу SQL, содержащую 5 явных столбцов: Id, FirstName, LastName, RegistrationId и PreviousVisits. Обратите внимание, что поля RegistrationId и PreviousVisits являются свойствами производных классов, но они все еще отображаются в созданной таблице. Кроме того, создается неявный столбец Discriminator, чтобы отслеживать, к какому подклассу относится каждый объект. Этот столбец дискриминатора можно настроить для сопоставления с определенным свойством, если мы также пожелаем.

К основным сильным сторонам использования TPH относятся:

  • Быстрые полиморфные и неполиморфные запросы.Одно из основных преимуществ TPH заключается в том, что SQL-запросы выполняются быстро — намного быстрее, чем с TPC или TPT. Поскольку вся информация хранится в таблице и контролируется столбцом Discriminator, полиморфные запросы не требуют сложных операций INNER JOIN или UNION. Это позволяет максимально эффективно использовать концепции ООП посредством высокопроизводительных полиморфных запросов к базовому классу.
  • Минимальное количество созданных таблиц. По сравнению с другими подходами, которые создают несколько таблиц в зависимости от ширины и глубины иерархии, TPH создает только одну таблицу — независимо от сложности наследования. Это также приводит к простой для понимания таблице, которая поддается специальной отчетности.
  • Простая эволюция схемы: добавлять и удалять свойства так же просто, как добавлять и удалять столбцы в таблице. Это делает частые изменения в иерархии совершенно безболезненными. Кроме того, общие свойства в подклассах могут даже иметь один и тот же столбец, если их тип одинаков!

Конечно, есть и соответствующие слабые стороны использования TPH:

  • Нестандартизированная база данных. В отличие от двух других подходов, TPH приводит к созданию базы данных, которая не подчиняется 3NF. Нарушение происходит из-за свойств, зависящих от ключа Discriminator, что приводит к функциональной зависимости от атрибута непервичного ключа. Это приносит в жертву долговременную стабильность, и вопрос о том, является ли это проблемой, следует оценивать в каждом конкретном случае и обязательно проконсультироваться с вашим администратором баз данных!
  • Столбцы, допускающие значение NULL. Свойства подкласса приводят к появлению столбцов, допускающих значение NULL в создаваемой таблице. Это связано с тем, что другие классы не содержат это свойство, а это означает, что запись должна быть NULL, поскольку она не существует. Например, класс Patient не обладает свойством RegistrationId, поэтому все записи в этом столбце должны иметь значение NULL. Это может создать проблему для целостности данных, если у вас есть свойства, для которых вы хотите установить ограничение NOT NULL.
  • Таблицы могут быстро расти из-за неиспользуемого пространства.Если подклассы содержат большое количество свойств, таблицы могут вырасти и содержать большое количество столбцов. Кроме того, многие из этих столбцов могут быть заполнены NULL записями, что приводит к большому объему неиспользуемого пространства в таблице.

Принимая это во внимание, TPH хорошо подходит для ситуаций, когда скорость запросов является неотъемлемой частью приложения, а подклассы не содержат большого количества уникальных свойств. В ситуациях, когда стандартизация и целостность данных имеют первостепенное значение, может быть лучше избегать TPH и выбрать один из других методов.

Заключительные замечания

Теперь, когда мы знакомы с каждым из подходов к отображению наследования в C#, попробуйте их в своем следующем проекте!

Хотите увидеть его в действии? Нажмите, чтобы прочитать часть 2, в которой мы рассмотрим реализацию простых операций CRUD с использованием подхода TPH!

P.S. Хорошо, я немного солгал, вторая часть все еще находится в разработке, хе-хе, но следите за мной на Medium, чтобы получать уведомления, когда она будет опубликована!

Понравилось читать эту историю?

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

Подпишитесь на меня на Medium, чтобы быть в курсе, когда я напишу следующую статью.
Я регулярно пишу статьи о технологиях и жизни в целом.