Устранение того факта, что первичные ключи не являются частью вашего бизнеса


25

Практически во всех случаях первичные ключи не являются частью вашего бизнеса. Конечно, у вас могут быть некоторые важные объекты, с которыми сталкиваются пользователи, с уникальными индексами ( UserNameдля пользователей или OrderNumberдля заказов), но в большинстве случаев нет никакой необходимости явно идентифицировать доменные объекты по одному значению или набору значений кому-либо, кроме, возможно, административный пользователь. Даже в этих исключительных случаях, особенно если вы используете глобальные уникальные идентификаторы (GUID) , вам больше понравится или вы захотите использовать альтернативный ключ, чем раскрывать сам первичный ключ.

Таким образом, если мое понимание доменного дизайна является точным, первичные ключи не должны и, следовательно, не должны быть раскрыты, и хорошее избавление. Они безобразны и ограничивают мой стиль. Но если мы решим не включать первичные ключи в модель предметной области, это приведет к следующим последствиям:

  1. Наивно, объекты передачи данных (DTO), которые происходят исключительно из комбинаций моделей предметной области, не будут иметь первичных ключей
  2. Входящие DTO не будут иметь первичного ключа

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

Иными словами, какое из следующих решений является правильным подходом к идентификации конкретных объектов после удаления PK в моделях доменов?

  1. Возможность идентифицировать объекты, с которыми вам нужно иметь дело, с помощью других атрибутов.
  2. Получение первичного ключа обратно в DTO; то есть, устранение PK при отображении из постоянства в домен, а затем рекомбинирование PK при отображении из домена в DTO?

РЕДАКТИРОВАТЬ: Давайте сделаем это конкретным.

Скажем , моя модель домена , VoIPProviderкоторый включает в себя такие области , как Name, Description, URL, а также ссылки нравится ProviderType, PhysicalAddressи Transactions.

Теперь предположим, что я хочу создать веб-сервис, который позволит привилегированным пользователям управлять VoIPProviders.

Возможно, в этом случае удобный идентификатор пользователя бесполезен; В конце концов, поставщики VoIP - это компании, чьи имена, как правило, различны в компьютерном смысле и даже достаточно различны в человеческом смысле по деловым причинам. Поэтому может быть достаточно сказать, что уникальность VoIPProviderполностью определяется (Name, URL). Итак, теперь давайте скажем, что мне нужен метод, PUT api/providers/voipчтобы привилегированные пользователи могли обновлять VoIPпоставщиков. Они отправляют VoIPProviderDTO, который включает в себя многие, но не все поля из VoIPProvider, в том числе потенциально некоторые. Тем не менее, я не могу читать их мысли, и они все еще должны сказать мне, о каком поставщике мы говорим.

Кажется, у меня есть 2 (возможно, 3) варианта:

  1. Включите первичный ключ или альтернативный ключ в мою модель домена и отправьте его в DTO, и наоборот
  2. Определите поставщика, о котором мы заботимся, с помощью уникального индекса, например: (Name, Url)
  3. Представьте некоторый промежуточный объект, который всегда может отображаться между постоянным уровнем, доменом и DTO таким образом, который не раскрывает подробности реализации о постоянном уровне - скажем, путем введения временного идентификатора в памяти при переходе от домена к DTO и обратно,

1
Укажите на размышление: часто, когда используются суррогатные ПК, когда существует хороший бизнес-ключ, общение с экспертами в области становится бедным. Похоже, мы в конечном итоге работаем на платформе ORM, а не наоборот.
Тулаинс Кордова

@ user61852 хорошо, независимо от ORM, даже если вы действительно низкого уровня, вам все еще нужен первичный ключ в реализации уровня базы данных. Поэтому я согласен, что суррогатное PK дает вам преимущества по сравнению с фактическим PK, используемым конкретным механизмом сохранения, но если этот PK действительно представляет значимый бизнес-объект, он обязательно уникален и поэтому имеет по крайней мере одно уникальное свойство, связанное с бизнесом, определяющее это нет?
tacos_tacos_tacos

1
Все преимущества суррогатов связаны с компьютером и не связаны с человеком.
Тулаинс Кордова

