Рубрики
Без рубрики

Использование объектов значений с помощью JPA

В Тактическом предметно-ориентированном проектировании мы узнали, что такое объект ценности и для чего он хорош. Мы никогда… Помечено доменным дизайном, jpa, java, ddd.

Доменно-ориентированный дизайн (Серия из 10 частей)

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

Объекты ценности являются одними из самых простых и полезных строительных блоков в предметно-ориентированном проектировании, поэтому давайте начнем с рассмотрения различных способов использования объектов ценности в JPA. Чтобы сделать это, мы собираемся украсть понятия простого типа и сложный тип из спецификации XML-схемы.

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

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

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

Давайте начнем с примера объекта значения:

public class EmailAddress implements ValueObject { // <1>

    private final String email; // <2>

    public EmailAddress(@NotNull String email) { 
        this.email = validate(email); // <3>
    }

    @Override
    public @NotNull String toString() { // <4>
        return email;
    }

    @Override
    public boolean equals(Object o) { // <5>
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        EmailAddress that = (EmailAddress) o;
        return email.equals(that.email);
    }

    @Override
    public int hashCode() { // <6>
        return email.hashCode();
    }

    public static @NotNull String validate(@NotNull String email) { // <7>
        if (!isValid(email)) {
            throw new IllegalArgumentException("Invalid email: " + email);
        }
        return email;
    }

    public static boolean isValid(@NotNull String email) { // <8>
        // Validate the input string, return true or false depending on whether it is a valid e-mail address or not
    }

}
  1. Объект значения – это пустой интерфейс маркера. Он используется только для целей документации и не имеет никакого функционального значения. Если хочешь, можешь не упоминать об этом.
  2. Строка , содержащая адрес электронной почты , помечена как окончательная . Поскольку это единственное поле в классе, оно делает класс действительно неизменяемым.
  3. Строка ввода проверяется в конструкторе, что делает невозможным создание экземпляров Адреса электронной почты , содержащих неверные данные.
  4. Строка адреса электронной почты доступна с помощью метода toString() . Если вы хотите использовать этот метод для целей отладки, вы можете использовать другой метод получения по вашему выбору (я иногда использую метод unwrap() , поскольку простые объекты значений по сути являются оболочками других значений).
  5. Два объекта значений, имеющих одинаковое значение, считаются равными, поэтому мы должны соответствующим образом реализовать метод equals() .
  6. Мы изменили равно() так что теперь мы также должны изменить Хэш-код() .
  7. Это статический метод, который используется конструктором для проверки входных данных, но его также можно использовать извне для проверки строк, содержащих адреса электронной почты. Эта версия создает исключение, если адрес электронной почты неверен.
  8. Еще один статический метод, который проверяет строки адресов электронной почты, но этот просто возвращает логическое значение. Это также может быть использовано извне.

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

@Converter // <1>
public class EmailAddressAttributeConverter implements AttributeConverter { // <2>

    @Override
    @Contract("null -> null")
    public String convertToDatabaseColumn(EmailAddress attribute) {
        return attribute == null ? null : attribute.toString(); // <3>
    }

    @Override
    @Contract("null -> null")
    public EmailAddress convertToEntityAttribute(String dbData) {
        return dbData == null ? null : new EmailAddress(dbData); // <4>
    }
}
  1. @Конвертер является стандартной аннотацией JPA. Если вы хотите, чтобы в режиме гибернации конвертер автоматически применялся ко всем атрибутам Адреса электронной почты , установите для параметра автоматическое применение значение true (в данном примере значение false по умолчанию).
  2. AttributeConverter – это стандартный интерфейс JPA, который принимает два общих параметра: тип столбца базы данных и тип атрибута.
  3. Этот метод преобразует Адрес электронной почты в строку. Пожалуйста, обратите внимание, что входной параметр может быть нулевым .
  4. Этот метод преобразует строку в Адрес электронной почты . Опять же, пожалуйста, обратите внимание, что входной параметр может быть нулевым .

Вы можете хранить конвертер либо в том же пакете, что и объект value, либо в подпакете (например, .конвертеры ), если хотите сохранить пакеты вашего домена в чистоте и порядке.

Наконец, вы можете использовать этот объект value в своих сущностях JPA следующим образом:

@Entity
public class Contact {

    @Convert(converter = EmailAddressAttributeConverter.class)  // <1>
    private EmailAddress emailAddress;

    // ...
}
  1. Эта аннотация информирует вашу реализацию JPA о том, какой конвертер использовать. Без этого, например, Hibernate попытается сохранить адрес электронной почты в виде сериализованного POJO, а не строки. Если вы отметили свой конвертер для автоматического применения, то аннотации @Convert не потребуется. Однако я обнаружил, что менее подвержено ошибкам явное указание того, какой конвертер использовать. Я сталкивался с ситуациями, когда конвертер должен был автоматически применяться, но по какой-то причине не был обнаружен Hibernate, и поэтому объект value сохранялся как сериализованный POJO, а тест интеграции прошел, поскольку он использовал встроенную базу данных H2 и позволил Hibernate генерировать схему.

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

Длина Действительно Имеет Значение

Первое предостережение связано с длиной столбца базы данных. По умолчанию JPA ограничивает длину всех столбцов строки базы данных ( varchar ) 255 символами. Длина адресов электронной почты может составлять 320 символов поэтому, если пользователь вводит в систему адрес электронной почты, который превышает 255 символов, вы получите исключение при попытке сохранить объект value. Чтобы исправить это, вам необходимо выполнить следующее:

  1. Убедитесь, что столбец вашей базы данных достаточно широк, чтобы содержать действительный адрес электронной почты.
  2. Убедитесь, что ваш метод проверки включает проверку длины входных данных. Не должно быть возможности создавать Адреса электронной почты экземпляры, которые не могут быть успешно сохранены.

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

Не Делайте Предположений Относительно Устаревших Данных

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

  1. Очистите свою базу данных и исправьте или удалите все недействительные адреса электронной почты.
  2. Создайте второй конструктор, используемый только преобразователем атрибутов, который обходит проверку и вместо этого устанавливает флаг недопустимый внутри объекта value. Это позволяет создавать недопустимые Адрес электронной почты объекты для существующих устаревших данных, заставляя новые адреса электронной почты быть правильными. Код будет выглядеть примерно так:
public class EmailAddress implements ValueObject {

    private final String email;
    private final boolean invalid; // <1>

    public EmailAddress(@NotNull String email) { 
        this(email, true);
    }

    EmailAddress(@NotNull String email, boolean validate) { // <2>
        if (validate) {
            this.email = validate(email);
            this.invalid = false;
        } else {
            this.email = email;
            this.invalid = !isValid(email);
        }
    }

    public boolean isInvalid() { // <3>
        return invalid;
    }

    // The rest of the methods omitted

}
  1. Этот логический флаг используется только внутри объекта value и никогда не сохраняется в базе данных.
  2. В этом примере конструктор имеет видимость пакета, чтобы предотвратить его использование внешним кодом (мы хотим, чтобы все новые объекты электронной почты были действительными). Однако для этого также требуется, чтобы преобразователь атрибутов находился в том же пакете.
  3. Этот флаг может быть передан Нам, чтобы указать пользователю, что адрес электронной почты указан неправильно и его необходимо исправить.

Там! У нас есть все рассмотренные случаи и надежная и четкая стратегия для внедрения и сохранения простых объектов ценности. Однако базовая технология базы данных, о которой в принципе нашему объекту ценности вообще не нужно заботиться, уже сумела внедриться в процесс реализации (хотя на самом деле это не видно в коде). Это компромисс, на который мы должны пойти, если хотим использовать все, что может предложить JPA. Этот компромисс будет еще больше, когда мы начнем иметь дело со сложными ценными объектами. Давайте выясним, как это сделать.

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

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

Давайте начнем с конструктора или конструкторов, потому что нам понадобятся два из них. Первый конструктор – это конструктор инициализации, который будет общедоступным. Этот конструктор является единственным разрешенным способом создания новых экземпляров объекта value в коде.

Второй конструктор является конструктором по умолчанию, и он будет использоваться только в режиме гибернации. Он не обязательно должен быть общедоступным, поэтому, чтобы предотвратить его использование в коде, вы можете сделать его защищенным, защищенным пакетом или даже закрытым (он работает с Hibernate, но, например, IntelliJ IDEA будет жаловаться). Иногда я также создаю пользовательскую аннотацию @UsedByHibernateOnly или аналогичную, которую я использую для обозначения этих конструкторов. Затем вы можете настроить свою среду разработки так, чтобы она игнорировала эти конструкторы при поиске неиспользуемого кода.

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

