How to Design a CRUD Web Service for Inheritable Entity
Introduction
Sometimes we need to develop a web service that provides CRUD (Create, Read, Update, and Delete) operations for an entity that supports inheritance. However, the traditional layered architecture (controller layer, service layer, and repository layer) is not enough to have a clean and scalable code. Indeed, if we put the business logic and mapping code inside the service layer, we will have two main problems:
- Having multiple conditions based on the DTO class type
- Each time we want to introduce a new subclass, we need to modify the service layer, which is not good if we are developing a framework.
The code below illustrates what the service class will look like if we use the traditional architecture:
public UppserClassServiceImpl implements UppserClassService{
...
public UpperClassDto save(UpperClassDto upperClassDto){
if(upperClassDto instanceof SubClass1Dto){
SubClass1Dto dto= (SubClass1Dto) upperClassDto;
// Mapping data from DTO to Entity
SubClass1Entity entity= new SubClass1Entity();
entity.setField1(dto.getFieldA());
...
//Perform some business logic
...
//Save entity
repository.save(entity);
dto.setId(entity.getId);
}
else if(upperClassDto instanceof SubClass2Dto){
//make similar code
...
}
else if(...){
....
}
...
return upperClassDto;
}
...
}
As we see, for one CRUD operation, we have many conditions and mapping code lines for each subclass. Therefore, we can imagine how complex the code will be for the whole service class.
Proposed Design
The following class diagram illustrates the proposed design:
EntityController
: Rest controller that handles the incoming requestsEntityManager
: Dispatches the requests received by the controller to the correct subentity serviceEntityService
: Generic interface that defines the CRUD operations for subentitiesSubEntityNService
: Specific implementation of CRUD operations for the subentityN
; it contains the code that performs the subentity mapping and the business logicRepository
: Provides database access method
Implementation Example
In this part, we will see an implementation example of the proposed design based on the Java programming language with Spring Boot.
For this example, suppose that we want to provide a real estate management web service.
Annotation
This annotation is used to link the Entities and DTO to the corresponding service classes.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface EntityHandler {
/**
* The name of the bean that provides CRUD operations for annotated entity or DTO
*/
String beanName();
}
Entities Classes
The following classes represent the real estate database entities. We have an abstract class from which all of the real estate types are inheriting. For this example, we will use apartment
, house
and land
as subclasses.
@Data
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)//signle tableinheritance strategy
@DiscriminatorColumn(name = "type")
@Table(name = "reat_estate")
public abstract class RealEstate implements Serializable{
@Id
private Integer id;
private Double area;
@Column(name="land_title")
private String landTitle;
...
}
@Data
@Entity
@DiscriminatorValue("apartment")
@EntityHandler("apartmentService")
public class Apartment extends RealEstate {
...
}
@Data
@Entity
@DiscriminatorValue("house")
@EntityHandler("houseService")
public class House extends RealEstate {
@Column(name="floor_count")
private Integer floorCount;
...
}
@Data
@Entity
@DiscriminatorValue("land")
@EntityHandler("landService")
public class Land extends RealEstate {
@Column(name="contains_well")
private Boolean containsWell;
...
}
Data Transfer Objects Classes (DTO)
Here, we define the DTO classes that correspond to each entity. We use inheritance between DTO classes as below as we did with entities using Jackson annotations:
@Data
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
@Type(value = Apartment.class, name = "apartment"),
@Type(value = House.class, name = "house"),
@Type(value = Land.class, name = "land")
})
public abstract class RealEstateDto implements Serializable{
private Integer id;
private Double area;
private String landTitle;
...
}
@Data
@EntityHandler("apartmentService")
public class ApartmentDto extends RealEstateDto {
...
}
@Data
@EntityHandler("houseService")
public class HouseDto extends RealEstateDto {
private Integer floorCount;
...
}
@Data
@EntityHandler("landService")
public class LandDto extends RealEstateDto {
private Boolean containsWell;
...
}
Service Layer
The service layer implements the CRUD operation for the subclasses. In the code below, we used an interface to define the CRUD methods signatures followed by an implementation example for the Apartment
subclass.
public interface RealEstateService<E extends RealEstate,D extends RealEstateDto> {
/**
* converts the entity given as parameter to the correspending STO
*/
D get(E entity);
/**
* Saves DTO in database
*/
D save(D dto);
/**
* Updates and entity from the corresponding DTO
*/
D update(E entity, D dto);
/**
* Deletes entity from databse
*/
void delete(E entity);
}
@Service("apartmentService")
@RequiredArgsConstructor(onConstructor = @_(@Autowired)) //use @__(@Autowired) for jdk 11
public ApartmentService implements RealEstateService<Apartment,ApartmentDto> {
protected final RealEstateRepository realEstateRepository;
@Override
public ApartmentDto get(Apartment apartment){
ApartmentDto dto = new ApartmentDto();
dto.setId(apartment.getId());
...
return dto;
}
@Override
public ApartmentDto save(ApartmentDto dto){
Apartment entity= new Apartment();
entity.setArea(dto.getArea());
...
// Business logic if exists
realEstateRepository.save(entity);
return get(entity);
}
@Override
public ApartmentDto update(Apartment entity, ApartmentDto dto){
entity.setArea(dto.getArea());
...
// Business logic if exists
realEstateRepository.save(entity);
return get(entity);
}
@Override
public void delete(Apartment entity){
// Business logic if exists
realEstateRepository.delete(entity);
}
}
@Service("houseService")
@RequiredArgsConstructor(onConstructor = @_(@Autowired)) //use @__(@Autowired) for jdk 11
public HouseService implements RealEstateService<House,HouseDto> {
//Use the samelogic as ApartmentService
...
}
@Service("landService")
@RequiredArgsConstructor(onConstructor = @_(@Autowired)) //use @__(@Autowired) for jdk 11
public LandService implements RealEstateService<Land,LandDto> {
//Use the samelogic as ApartmentService
...
}
Manager Layer
The management layer has the responsibility of calling the right services for the incoming requests. To do so, we will extract the CrudHandler
annotation from the Entity/DTO we have to get the associated bean name. Then we will get the bean from the Spring context in order to call the desired CRUD operation.
@Service
@Transactional
@RequiredArgsConstructor(onConstructor = @_(@Autowired)) //use @__(@Autowired) for jdk 11
public class RealEstateManager {
private final ApplicationContext appContext;
private final RealEstateRepository realEstateRepository;
public RealEstateDto get(Integer id){
Optional<RealEstate> opt = realEstateRepository.findById(id);
//manage Optional is empty
RealEstate entity = opt.get();
CrudHandler annotation = entity.getAnnotation(CrudHandler.class);
//check annotation not null
RealEstateService service = appContext.getBean(annotation.beanName,RealEstateService.class);
//check serviceis not null
return service.get(entity);
}
public RealEstateDto save(RealEstateDto dto){
CrudHandler annotation = dto.getAnnotation(CrudHandler.class);
//check annotation not null
RealEstateService service = appContext.getBean(annotation.beanName,RealEstateService.class);
//check serviceis not null
return service.save(dto);
}
public RealEstateDto update(Integer id, RealEstateDto dto){
Optional<RealEstate> opt = realEstateRepository.findById(id);
//manage Optional is empty
RealEstate entity = opt.get();
CrudHandler annotation = entity.getAnnotation(CrudHandler.class);
//check annotation not null
RealEstateService service = appContext.getBean(annotation.beanName,RealEstateService.class);
//check serviceis not null
return service.update(entity, dto);
}
public void delete(Integer id){
Optional<RealEstate> opt = realEstateRepository.findById(id);
//manage Optional is empty
RealEstate entity = opt.get();
CrudHandler annotation = entity.getAnnotation(CrudHandler.class);
//check annotation not null
RealEstateService service = appContext.getBean(annotation.beanName,RealEstateService.class);
//check serviceis not null
return service.delete(entity);
}
}
Conclusion
In this post, we have seen a new manner for designing a scalable CRUD web service. In order to support a new subclass, all we have to do is to define the corresponding entity, DTO, and service class without any modification in other layers.