2
@ user61852: Я согласен на 100% (я написал что-то другое?). Для средств связи используйте «бизнес-ключ». Добавьте уникальные ограничения для любого бизнес-ключа. Но избегайте использования бизнес-ключей для реализации ваших ссылок на базу данных.
Док Браун

2
бизнес-ключи вечно уникальны - пока их нет. Если вы используете бизнес-ключи в качестве основных, когда это происходит, то изменение бизнес-правил ломает больше вещей.
PSR

Ответы:


31

Вот как мы решаем эту проблему (более 15 лет, когда даже термин «дизайн, управляемый доменом» не был изобретен):

  • при отображении модели предметной области в реализацию базы данных или модели классов на конкретном языке программирования у вас есть простое согласованное правило, например, «для каждого объекта предметной области, сопоставленного с реляционной таблицей, первичным ключом является« TablenameID ».
  • этот первичный ключ является полностью искусственным, он всегда имеет один и тот же тип и не имеет делового значения - просто суррогатный ключ
  • «графическая версия» модели вашего домена (та, которую вы используете для общения с экспертами своего домена) не содержит первичных ключей. Вы не предоставляете их непосредственно экспертам (но вы предоставляете их любому, кто фактически внедряет код для системы).

Поэтому, когда вам нужен первичный ключ для технических целей (например, для сопоставления отношений с базой данных), у вас есть один доступный, но если вы не хотите «видеть его», измените уровень абстракции на «модель экспертов домена». ». И вам не нужно поддерживать «две модели» (одна с ПК и одна без); вместо этого поддерживайте только модель без PK и используйте генератор кода для создания DDL для вашей БД, который автоматически добавляет PK в соответствии с правилами отображения.

Обратите внимание, что это не запрещает добавлять любые «бизнес-ключи», такие как дополнительный «OrderNumber», кроме суррогата OrderID. Технически эти бизнес-ключи становятся альтернативными ключами при сопоставлении с вашей базой данных. Просто избегайте их использования для создания ссылок на другие таблицы, всегда предпочитайте использовать суррогатные ключи, если это возможно, это будет намного проще.

Ваш комментарий: использование суррогатного ключа для идентификации записей - это не бизнес, а чисто техническая операция. Чтобы прояснить это, посмотрите на ваш пример: пока вы не определите дополнительные уникальные ограничения, можно будет иметь два объекта VoIPProvider с одинаковой комбинацией (name, url), но разными VoIPProviderID.


Как насчет того, чтобы взять объект домена из возвращенного объекта персистентности (сущность, модель строки таблицы или что-то еще), перейти в DTO, вернуть его обратно и вернуться к постоянству? Это просто сделано с помощью суррогатного ключа (то есть бизнес-ориентированного определения уникальности), который требует разрешения для каждой персистентной операции?
tacos_tacos_tacos

1
@tacos_tacos_tacos: давайте придерживаться вашего примера VoIPProvider. Я бы на самом деле добавил «VoIPProviderID» в ваш DTO, по крайней мере, на «стороне реализации» (если у вас есть также графическая версия для ваших экспертов по доменам, я бы, вероятно, не показывал ее там). В целях обновления стандартным способом идентификации конкретного VoIPProvider должен быть «VoIPProviderID», который вы получили при извлечении данных из базы данных. Если пользователи вашего API предпочитают идентификацию по (имени, URL), укажите это дополнительно. ...
Док Браун

... и если производительность кажется реальной, измеримой проблемой, вы можете также рассмотреть возможность кэширования сопоставления (name, url) с VoIPProviderID где-нибудь. Но я бы не рекомендовал осуществлять такую ​​оптимизацию заранее, преждевременно.
Док Браун

1
Натуральные ключи иногда кажутся действительно убедительными, но их слишком легко сжечь (например, «упс, теперь у меня несколько арендаторов, и это уже не уникально»).
Кейси

1
@corsiKa: в контексте того, что спросил OP, я настоятельно рекомендовал бы иметь чисто автоматически сгенерированный ключ "OrderID" (который не печатается в квитанции, а просто используется для внутренних вещей, таких как ссылки на базу данных), и отдельный бизнес-ключ «OrderNumber» (который может, например, содержать что-то вроде текущего года, который может использоваться для сортировки и фильтрации, который впоследствии может быть изменен / исправлен и который может быть напечатан на квитанциях). ОП запросил «доменно-управляемый дизайн», «OrderNumber» является частью модели домена, в то время как «OrderID» является лишь деталью реализации.
Док Браун

4

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

Доступны уникальные индексы, так что вы можете дополнительно ограничить свою схему БД, а не в качестве оптовой замены ПК. Если вы не выставляете PK, потому что они уродливы, но вместо этого выдают уникальный ключ ... на самом деле вы не делаете ничего другого. (Я полагаю, вы не перепутали PK и столбец идентификации?)


Когда я хочу выполнить операцию, включающую определенный экземпляр объекта домена, который сохраняется, мне нужно иметь возможность включать в DTO либо явно (через некоторый вид ключа, либо первичный, либо альтернативный удобный идентификатор пользователя, который является уникальным). к этой таблице и может также быть индексом в таблице) или неявно (через некоторую комбинацию полей, значения которых однозначно идентифицируют конкретную запись) ... и, кроме того, в практическом смысле мне нужно будет отправить в DTO какая-то форма отслеживания изменений, если бы я сделал это другим способом (OriginalVal против NewVal для всех полей, которые идентифицируют запись), нет?
tacos_tacos_tacos

не является ли явный v неявный вопрос той же разницей? Вы можете иметь PK, который охватывает несколько столбцов, как уникальный индекс. Я не вижу никакой разницы между ними для ваших целей.
gbjbaanb

Конечно, мы могли бы иметь PK на нескольких столбцах, например. Но для меня это утечка информации о базе данных (хранилище), которая не должна иметь ничего общего с сердцем и душой, бизнес-сущностью. Если какой-то кортеж полей бизнес-сущности охватывает ПК для БД, то это здорово. Но это не обязательно должно быть наоборот, не так ли?
tacos_tacos_tacos

Вы переосмысливаете это. Уникальный индекс является таким же артефактом схемы БД, как и PK. Подумайте об этом так: PK - это только первый (или основной) уникальный индекс. это «особый», потому что вам нужен только один такой индекс.
gbjbaanb

Да, но любой значимый предметный объект должен быть строго идентифицирован по крайней мере в одной из его областей, связанных с бизнесом, не так ли? Тот факт, что это определяет индекс в БД, больше важен для производительности, чем для простого запроса к БД ... Я бы предпочел PK с одним столбцом, а не уникальный индекс с 6 столбцами, и они действительно служат различным целям - PK (или индекс с небольшим количеством полей) также доступен для удобства DBA / DBD, верно?
tacos_tacos_tacos

4

Без первичных ключей во внешнем интерфейсе нет простого способа узнать, что вы отправляете. Чтобы это исправить, вам понадобится куча дополнительной работы по анализу данных, что снизит производительность и, вероятно, займет больше времени и будет более уродливым, чем прикрепление ключа к каждому элементу.

В качестве примера скажем, что я хочу отредактировать сообщение в приложении; как приложение узнает, какое сообщение я хочу отредактировать, не подключив первичный ключ? Редактирование объектов происходит постоянно, и делать это без ключей практически невозможно. Но если у вас есть объекты, которые не должны редактироваться, пропустите ключ, если вы считаете, что он отвлекает, но наличие первичных ключей может повысить производительность.


Ну, сообщение крайний пример, но даже тогда мы знали бы MessageSender, MessageRecipient, TimeSent- это должно быть уникальным.
tacos_tacos_tacos

1
@tacos_tacos_tacos, тогда как вы создаете FK для других таблиц? Это должен быть MessageSenderId, который, вероятно, сопоставляется с таблицей Users в UserId. Вы не хотели бы использовать имя пользователя в качестве ключа между таблицами, поскольку это может измениться и стать кошмаром обслуживания. Вот почему вы обычно объединяете таблицы только с использованием первичных ключей, а не другого столбца (есть, конечно, исключения). Эта структура БД все еще должна быть обязательной. Теперь вы всегда можете перейти к модели CQRS для своего приложения ... в этом случае правила изменятся. Особенно, если вы также используете Event Sourcing.
CaffGeek

4

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

Я вижу, вы ответили с комментарием: MessageSender, MessageRecipient, TimeSent (для сообщения). Вы можете ВСЕГДА иметь двусмысленность таким образом (например, сгенерированными системой сообщениями о том, что происходит часто). И как вы собираетесь проверить MessageSender и MessageRecipient здесь? Предположим, вы проверяете их, используя FirstName, Lastname, DateOfBirth, и в конечном итоге вы столкнетесь с ситуацией, когда у вас будет 2 человека, рожденных в один и тот же день с одинаковым именем. Не говоря уже о том, что вы столкнетесь с ситуацией, когда у вас есть сообщение с именем tacostacostacos-America-1980-Doc Brown-France-1965-23/5/2014-11:43:54.003UTC+200. Это монстр имени, и у вас все еще нет гарантии, что у вас будет только 1 из них.

причина, по которой мы используем первичный ключ, заключается в том, что мы ЗНАЕМ, что он будет уникальным в течение всего жизненного цикла программного обеспечения, независимо от того, какие данные вводятся, и мы ЗНАЕМ, что это будет предсказуемый формат (что произойдет, если ваш вышеупомянутый ключ имеет тире в имени пользователя - вся ваша система идет в дерьмо).

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

Другая причина, по которой PK так полезен, - это то, что вы можете сделать из вышеизложенного: PK делает это так, что вам не нужно заставлять компьютер интерпретировать сгенерированный пользователем код. Если китайский пользователь использует ваш код и вводит несколько китайских символов, ваш код внезапно не должен иметь возможность работать с ними внутренне, но он может просто использовать Guid, сгенерированный системой. Если у вас есть арабский пользователь, который вводит арабский язык, ваша система не должна справляться с этим внутренне, но может игнорировать то, что он там есть.

Как уже говорили другие, Guid - это то, что может храниться внутри в фиксированном размере. Вы знаете, с чем работаете, и это можно использовать повсеместно. Вам не нужно создавать правила оформления того, как вы создаете определенный идентификатор и сохраняете его. Если ваша система берет только первые 10 букв имени, она не видит никакой разницы между Михаэлем Гуггенхаймером и Михаэлем Гугштайном, и это запутает эти 2. Если вы отрежете это произвольно, вы можете запутаться. Если вы ограничите пользовательский ввод, вы можете столкнуться с проблемами с пользовательскими ограничениями.

Когда я смотрю на существующие системы, такие как Dynamics CRM, они также используют внутренний ключ (PK), чтобы пользователь вызывал одну запись. Если у пользователя есть запрос, не связанный с идентификатором, он возвращает массив возможных ответов и позволяет пользователю выбрать его. Если есть вероятность неопределенности, они предоставят выбор пользователю.

Наконец, это также недостаток безопасности через неизвестность. Если вы не знаете идентификатор записи, единственный вариант - угадать его. если идентификатор легко угадать (поскольку информация, из которой он сделан, общедоступна), любой может его изменить. Вы даже можете побудить пользователя изменить его с помощью классических методов CSRF или XSS. Теперь, очевидно, ваша безопасность должна уже учитывать и снижать безопасность, прежде чем когда-либо публиковать живую версию, но вы все равно должны усложнить возможность потенциального злоупотребления.


1

При выдаче идентификатора для внешней системы вы должны указывать только URI или, альтернативно, ключ или набор ключей, которые имеют те же свойства, что и URI, вместо того, чтобы предоставлять первичный ключ базы данных напрямую (далее я буду ссылаться на и URI, или ключ, или набор ключей, которые имеют те же свойства, что и URI, что и просто URI, другими словами, приведенный ниже URI не обязательно означает URI RFC 3986).

URI может содержать, а может и не содержать первичный ключ объекта, и может фактически состоять из альтернативных ключей, а может и не состоять. Это не имеет значения, Важно то, что только системе, которая генерирует URI, разрешается разделять или объединять URI, чтобы сформировать понимание того, что является упомянутым объектом. Внешние системы всегда должны использовать URI в качестве непрозрачного идентификатора. Не имеет значения, может ли пользователь-человек определить, что одна часть URI на самом деле является суррогатным ключом базы данных или что он состоит из нескольких бизнес-ключей, сгруппированных вместе, или что это фактически base64 из этих значений. Это не имеет значения. Важно то, что внешняя система не обязана понимать, что означает идентификатор, чтобы использовать идентификатор. Внешние системы никогда не должны анализировать компоненты внутри идентификатора или объединять идентификатор с другими идентификаторами для ссылки на что-то в вашей системе.

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

Вернемся к вашему примеру с VoIP. Предположим, что поставщик VoIP может быть однозначно определен либо (VoIPProviderID), либо (имя, URL), либо (GUID). Когда внешней системе необходимо обновить провайдера VoIP, она может просто передать PUT / provider / by-id / 1234 или PUT /provider/foo-voip/bar-domain.comили, PUT /3F2504E0-4F89-41D3-9A0C-0305E82C3301и ваша система поймет, что внешняя система хочет обновить VoIPProvider. Эти URI генерируются вашей системой, и только ваша система должна понимать, что все они означают одно и то же. Внешняя система должна просто обрабатывать все, что находится в URI, в основном как PUT <whatever>.

Предположим, что у вас есть данные для разных поставщиков VoIP, которые хранятся в разных таблицах с разными схемами (таким образом, совершенно разные наборы ключей идентифицируют каждого поставщика VoIP на основе таблицы, в которой они хранятся). Если у вас есть URI, он может быть доступен для внешней системы единообразно, независимо от того, как ваша система идентифицирует конкретного поставщика VoIP. Для внешней системы это все просто непрозрачный указатель.

Когда ваша система использует URI для ссылки на объекты таким образом, вы ничего не пропускаете в отношении того, как вы реализуете свою систему. Вы генерируете URI, и клиент просто передает его вам.


0

Я собираюсь нацелиться на это дико неточное и наивное утверждение:

Возможно, в этом случае удобный идентификатор пользователя бесполезен; В конце концов, поставщики VoIP - это компании, чьи имена, как правило, различны в компьютерном смысле и даже достаточно различны в человеческом смысле по деловым причинам.

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

Затем мы переходим к тому факту, что названия компаний не являются даже отдаленно уникальными, о чем свидетельствует ориентир Apple против Apple .

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

Для справки, я предпочитаю, как django справляется с этим:

class VoipProvider(models.Model):
    name=fields.TextField()
    address=fields.TextField()

class Customer(models.Model):
    name=fields.TextField()
    address=fields.TextField()
    voipProvider=fields.ForeignKeyField(VoipProvider)

Таким образом, сведения о поставщике клиента могут быть доступны в коде с помощью:

myCustomer.voipProvider.name #returns the name of the customers VOIP Provider.

Хотя первичные / внешние ключи не видны, они есть и могут использоваться для доступа к элементам, но они абстрагированы.


Вы абсолютно правы, но я предполагаю, что имел в виду, что «в некоторых доменах, возможно, существует естественный набор значений, которые всегда уникальны». Если они изменяются, но они по-прежнему уникальны, они по-прежнему идентифицируют одну запись.
tacos_tacos_tacos

0

Я думаю, что мы часто все еще неправильно рассматриваем эту проблему с точки зрения БД: о, нет естественного ключа, поэтому нам нужно создать суррогатный ключ. О нет, мы не можем выставить суррогатный ключ обратно в доменные объекты, это утечка и т. Д.

Однако иногда лучше придерживаться следующего: если у бизнес-объекта (домена) нет естественного ключа, то, возможно, ему следует дать его. Это двойная проблема бизнес-сферы: во-первых, вещи нуждаются в идентичности, даже в отсутствие баз данных. Во-вторых, хотя мы пытаемся сделать вид, что настойчивость - это некая абстрактная идея, невидимая для области, реальность такова, что настойчивость все еще остается бизнес-концепцией. Очевидно, что существуют проблемы, когда выбранный естественный ключ не поддерживается БД в качестве первичного ключа (например, GUID в некоторых системах) - в этом случае вам потребуется добавить суррогатный ключ.

Таким образом, вы попадаете в очень похожее место, например, у вашего клиента есть целочисленный идентификатор, но вместо того, чтобы чувствовать себя плохо из-за того, что он просочился из БД в домен, вы чувствуете себя счастливым, потому что бизнес согласился, что все клиенты должны быть назначены ID, и вы сохраняете это в БД, как вы должны. Вы все еще можете ввести суррогат, например, для поддержки переименования идентификатора клиента.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.