Overview
Enums are a powerful feature of the java language. They were introduced in Java 5. They are a special type that allows us to declare a set of predefined constants. They improve readability, provide compile time checks and are type safe.
In this article, we are going to extend the functionality of the customer endpoint of previous articles. What we are going to do is to add a new status field to the Customer class. Then, we will be able to persist and search based on the status.
The article is divided into two main sections:
- Mapping Enums with JPA.
- Request Parameters and Enums in Spring.
Without any further delay, lets move to the next section and write some code.
Mapping Enums with JPA
The simplest way to map an enum with a database column is using the @Enumerated annotation from the jakarta.persistence package. This annotation accepts two types of mapping. The default is ORDINAL which means that the enum will be stored as an integer. The other type, STRING, stores the enum as enum value/constant.
Let's see this is action. The steps are:
- Create the enum. It is good practice to use capitals for the values/constants.
- Add a new field to the Customer Entity.
- Annotate the ne field with @Enumerated.
@Entity
public class Customer {
public enum Status { ACTIVATED, DEACTIVATED, SUSPENDED };
...
@Enumerated(EnumType.ORDINAL)
private Status status;
...
}
The next picture shows records saved for the Customer Entity in the database
The ORDINAL values begin with 0. The representation of each value in the enum is then
0 -> ACTIVATED
1 -> DEACTIVATED
2 -> SUSPENDED
Lets see what happens then using the STRING type mapping in our Entity
@Enumerated(EnumType.STRING)
private Status status;
The data in the customer table now is presented in the following picture
Now the column is easier to interpert. On the other hand, there is duplication. This could be a problem if the volume of data is high.
These two out of the box solutions work well but they are limited. What if we want to use different values in the table or even different database types (other than INT or VARCHAR)?
Using a @Convert for custom mapping
The @Convert annotation converts an entity field to a specific value in the database. It requires a Converter class that perfoms the transformation from entity attribute state into database column representation and back again.
For our case, the values in the column will be as follows ACTIVATED -> 1, DEACTIVATED -> 2 and SUSPENDED -> 3.
The steps to accomplish this are described here:
- Add code property to the Status enum. This will be the value to be in the table column.
- Create a converter class to make the conversation.
- Annotate the entity field.
The change in the enum is straghtforward.
public enum Status {
ACTIVATED(1), DEACTIVATED(2), SUSPENDED(3);
int statusId;
private Status(int statusId) {
this.statusId = statusId;
}
public int getStatusId() {
return statusId;
}
};
The converter class must implement the interface AttributeConverter. It is a generic interface taking two parameters and declaring two methods. The first type is the entity attribute type and the second is the type of the data to which the attribute will be converted.
Here we create the converter for the Status enum
@Converter
public class CustomerStatusConverter implements AttributeConverter<Customer.Status, Integer> {
@Override
public Integer convertToDatabaseColumn(Customer.Status status)
{
return status.getStatusId();
}
@Override
public Customer.Status convertToEntityAttribute(Integer
statusId) {
return Arrays.stream(Customer.Status.values())
.filter(s -> s.getStatusId() == statusId)
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}
}
Finally, the entity attribute is annotated with @Convert indicating the converter class.
@Convert(converter = CustomerStatusConverter.class)
private Status status;
Optionally, the annotation @Converter can take the property autoApply. Its default value is false, hence it is disabled. When it is set to true, Spring will automatically apply the converter to the entity attribute of the type specified in the first param.
That's is all needed. To test it, we are going to persist a few Customer using a DataInitalizer.
Initializing the Customer table
We will fill up the customer table with a few random records using a CommandLineRunner. This is a functional interface declaring a run method. Spring will execute the run method after all beans are instanciated and application context is fully loaded but before application is up.
@Configuration
public class DataInitializer implements CommandLineRunner {
private CustomerRepo customerRepo;
public DataInitializer(CustomerRepo customerRepo) {
this.customerRepo = customerRepo;
}
@Override
public void run(String... args) {
IntStream.rangeClosed(1,9)
.forEach(this::createRandomCustomer);
}
public void createRandomCustomer(int id) {
Customer customer = new Customer();
customer.setId((long)id);
customer.setName("name "+id+" surname "+id);
customer.setEmail("organisation"+id+"@email.com");
customer.setDateOfBirth(LocalDate.of(1980 + 2*id,id % 12,
(3*id) % 28));
customer.setStatus(switch (id % 3) {
case 0 -> Customer.Status.ACTIVATED;
case 1 -> Customer.Status.DEACTIVATED;
case 2 -> Customer.Status.SUSPENDED;
default -> throw new IllegalStateException("Unexpected
value: " + id % 3);
});
customerRepo.save(customer);
}
}
Now there are a few records available in the table with the values we want in the STATUS column.
Adding a find method to the Repository
The last step before moving to the web layer is to create a new method to filter by name and status in the CustomerRepo class. Spring Data JPA offers the @Query annotation to execute JPQL or native SQL. This comes in handy when Spring Data JPA query methods do not provide a solution for the search criteria.
We want to search by status and name plus ignore nulls in a single method. Then, with the help of @Query we can write the code
public interface CustomerRepo extends JpaRepository<Customer, Long> {
@Query("""
SELECT c FROM Customer c
WHERE (c.status = :status or :status is null)
and (c.name like :name or :name is null) """)
public List<Customer> findCustomerByStatusAndName(
@Param("status") Customer.Status status,
@Param("name") String name);
}
All is set now to go to the web layer.
Request Parameters and Enums in Spring
The idea is to add filters so that clients of the API can search customers by name and status. The filters will be passed as request parameters in the GET HTTP request. Name can accept any string data. Conversely, status will be restricted to the enum predefined values.
Spring MVC supports enums as parameters as well as path variables. Simply putting the params in the method signature of the controller will be enough
@RestController
@RequestMapping("api/v1/customers")
public class CustomerController {
...
@GetMapping
public List<CustomerResponse> findCustomers(
@RequestParam(name="name", required=false) String name,
@RequestParam(name="status", required=false)
Customer.Status status) {
return customerRepo
.findCustomerByStatusAndNameNamedParams(status, name)
.stream()
.map(CustomerUtils::convertToCustomerResponse)
.collect(Collectors.toList());
}
...
}
The controller is ready to receive the parameters. Let's suppose the URL http://localhost:8080/api/v1/customers?status=ACTIVATED is invoked. Spring will attempt to convert the value coming from the http parameter status to a valid status enum.
In this case both values match, the http param and the status enum value. Hence, the response back from the API is the list of activated customers
[
{
"id": 3,
"name": "name 3 surname 3",
"email": "organisation3@email.com",
"dateOfBirth": "09-03-1986",
"status": "activated"
},
{
"id": 6,
"name": "name 6 surname 6",
"email": "organisation6@email.com",
"dateOfBirth": "18-06-1992",
"status": "activated"
},
{
"id": 9,
"name": "name 9 surname 9",
"email": "organisation9@email.com",
"dateOfBirth": "27-09-1998",
"status": "activated"
}
]
What happens if the parameter it does not match any enum value or it is invalid? Well, if they do not match or it is invalid, Spring will throw a ConversionFailedException.
Let's demostrate it with the following calls. First, URL is http://localhost:8080/api/v1/customers?status=activated
The response is not what we could have expected
{
"status": 400,
"message": "There was a error converting the value activated
to type Status. ",
"timestamp": "2023-06-24T19:33:26.6583667"
}
The second URL will be http://localhost:8080/api/v1/customers?status=DELETED
The response from the server could be accepted in this scenario as the status does not exist.
{
"status": 400,
"message": "There was a error converting the value DELETED to
type Status.",
"timestamp": "2023-06-24T19:08:49.6261182"
}
The reason for this behaviour is because Spring makes use of the class StringToEnumConverterFactory. The convert method delegates to the Enum valueOf method which throws a IllegalArgumentException if no enum constant is found.
@Nullable
public T convert(String source) {
return source.isEmpty() ? null : Enum.valueOf(this.enumType,
source.trim());
}
We would like to alter this behaviour and permit to filter using lower cases. Similar to what was done in the JPA section, we will have to write a custom converter.
Creating a Spring Converter
Adding a custom converter is pretty easy. Just have to implement the Converter interface and override its convert method. Convert method takes two generic types, the type of the source and the type of the outcome.
@Component
public class StringIgnoreCaseToEnumConverter implements
Converter<String, Customer.Status> {
@Override
public Customer.Status convert(String source) {
return Customer.Status.valueOf(source.toUpperCase());
}
}
The above method takes the incoming String. And it is transformed to uppercase prior to return the enum constant via Enum.valueOf. Therefore, the string "activated" will become "ACTIVATED". Consequently, it will return the enum constant Status.ACTIVATED.
Let's re-test now the previous URL and check the results. Calling the URL http://localhost:8080/api/v1/customers?status=activated generates the output we wanted
[
{
"id": 3,
"name": "name 3 surname 3",
"email": "organisation3@email.com",
"dateOfBirth": "09-03-1986",
"status": "activated"
},
...
]
Conclusion
This article has explained in detailed how to use enums in the persistence layer and the web layer. Here is a summary of the main points of it.
For the persistence layer, JPA provides the ORDINAL and STRING options out of the box. If this does not meet the requirements of the application, custom converters provide total control on how the mapping is done.
For the web layer, Spring MVC comes with enums support for request params and path variable. It converts String to enums automatically if there is exact matching. This behaviour can be changed by implementing a converter.
Code can be found in the repository by clicking in this link
Hope you enjoyed reading it. Stay tuned for more articles covering Spring and Java topics!
Top comments (1)
very good post. clear and clean. Thanks a lot. By the way, what should I do if I need to set a default value for enum column?