DDD: идентификатор объекта перед сохранением

В доменно-ориентированном дизайне одной из определяющих характеристик сущности является ее идентичность.

Проблема:

Я не могу предоставить сущностям уникальный идентификатор при создании экземпляра. Этот идентификатор предоставляется репозиторием только после того, как объект сохраняется (это значение предоставляется из базовой базы данных).

Я не могу начать использовать значения Guid на этом этапе. Существующие данные хранятся с int значениями первичного ключа, и я не могу сгенерировать уникальный int при создании экземпляра.

Мое решение:

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

Код (абстрактный базовый класс для всех сущностей):

public abstract class Entity<IdType>
{
    private readonly IdType uniqueId;

    public IdType Id
    {
        get 
        { 
            return uniqueId; 
        }
    }

    public Entity()
    {
        uniqueId = default(IdType);
    }

    public Entity(IdType id)
    {
        if (object.Equals(id, default(IdType)))
        {
            throw new ArgumentException("The Id of a Domain Model cannot be the default value");
        }

        uniqueId = id;
    }

    public override bool Equals(object obj)
    {
        if (uniqueId.Equals(default(IdType)))
        { 
            var entity = obj as Entity<IdType>;

            if (entity != null)
            {
                return uniqueId.Equals(entity.Id);
            }
        }

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return uniqueId.GetHashCode();
    }
}

Вопрос:

  • Считаете ли вы это хорошей альтернативой генерации значений Guid при создании экземпляра?
  • Есть ли лучшие решения этой проблемы?

person Dave New    schedule 21.01.2014    source источник
comment
Какую базу данных вы используете? Если вы используете RavenDB или NHibernate, вы можете воспользоваться шаблоном HiLo, который позволяет заранее получить идентификатор, прежде чем сохранять объект в базе данных.   -  person Adrian Thompson Phillips    schedule 21.01.2014
comment
Entity Framework 6 поверх базы данных SQL Azure. Я не верю, что можно получить идентификатор перед тем, как что-то вставить.   -  person Dave New    schedule 21.01.2014
comment
Зачем вам нужен идентификатор до того, как сущность будет сохранена, и почему вы должны полагаться на значения, предоставленные БД? Сообщите нам более подробную информацию о вашем домене, потому что возможно, ваши предположения ошибочны.   -  person Bartłomiej Szypelow    schedule 21.01.2014
comment
@ BartłomiejSzypelow: Потому что сущности должны иметь идентичность, иногда даже с точки зрения создания. Я мог бы просто положиться на ссылку, но я работаю над распределенной системой, поэтому идентичность объекта должна быть уточнена идентификатором (поскольку будет иметь место сериализация). Я пытаюсь добиться ранней генерации идентичности (посмотрите книгу Вона Вернона: «Реализация дизайна, основанного на предметной области»).   -  person Dave New    schedule 22.01.2014
comment
Вы сказали, что раздали. Еще одна причина для GUID. Как вы упомянули IDDD, есть также глава о взломе с использованием таблицы, имитирующей SEQUENCE Oracle для ранней генерации идентификаторов. Мне не нравится такой подход, но как насчет вас?   -  person Bartłomiej Szypelow    schedule 22.01.2014
comment
Мне очень любопытно: как вам удалось разрешить вашему репо устанавливать частное поле uniqueId после вставки? И как ваше репо обращается к этому полю при обновлении, чтобы знать, какую строку обновлять?   -  person Timo    schedule 28.06.2018


Ответы (5)


Я не могу предоставить сущностям уникальный идентификатор при создании экземпляра. Этот идентификатор предоставляется репозиторием только после того, как объект сохраняется (это значение предоставляется из базовой базы данных).

Сколько у вас мест, где вы создаете список объектов одного типа, и у вас есть несколько объектов с идентификатором по умолчанию?

Считаете ли вы это хорошей альтернативой генерации значений Guid при создании экземпляра?

