Шаблон проектирования объекта значения в Hibernate/Spring

Шаблон проектирования объекта значения в Hibernate/Spring

29 декабря 2023 г.

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

Что такое объект-значение?

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

Зачем использовать объекты-значения в Spring и Hibernate?

В контексте разработки приложений, построенных на Spring и Hibernate, использование объектов Value имеет множество преимуществ. Они делают код более понятным, упрощают обслуживание и помогают повысить производительность. Например, объекты-значения можно использовать для представления даты и времени, адресов, географических координат, валюты и других атрибутов, которые можно обобщить как данные, не имеющие собственной идентичности.

Цель этой статьи

Цель этой статьи — рассмотреть применение шаблона Value Object в контексте приложений, созданных с использованием Spring и Hibernate. Мы начнем с объяснения того, как создавать объекты-значения в Java, а затем рассмотрим, как их можно интегрировать в Hibernate для хранения в базе данных. Мы также рассмотрим методы передачи и использования объектов-значений в приложениях Spring и предоставим примеры и советы по их эффективному использованию.

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

Шаблон объекта значения в Java

Объект значения — это неизменяемый класс, который оборачивает простое значение (int, string, bool...) или другие объекты значений, одновременно проверяя входные значения.

Вы не можете создать экземпляр объекта Value с недопустимым входным значением.

Классическим примером объекта значения является номер телефона.

var phoneNumberRaw = "+78005553535";
var phoneNumber = new PhoneNumber("+78005553535");

В первом случае мы сохраняем номер телефона как строку. Во втором случае у нас есть класс PhoneNumber, имеющий некоторую проверку.

@Value
public class PhoneNumber {
    private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance();

    String value;

    public PhoneNumber(String value) {
        this.value = validateAndNormalizePhoneNumber(value);
    }

    private static String validateAndNormalizePhoneNumber(String value) {
        try {
            if (Long.parseLong(value) <= 0) {
                throw new PhoneNumberParsingException("The phone number cannot be negative: " + value);
            }
            final var phoneNumber = PHONE_NUMBER_UTIL.parse(value, "RU");
            final String formattedPhoneNumber = PHONE_NUMBER_UTIL.format(phoneNumber, E164);
            // return the phone in the format 78005553535
            return formattedPhoneNumber.substring(1);
        } catch (NumberParseException | NumberFormatException e) {
            throw new PhoneNumberParsingException("The phone number isn't valid: " + value, e);
        }
    }
}

Преимущества использования объекта значения:

  1. Нормализация значения объекта значения
  2. Не все объекты-значения нормализуют значения. В нашем примере все три номера телефона являются одним и тем же номером телефона с точки зрения значения:

    var set = new HashSet<PhoneNumber>();
    
    set.add(new PhoneNumber("+78005553535"));
    set.add(new PhoneNumber("78005553535"));
    set.add(new PhoneNumber("88005553535"));
    
    assertEquals(1, set.size());
    

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

    1. Более безопасный API.
    2. public static User newUser(String phoneNumber,
                                 String passportNumber) {  }
      

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

      public static User newUser(PhoneNumber phoneNumber,
                                 PassportNumber passportNumber) {  }
      

      Как реализовать в Hibernate

      @Entity
      public class User {
          @Id
          private UUID id;
      
          @Column(name = "phone_number")
          @Convert(converter = PhoneNumberConverter.class)
          private PhoneNumber phoneNumber;
      }
      

      @Converter
      public class PhoneNumberConverter implements AttributeConverter<PhoneNumber, String>{
          public String convertToDatabaseColumn(PhoneNumber attribute) {
              return attribute.getValue();
          }
      
          public PhoneNumber convertToEntityAttribute(String dbData) {
              return new PhoneNumber(dbData);
          }
      }
      

      Проблемы:

      1. Проблема с запросом лайка
      2. Например, мы проверяем имя заказа по следующим правилам:

        • Не более 70 символов.
        • Только английские буквы и пробел.
        • Автоматическая обрезка()
        • Несколько пробелов преобразуются в один: «Красный стул» ->> "Красный стул"

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

        @Value
        public class OrderName {
          String value;
        
          public OrderName(String value) {
            this.value = validateOrderName(value);
          }
        
          private static String validateOrderName(String value) {
            
          }
        }
        

        @Entity
        public class Order {
             
            @Convert(converter = OrderNameConverter.class)
            private OrderName name;
        }
        

        Код репозитория:

        public interface OrderRepository extends JpaRepository<Order, UUID>{
            List<Order> findByNameLike(String name);
        }
        

        В результате получаем ошибку:

        Unexpected exception thrown: org.springframework.dao.InvalidDataAccessApiUsageException: Argument [name%] of type [java.lang.String] did not match parameter type [com.example.domain.OrderName (n/a)]
        

        Проблема в том, что Order.name нельзя выполнить поиск по %. Проблема решена с помощью собственного запроса:

        @Query(
            value = "SELECT * FROM order WHERE name LIKE :name",
            nativeQuery = true
        )
        List<Order> findByNameLike(String name);
        

        1. Если правила проверки/нормализации изменились, мы не можем вычесть данные.
        2. Проблему можно решить обновлением базы старых данных, но

          • Это не всегда приемлемо
          • Обновления могут быть нетривиальными.

          Решение заключается в применении объекта значения только при вставке/изменении данных.

          Value Object Flow

          Преимущества такого подхода:

          * Новые правила проверки/нормализации не нарушат совместимость с существующими данными. * Правила по-прежнему собраны в одном месте, их легко изменить. * Если мы куда-то передаем объект значения, мы уверены в достоверности значений на основе скомпилированных требований. * Нравится - запрос работает как положено

          public interface OrderRepository extends JpaRepository<Order, UUID> {
              List<Order> findByNameLike(String name);
          }
          

          Выводы

          • Объект Value позволяет сделать код более безопасным и простым в обслуживании.
          • Имейте в виду, что проверка меняется, но данные остаются
          • Применять проверку только к тем данным, которыми вы владеете.
          • Объект значения можно использовать только для добавляемых/изменяемых данных, но не обязательно для читаемых данных.


          Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE