paint-brush
Value Object Design Pattern in Hibernate/Springby@ndee80
800 reads
800 reads

Value Object Design Pattern in Hibernate/Spring

by Andrei RogalenkoDecember 28th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

We'll start by explaining how to create Value Objects in Java, and then look at how they can be integrated into Hibernate for storage in a database.
featured image - Value Object Design Pattern in Hibernate/Spring
Andrei Rogalenko HackerNoon profile picture

In the world of software development, there are many scenarios where it is necessary to model and work with data that does not have its own identity and is not an entity in the strict sense. This data is information that characterizes an aspect of an object or event, and can be used in different contexts.

What is a Value Object?

Value Object is one of the key design patterns that allows you to model and work with this kind of data. Value Objects are objects whose identity is determined by their values, not by an identifier. They are immutable and do not have their own life cycle, unlike entities that have a unique identifier and can change over time.

Why use Value Objects in Spring and Hibernate?

In the context of developing applications built on Spring and Hibernate, using Value Objects has many advantages. They make the code more understandable, make maintenance easier, and help improve performance. For example, Value Objects can be used to represent date and time, addresses, geographic coordinates, currency, and other attributes that can be generalized as data that does not have its own identity.

The purpose of this article

The purpose of this article is to consider the application of the Value Object pattern in the context of applications built using Spring and Hibernate. We'll start by explaining how to create Value Objects in Java, and then look at how they can be integrated into Hibernate for storage in a database. We will also look at methods for transferring and using Value Objects in Spring applications and provide examples and tips on how to use them effectively.


Let's start by learning how to model Value Objects in Java and why it's important for your application.

Value Object pattern in Java

Value Object is an immutable class that wraps a simple value (int, string, bool...) or other Value Objects, while validating input values.


You cannot create an instance of Value Object with an invalid input value.


A classic example of a Value Object is a phone number.

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


In the first case, we store the phone number as a String. In the second case, we have a PhoneNumber class, which has some kind of validation.


@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);
        }
    }
}


Advantages of using Value Object:

  1. Normalization of the value of the Value Object


Not all Value Objects normalize values, in our example, all three phone numbers are the same phone number in terms of 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());


That is, when saving a Value Object, its value is normalized and subsequently compared with other Value Object objects by value.


  1. A more secure API.


public static User newUser(String phoneNumber,
                           String passportNumber) { … }


In this example, you can pass obviously incorrect data to the user creation function and subsequently get a lot of problems. If Value Object is used as parameters, then guaranteed validated data is transmitted.


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


How to implement in 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);
    }
}


Problems:

  1. The problem with the Like request


For example, we validate the order name according to the following rules:

  • No more than 70 characters
  • Only English letters and a space
  • Automatic trim()
  • Multiple spaces are converted to one: "Red chair" -> "Red chair"


The Entity code will look like this:

@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;
}


Repository Code:

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


As a result, we get an error:

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)]


The problem is that for Order.name you can't search by %. The problem is solved using native query:


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


  1. If the validation/normalization rules have changed, then we cannot subtract the data.


The problem can be solved by updating the database of old data, but

  • This is not always acceptable
  • Updates can be non-trivial


The solution is apply Value Object only when inserting/changing data.



Value Object Flow


The advantages of this approach:


  • New validation/normalization rules will not break compatibility with existing data

  • The rules are still encapsulated in one place, they are easy to change

  • If we pass a Value Object somewhere, we are sure of the validity of the values based on the compiled requirements

  • Like-the request is working as expected


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

Conclusions

  • Value Object allows you to make the code more secure and easier to maintain
  • Keep in mind that validation changes, but the data remains
  • Apply validation only to the data that you own
  • Value Object can only be used on data that is being added/modified, but not necessarily on readable data.