Spring Boot and relational databases - part 6: Setting constraints
In the previous part of this series, support for deleting contacts and addresses was added. This part describes the steps to add different kind of constraints to the database tables to prevent duplicate contacts and addresses.
Updating dependencies
Before changing the code, I updated to a newer version of Spring Boot
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
Updating application.properties
Updating to a later version of Spring Boot made another change neccessary. Prior to version 2.3, the name for the in-memory database was constant. After version 2.3, it will be re-generated at each application startup. This means that the h2-console will not function correctly, since the database is not found.
To work around that, we can add the following property to the application.properties:
spring.datasource.url=jdbc:h2:mem:testdb
Setting constraints for contacts
The first goal is to make the email unique for each contact. This can be achieved with a very little change in the Contact
class:
@Column(name = "email", unique = true, nullable = false)
private String email;
This ensures that the email address is unique and cannot be null.
Handling the database error
Since a new constraint was added, an exception will occur each time when the email address is null or not unique.
Right now this will lead to lots of stack traces and an internal server error. However, it will be nice to produce a more explicit error message and HTTP status code to let the client know why the request failed. This can be done using ControllerAdvice
:
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpClientErrorException.class)
public void handleConstraintViolation(){
// do something here, if needed
}
}
The class GlobalExceptionHandler
is annotated with ControllerAdvice
. The method is annotated with ExceptionHandler
. Here it is defined which exception is handled (HttClientErrorException
).
The method is also annotated with ResponseStatus
which lets us define which HTTP status is returned.
So each time when an HttpClientErrorException
is thrown, our appication will respond with HTTP status 400.
The only change that is left is in the ContactService
class:
public void createContact(final Contact contact){
try {
contactRepository.save(contact);
}catch (DataIntegrityViolationException ex){
throw new HttpClientErrorException(HttpStatus.BAD_REQUEST);
}
}
Here, the DataIntegrityViolationException
is caught and an HttpClientErrorException
is thrown.
Setting constraints for addresses
When it comes to constraints for the address table, the situation is slightly different. Here, it is not sufficient to define one or more columns as unique. Instead, we could use composite keys.
Using @Embeddable
One way of defining composite keys is by defining an own class representing the composite key. In this example, I called it AddressId
.
@Embeddable
public class AddressId implements Serializable {
private String street;
private String postalCode;
private String city;
...
}
This class has three members: street
, postalCode
and city
. These member variables together form the address’ key. The class is annotaded with @Embeddable
.
Now the Address
class needs to be changed so that the new composite key is used:
@Entity
public class Address {
@EmbeddedId
private AddressId addressId;
...
}
The @EmbeddedId
annotation indicates that this id is a composite key. The member variables that belonged to the Address
class earlier have been removed completely.
A test run
This can be tested with the following request:
curl -v -H "Content-Type: application/json" -X POST -d '{"firstName": "John", "lastName": "Doe", "email":"test-1@test.com", "address": {"addressId":{"street":"Street","postalCode":"77588", "city":"Town"}}}' http://localhost:8080/contact
The request will succeed and a new contact is added. Now let’s do a similar request:
curl -v -H "Content-Type: application/json" -X POST -d '{"firstName": "John", "lastName": "Doe", "email":"test-2@test.com", "address": {"addressId":{"street":"Street","postalCode":"77588", "city":"Town"}}}' http://localhost:8080/contact
This request will fail because the address is not unique.
Further resources
The code example can be found here