Шаблон проектирования объекта значения в 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);
}
}
}
Преимущества использования объекта значения:
- Нормализация значения объекта значения ол>
- Более безопасный API. ол>
- Проблема с запросом лайка ол>
- Не более 70 символов.
- Только английские буквы и пробел.
- Автоматическая обрезка()
- Несколько пробелов преобразуются в один: «Красный стул» ->> "Красный стул"
- Если правила проверки/нормализации изменились, мы не можем вычесть данные. ол>
- Это не всегда приемлемо
- Обновления могут быть нетривиальными.
- Объект Value позволяет сделать код более безопасным и простым в обслуживании.
- Имейте в виду, что проверка меняется, но данные остаются
- Применять проверку только к тем данным, которыми вы владеете.
- Объект значения можно использовать только для добавляемых/изменяемых данных, но не обязательно для читаемых данных.
Не все объекты-значения нормализуют значения. В нашем примере все три номера телефона являются одним и тем же номером телефона с точки зрения значения:
var set = new HashSet<PhoneNumber>();
set.add(new PhoneNumber("+78005553535"));
set.add(new PhoneNumber("78005553535"));
set.add(new PhoneNumber("88005553535"));
assertEquals(1, set.size());
То есть при сохранении объекта-значения его значение нормализуется и впоследствии сравнивается с другими объектами-значениями по значению.
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);
}
}
Проблемы:
Например, мы проверяем имя заказа по следующим правилам:
Код сущности будет выглядеть следующим образом:
@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);
Проблему можно решить обновлением базы старых данных, но
Решение заключается в применении объекта значения только при вставке/изменении данных.
Преимущества такого подхода:
* Новые правила проверки/нормализации не нарушат совместимость с существующими данными. * Правила по-прежнему собраны в одном месте, их легко изменить. * Если мы куда-то передаем объект значения, мы уверены в достоверности значений на основе скомпилированных требований. * Нравится - запрос работает как положено
public interface OrderRepository extends JpaRepository<Order, UUID> {
List<Order> findByNameLike(String name);
}
Выводы
Оригинал