Наконец, вам нужно переопределить равно и Хэш-код чтобы они сравнивались на основе значения, а не на основе идентичности объекта.

Вот пример того, как может выглядеть готовый объект со сложной стоимостью:

@Embeddable
public class PersonName implements ValueObject { // <1>

    private String firstname; // <2>
    private String middlename;
    private String lastname;

    @SuppressWarnings("unused")
    PersonName() { // <3>    
    }

    public PersonName(@NotNull String firstname, @NotNull String middlename, @NotNull String lastname) { // <4>
        this.firstname = Objects.requireNonNull(firstname);
        this.middlename = Objects.requireNonNull(middlename);
        this.lastname = Objects.requireNonNull(lastname);
    }

    public PersonName(@NotNull String firstname, @NotNull String lastname) { // <5>
        this(firstname, "", lastname);
    }

    public @NotNull String getFirstname() { // <6>
        return firstname;
    }

    public @NotNull String getMiddlename() {
        return middlename;
    }

    public @NotNull String getLastname() {
        return lastname;
    }

    @Override
    public boolean equals(Object o) { // <7>
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonName that = (PersonName) o;
        return firstname.equals(that.firstname)
            && middlename.equals(that.middlename)
            && lastname.equals(that.lastname);
    }

    @Override
    public int hashCode() { // <8>
        return Objects.hash(firstname, middlename, lastname);
    }
}
  1. Мы используем тот же интерфейс Объект значения маркер, который мы использовали для простых объектов значения. Опять же, вы можете опустить это, если хотите.
  2. Никакие поля не помечены как окончательные .
  3. Конструктор по умолчанию защищен пакетом и вообще не используется никаким кодом.
  4. Конструктор инициализации должен использоваться кодом.
  5. Если не все поля обязательны, создайте перегруженные конструкторы или используйте шаблон builder или essence. Заставлять вызывающий код передавать аргументы null или по умолчанию некрасиво (мое личное мнение).
  6. Внешний мир получает доступ к полям только от добытчиков. Сеттеров вообще нет.
  7. Два объекта значений, имеющих одинаковое значение, считаются равными, поэтому мы должны соответствующим образом реализовать метод equals() .
  8. Мы изменили равно() так что теперь мы также должны изменить Хэш-код() .

Этот объект значения затем может быть использован в таких объектах, как этот:

@Entity
public class Contact {

    @Embedded
    private PersonName name;

    // ...
}

Еще одна вещь (или Четыре)

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

Говоря о базах данных, есть еще несколько вещей, о которых следует подумать при работе с объектами @Embeddable value: имена столбцов и возможность обнуления.

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

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

Режим гибернации по умолчанию записывает значение NULL во все столбцы, если поле равно нулю. Аналогично, при чтении из базы данных, если все столбцы равны нулю, Hibernate установит для поля значение null. Обычно это нормально, при условии, что вы на самом деле не хотите иметь экземпляр объекта value, все поля которого имеют значение null. Это также означает, что даже если для вашего объекта value может потребоваться, чтобы одно или несколько его полей не были нулевыми, таблица базы данных должна разрешать нули в этом столбце или столбцах, если весь объект value может быть нулевым.

Наконец, если у вас в конечном итоге появится класс @Embeddable , расширяющий другой класс @Embeddable , не забудьте добавить аннотацию @MappedSuperclass в родительский класс. Если вы оставите это, все в вашем родительском классе будет проигнорировано. Это приведет к некоторому странному поведению и потере данных, которые неочевидны для отладки.

Как вы можете видеть, базовая технология базы данных и сохраняемости присутствует в реализации наших сложных объектов ценности даже в большей степени, чем в случае простых объектов ценности. С точки зрения производительности, на мой взгляд, это приемлемый компромисс. Можно писать объекты домена, совершенно не зная о том, как они сохраняются, но тогда потребуется гораздо больше работы в репозиториях – работы, которую вам придется выполнять самостоятельно. Если у вас нет действительно веской причины, это часто не стоит затраченных усилий (тем не менее, это интересный опыт обучения, поэтому, если у вас есть интерес и время, обязательно попробуйте).

Доменно-ориентированный дизайн (Серия из 10 частей)

Оригинал: “https://dev.to/peholmst/using-value-objects-with-jpa-27mi”