Если вы не используете ORM, ваш подход достаточно хорош. Особенно при реализации карты идентификации и единица работы - это ваша ответственность. Но вы исправили только Equals(object obj). GetHashCode() метод не проверяет наличие uniqueId.Equals(default(IdType)).

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

Я привык писать собственные реализации Equals() для сущностей предметной области, но это может быть излишним, когда дело доходит до использования ORM. Если вы используете ORM, он предоставляет реализации идентификационной карты и unit of work готовые шаблоны, и вы можете на них положиться.

person Ilya Palkin    schedule 21.01.2014

Вы можете использовать генератор последовательности для генерации уникальных _1 _ / _ 2_ идентификаторов при создании экземпляра объекта сущности.

Интерфейс выглядит так:

interface SequenceGenerator {
    long getNextSequence();
}

Типичная реализация генератора последовательностей использует таблицу последовательностей в базе данных. Таблица последовательности содержит два столбца: sequenceName и allocatedSequence.

Когда getNextSequence вызывается в первый раз, он записывает большое значение (скажем, 100) в столбец allocatedSequence и возвращает 1. Следующий вызов вернет 2 без необходимости доступа к базе данных. Когда последовательность 100 заканчивается, она считывает и снова увеличивает allocatedSequence на 100.

Взгляните на SequenceHiLoGenerator в исходном коде Hibernate. В основном он делает то, что я описал выше.

person nwang0    schedule 21.01.2014

Я считаю, что решение на самом деле довольно простое:

  • Как вы упомянули, сущности должны иметь личность,

  • В соответствии с вашими (совершенно действительными) требованиями идентичность ваших сущностей назначается централизованно СУБД,

  • Следовательно, любой объект, которому еще не назначен идентификатор, не сущность.

Здесь вы имеете дело с типом объекта передачи данных, который не имеет идентичности. Вы должны думать об этом как о передаче данных из любой системы ввода, которую вы используете, в модель предметной области через репозиторий (который вам нужен здесь в качестве интерфейса для присвоения идентификаторов). Я предлагаю вам создать другой тип для этих объектов (тот, у которого нет ключа) и передать его методу Add / Create / Insert / New вашего репозитория.

Когда данные не нуждаются в особой предварительной обработке (т.е. не нуждаются в большой передаче), некоторые люди даже пропускают DTO и передают различные фрагменты данных напрямую через аргументы метода. Вот как вы должны смотреть на такие DTO: как на удобные объекты аргументов. Опять же, обратите внимание на отсутствие аргумента «ключ» или «идентификатор».

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

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

Вы, несомненно, будете беспокоиться о повторном использовании кода. В зависимости от того, как вы создаете свои объекты, вы, вероятно, захотите исключить некоторую логику проверки, чтобы репозиторий мог проверять данные перед их вставкой в ​​базу данных. Обратите внимание, что обычно в этом нет необходимости, если вы используете последовательности СУБД, и может быть причиной того, что некоторые люди систематически используют их, даже если они им не нужны. В зависимости от ваших требований к производительности примите во внимание приведенные выше комментарии, поскольку последовательность будет генерировать дополнительный круговой обход, которого вы часто сможете избежать.

  • Пример: создайте объект-валидатор, который вы используете как в сущности, так и в репозитории.

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

Я также добавлю, что, на мой взгляд, изменение поведения Equals (и других методов) в зависимости от того, представляет ли объект сущность или простой объект данных, просто не идеально. Используя метод, который вы используете, вам также необходимо убедиться, что значение по умолчанию, которое вы используете для ключа, правильно исключено из области значений во всей логике домена.

Если вы все еще хотите использовать эту технику, я предлагаю использовать специальный тип для ключа. Этот тип будет заключать / обертывать ключ с дополнительным состоянием, указывающим, существует ли ключ. Обратите внимание, что это определение настолько похоже на Nullable<T>, что я бы подумал об его использовании (вы можете использовать синтаксис type? в C #). С таким дизайном более ясно, что вы позволяете объекту не иметь идентичности (нулевой ключ). Также должно быть более очевидно, почему дизайн не идеален (опять же, на мой взгляд): вы используете один и тот же тип для представления как сущностей, так и объектов передачи данных без идентификации.

person tne    schedule 18.08.2014

На данный момент я не могу начать использовать значения Guid.

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

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

Другой известный мне вариант - это тот, который вы описали.

person Wiktor Zychla    schedule 21.01.2014
comment
Но как значение Guid будет согласованным вне физических границ? Если объект извлекается в двух разных системах или в разное время, их идентификаторы будут разными. Они должны быть такими же. У меня есть отдельная постоянная (EF) модель в реализациях репозитория. Спасибо за ответ! - person Dave New; 21.01.2014
comment
До того, как идентификатор будет сохранен, его нельзя будет получить из того же хранилища. С другой стороны, нет ничего плохого в том, что временная сущность пересекает физическую границу, даже не сохраняясь. Где-то в какой-то момент кто-то может сохранить это, и гидом будет его личность, то же самое, что вы дали ему при рождении. Обратите внимание, что он может даже сохраняться несколько раз и получать разные идентификаторы int в разных системах, и, таким образом, guid по-прежнему действует как идентификатор DDD, где идентификаторы int являются только идентификаторами постоянства. - person Wiktor Zychla; 21.01.2014
comment
Так вы говорите, что тот же самый объект, загруженный сегодня, может иметь другую идентичность при загрузке завтра? - person Dave New; 21.01.2014
comment
Нет, идентификатор (guid) не меняется. Идентификатор базы данных изменяется и является локальным по отношению к системе, в которой хранятся данные. Все равно от гидов не уйдешь. - person Wiktor Zychla; 21.01.2014

По моему опыту, предложенное вами решение совершенно верно. Я довольно часто использовал этот подход.

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

Однострочное переписывание для вашей реализации

Мне нравится аккуратно реализовывать Equals() и GetHashCode() сущности следующим образом. (Я включаю ToString(), так как всегда переопределяю его, чтобы упростить отладку и регистрацию.)

public override string ToString() => $"{{{this.GetType().Name} Id={this.Id}}}"; // E.g. {MyEntity Id=1} (extra brackets help when nesting)
public override bool Equals(object obj) => (this.Id == default) ? ReferenceEquals(this, obj) : this.Id == (obj as MyEntity)?.Id;
public override int GetHashCode() => this.Id.GetHashCode();

ReferenceEquals() vs base.Equals() - интересное обсуждение. :)

Альтернативное решение

Если вы хотите чего-то еще лучшего, вот еще одно предложение. Что, если бы вы могли иметь значение, которое (для наших намерений и целей) не хуже GUID, но вписывается в long? Что, если бы он также был обновлен без необходимости в репозитории?

Я понимаю, что ваша таблица в настоящее время может соответствовать только int в качестве PRIMARY KEY. Но если вы можете изменить это на long или для своих будущих таблиц, мое предложение может быть вам интересно.

В разделе Предложение: локально уникальная альтернатива GUID я объясняю, как создать локально уникальное, новое, строго возрастающее 64-битное значение. Он заменяет комбинацию идентификатора автоинкремента и идентификатора GUID.

Мне всегда не нравилась идея иметь и числовой идентификатор, и GUID. Это как сказать: «Это уникальный идентификатор объекта. И ... это другой его уникальный идентификатор». Конечно, вы можете оставить один вне домена и языка, но это оставляет вам техническую проблему одновременного управления и скрытием дополнительного числового идентификатора. Если вы предпочитаете иметь один идентификатор, который удобен как для домена (новый без репозитория и именованный идентификатор, а не GUID), так и для базы данных (маленький, быстрый и возрастающий), попробуйте мое предложение.

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

person Timo    schedule 29.10.2017