DEV Community

loading...

Using Value Objects with JPA

Petter Holmström
Father, husband, voluntary fire fighter, software engineer, drummer.
Updated on ・10 min read

In Tactical Domain-Driven Design, we learned what a value object is and what it is good for. We never really looked at how to use it in real-world projects. Now it is time to roll up our sleeves and have a closer look at some actual code!

Value objects are among the simplest and most useful building blocks in domain-driven design, so let's start by looking at different ways of using value objects with JPA. In order to do that, we are going to steal the concepts of simple type and complex type from the XML Schema specification.

A simple value object is a value object that contains exactly one value of some type, such as a single string or an integer. A complex value object is a value object that contains multiple values of multiple types, such as a postal adress complete with street name, number, postal code, city, state, country and so on.

Because we are going to persist our value objects into a relational database, we have to treat these two types differently when we implement them. However, these implementation details should not matter to the code that actually uses the value objects.

Simple Value Objects: Attribute Converters

Simple value objects are very easy to persist and can be truly immutable with final fields and all. In order to persist them, you have to write an AttributeConverter (standard JPA interface) that knows how to convert between a database column of a known type and your value object.

Let's start with an example value object:

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
    }

}
Enter fullscreen mode Exit fullscreen mode
  1. ValueObject is the empty marker interface. It is used only for documentational purposes and has no functional meaning. If you want to, you can leave it out.
  2. The string that contains the e-mail address is marked as final. As this is the only field in the class, it makes the class truly immutable.
  3. The input string is validated in the constructor, making it impossible to make instances of EmailAddress that contain invalid data.
  4. The e-mail address string is accessible through the toString() method. If you want to use this method for debugging purposes, you can use another getter method of your choice (I sometimes use an unwrap() method as simple value objects are essentially wrappers of other values).
  5. Two value objects having the same value are considered equal so we have to implement the equals() method accordingly.
  6. We changed equals() so now we have to change hashCode() as well.
  7. This is a static method that is used by the constructor to validate input, but it can also be used from the outside to validate strings containing e-mail addresses. This version throws an exception if the e-mail address is invalid.
  8. Another static method that validates e-mail address strings, but this one simply returns a boolean. This can also be used from the outside.

Now, the corresponding attribute converter would look like this:

@Converter // <1>
public class EmailAddressAttributeConverter implements AttributeConverter<String, EmailAddress> { // <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>
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. @Converter is a standard JPA annotation. If you want Hibernate to automatically apply the converter to all EmailAddress attributes, set the autoApply parameter to true (in this example it is false, which is the default).
  2. AttributeConverter is a standard JPA interface that takes two generic parameters: the database column type and the attribute type.
  3. This method converts an EmailAddress to a string. Please note that the input parameter can be null.
  4. This method converts a string to an EmailAddress. Again, please note that the input parameter can be null.

You can store the converter either in the same package as the value object, or in a sub-package (such as .converters) if you want to keep your domain packages nice and clean.

Finally, you can use this value object in your JPA entities like this:

@Entity
public class Contact {

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

    // ...
}
Enter fullscreen mode Exit fullscreen mode
  1. This annotation informs your JPA implementation which converter to use. Without it, e.g. Hibernate will try to store the e-mail address as a serialized POJO as opposed to a string. If you have marked your converter to be automatically applied, then no @Convert annotation will be needed. However, I've found that it is less error-prone to explicitly state which converter to use. I have experienced situations where the converter was supposed to be auto-applied, but for some reason was not detected by Hibernate and so the value object was persisted as a serialized POJO and the integration test passed since it used an embedded H2 database and let Hibernate generate the schema.

Now we are almost done with the simple value objects. However, there are two caveats that we have missed that may come back and bite us once we go into production. They both have to do with the database.

Length Does Matter

The first caveat has to do with the length of the database column. By default, JPA limits the lengths of all database string (varchar) columns to 255 characters. E-mail addresses can be 320 characters long so if a user enters an e-mail address into the system that exceeds 255 characters, you will get an exception when you try to save the value object. To fix this, you need to do the following:

  1. Make sure your database column is wide enough to contain a valid e-mail address.
  2. Make sure your validation method includes a length check of the input. It should not be possible to create EmailAddress instances that cannot be successfully persisted.

This of course applies to other string value objects as well. Depending on the use case you can either refuse to accept strings that are too long, or just silently truncate them.

Don't Make Assumptions About Legacy Data

The second caveat has to do with legacy data. Suppose you have an existing database with e-mail addresses that were previously handled as simple strings and you now introduce a nice, clean EmailAddress value object. If any of those old e-mail addresses are invalid, you will get an exception every time you try to load an entity that has an invalid e-mail address: your attribute converter uses the constructor to create new EmailAddress instances and that constructor validates the input. To fix this you can do any of the following:

  1. Sanitize your database and fix or remove all invalid e-mail addresses.
  2. Create a second constructor used only by the attribute converter that bypasses the validation and instead sets an invalid flag inside the value object. This makes it possible to create invalid EmailAddress objects for existing legacy data while forcing new e-mail addresses to be correct. The code could look something like this:
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

}
Enter fullscreen mode Exit fullscreen mode
  1. This boolean flag is used inside the value object only and is never stored in the database.
  2. The constructor has package visibility in this example to prevent outside code from using it (we want all new e-mail objects to be valid). However, this also requires the attribute converter to be in the same package.
  3. This flag can be passed on to UIs to indicate to the user that the e-mail address is wrong and needs to be corrected.

There! We have all the cases covered and a robust and clean strategy for implementing and persisting simple value objects. However, the underlying database technology, that in principle our value object should not need to care about at all, has already managed to sneak itself into the implementation process (even though it is not really visible in the code). This is a trade-off we have to make if we want to utilize everything that JPA has to offer. This trade-off will be even bigger when we start do deal with complex value objects. Let's find out how.

Complex Value Objects: Embeddables

Persisting a complex value object in a relational database involves mapping multiple fields to multiple database columns. In JPA, the primary tool for this is embeddable objects (annotated with the @Embeddable annotation). Embeddable objects can be persisted both as single fields (annotated with the @Embedded annotation) or as collections (annotated with the @ElementCollection annotation).

However, JPA imposes certain restrictions on embeddable objects that prevent them from being truly immutable. An embeddable object cannot contain any final fields and should have a default no-argument constructor. Still, we want to make our value objects appear and behave as if they were immutable to the outside world. How do we do that?

Let's start with the constructor, or constructors, because we are going to need two of them. The first constructor is the initializing constructor, which will be public. This constructor is the only allowed way to construct new instances of the value object in code.

The second constructor is the default constructor and it will only be used by Hibernate. It does not need to be public, so in order to prevent it from being used in code you can make it protected, package protected or even private (it works with Hibernate but e.g. IntelliJ IDEA will complain). Sometimes I also make a custom annotation, @UsedByHibernateOnly or similar, that I use to mark these constructors. You can then configure your IDE to ignore those constructors when looking for unused code.

As for the fields, it is pretty simple: do not mark the fields as final, only set your field values from within the initializing constructor and do not declare any setter methods or other methods that write to the fields. You may also have to configure your IDE to not suggest you make those fields final.

Finally, you need to override equals and hashCode so that they compare based on value and not based on object identity.

Here is an example of what a finished, complex value object may look like:

@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);
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. We use the same ValueObject marker interface that we used for simple value objects. Again, you can leave it out if you want to.
  2. No fields are marked as final.
  3. The default constructor is package protected and not used by any code at all.
  4. The initializing constructor is to be used by code.
  5. If not all fields are required, make overloaded constructors or use the builder or essence pattern. Forcing the calling code to pass in null or default arguments is ugly (my personal opinion).
  6. The outside world accesses the fields from getters only. There are no setters at all.
  7. Two value objects having the same value are considered equal so we have to implement the equals() method accordingly.
  8. We changed equals() so now we have to change hashCode() as well.

This value object can then be used in entities like this:

@Entity
public class Contact {

    @Embedded
    private PersonName name;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Just One More Thing (or Four)

The observant reader will now notice we have again missed something: the length checks with regards to the database column widths. Just as we had to deal with that for simple value objects, we have to deal with it here. I'm going to leave it as an exercise to the reader.

Speaking of databases, there are a few more things to think about when dealing with @Embeddable value objects: column names and nullability.

Normally, you specify the column names inside the embeddable using the @Column annotation. If you leave it out, the column names are derived from the field names. This may be enough for you, but in some cases you may find yourself using the same value object in different entities, with columns that have different names. In this case, you have to rely on the @AttributeOverride annotation (check it out if you are not familiar with it).

Nullability has to do with how you are going to persist the state where your value object is null. For simple value objects that was easy - just store NULL in the database column. For complex value objects being stored in a collection this is also easy - just leave the value object out. For complex value objects being stored in fields, you have to check your JPA implementation.

Hibernate, by default, will write NULL to all the columns if the field is null. Likewise, when reading from the database, if all columns are NULL Hibernate will set the field to null. This is normally fine, provided that you don't actually want to have a value object instance whose fields are all set to null. This also means that even though your value object may require one or more of its fields to not be null, the database table must allow nulls in that column or columns if the entire value object can be null.

Finally, if you end up having an @Embeddable class extending another @Embeddable class, remember to add the @MappedSuperclass annotation to the parent class. If you leave it out, everything in your parent class will be ignored. This will lead to some strange behavior and lost data that is not obvious to debug.

As you can see, the underlying database and persistence technology is even more present in the implementation of our complex value objects than it was for simple value objects. From a productivity perspective, this is an acceptable tradeoff in my opinion. It is possible to write the domain objects completely unaware of how they are persisted, but that will then require a lot more work in the repositories - work you would have to do yourself. Unless you have a really good reason, it is often not worth the effort (it is an interesting learning experience, though, so if you have the interest and the time then by all means give it a shot).

Discussion (0)