Design a CRUD Web Service for Inheritable Entity

Introduction

Sometimes we need to develop a web service that provides CRUD (Creport, Rlead, 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 requests
  • EntityManager: Dispatches the requests received by the controller to the correct subentity service
  • EntityService: Generic interface that defines the CRUD operations for subentities
  • SubEntityNService: Specific implementation of CRUD operations for the subentity N; it contains the code that performs the subentity mapping and the business logic
  • Repository: 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.

.

Leave a Comment