II. Spring Boot - Setting up CRUD operations with DRY principle : Don’t Repeat Yourself

elevysi · 05 April 2019 |

We have created a database and set up the security configurations; next we need to create methods to which the created configurations will apply.

Business logic is often the same boiler plate code; we know for a fact that some methods will be similar across all the entities of our domain model. That is why I prefer implementing abstract classes for services and data access objects (dao) such that the common methods are abstracted and do not need to be written by the specific classes.

Let’s start with the Data access layer by implementing an AbstractDAO interface and an AbstractDAOImplement class to implement the created interface. Both classes will be paramterized with an the entity class type and its primary key type so that we can execute creates, retrievals, edits and deletes on the entity given the primary key.

package com.tutorial.basic.dao;

import java.util.List;

public interface AbstractDAO<E, K> {
	
	public E findByID(K key);
	public E save(E entity);
	public E edit(E entity);
	public void delete(E entity);
	public List<E> findAll();
}

AbstractDAOImplement is implemented as follows:

package com.tutorial.basic.dao;


import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;

import org.springframework.stereotype.Repository;

@Repository
@Transactional
public abstract class AbstractDAOImplement<E, K> implements AbstractDAO<E, K>{
	
	@PersistenceContext
	private EntityManager em;
	private Class<E> type;
	
	
	public AbstractDAOImplement(){
		Type t = getClass().getGenericSuperclass();
		ParameterizedType pt = (ParameterizedType) t;
		type = (Class) pt.getActualTypeArguments()[0];
	}
	
	public E findByID(K key){
		return em.find(type, key);
	}
	public E save(E entity){
		em.persist(entity);
		em.flush();
		return entity;
	}
	public E edit(E entity){
		em.merge(entity);
		em.flush();
		return entity;
	}
	public void delete(E entity){
		if(entity != null){
			em.merge(entity);
			em.remove(entity);
		}
	}
	
	public List<E> findAll(){
		CriteriaBuilder builder = em.getCriteriaBuilder();
		CriteriaQuery<E> criteria = builder.createQuery(type);
		Root<E> root = criteria.from(type);
		criteria.select(root);
		
		TypedQuery<E> query = em.createQuery(criteria);
		
		return query.getResultList();
	}

}

Once the abstract DAO layer is defined, we need to implement the actual classes that will extend the abstract classes.

package com.tutorial.basic.dao;


import com.tutorial.basic.model.Post;

public interface PostDAO extends AbstractDAO<Post, Long> {
}

And PostDAOImplement

package com.tutorial.basic.dao;


import javax.transaction.Transactional;

import org.springframework.stereotype.Repository;

import com.tutorial.basic.model.Post;

@Repository
@Transactional
public class PostDAOImplement extends AbstractDAOImplement<Post, Long> implements PostDAO{

}

The Service layer follows the same approach as the data access layer, i.e. a parameterized abstract service interface, AbstratService, declaring the common method that always recur, i.e. findByID, findAll, save, edit and delete. The interface parameters correspond to the entity class and its primary key type, the two elements needed to abstract the common methods declared above.
An abstract implementation class is also put in place with calls to abstract DAO. The formal is also an interface, parameterized with the entity class and the primary key type as explained earlier.
Below are the snippets from the abstract interfaces and their implementations:

package com.tutorial.basic.service;

public interface AbstractService <E, K>{
	
	public E findByID(K key);
	public E save(E entity);
	public E edit(E entity);
	public void delete(E enity);
}

AbstractServiceImplement

package com.tutorial.basic.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.tutorial.basic.dao.AbstractDAO;


@Service
public abstract class AbstractServiceImplement<E, K> implements AbstractService<E, K>{
	
	@Autowired
	private AbstractDAO<E, K> abstractDAO;
	
	public E findByID(K key){
		return abstractDAO.findByID(key);
	}
	public E save(E entity){
		return abstractDAO.save(entity);
	}
	public E edit(E entity){
		return abstractDAO.edit(entity);
	}
	public void delete(E entity){
		abstractDAO.delete(entity);
	}
	
	public List<E> findAll(){
		return abstractDAO.findAll();
	}

}

The PostService will hence only extend the AbstractService and include specify the actual values of the abstract class parameters.

package com.tutorial.basic.service;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.tutorial.basic.dao.PostDAO;
import com.tutorial.basic.model.Post;

@Service
public class PostService extends AbstractServiceImplement<Post, Long> {
	
	private PostDAO postDAO;
	
	@Autowired
	public PostService(PostDAO postDAO){
		this.postDAO = postDAO;
	}

}

Controller is implemented as shown below:

package com.tutorial.basic.controller;

import java.util.List;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.tutorial.basic.model.Post;
import com.tutorial.basic.service.PostService;
import com.tutorial.common.dto.UserDTO;

@Controller
@RequestMapping(value="/ui/posts/")
public class PostController {
	
	private PostService postService;
		
	@Autowired
	public PostController(PostService postService){
		this.postService = postService;
	}
	
	@GetMapping(value="/")
	public String posts(Model model){
		
		model.addAttribute("posts", postService.findAll());
		return "indexPosts";
	}
	
	
	@GetMapping(value="/add")
	public String addPost(Model model){
		Post post = new Post();
		model.addAttribute("post", post);
		
		return "addPost";
	}
	
	@PostMapping(value="/add")
	public String doAddPost(@Valid Post post, BindingResult result, Model model){
		
		if(result.hasErrors()){
			return "addPost";
		}
		
		postService.save(post);
		return "redirect:/ui/posts/";
	}
	
	
	@GetMapping(value="/edit/{id}")
	public String editPost(Model model, @PathVariable("id")Long id){
		Post post = postService.findByID(id);
		if(post != null){
			model.addAttribute("post", post);
			return "editPost";
		}
		
		return "redirect:/ui/posts/";
	}
	
	@PostMapping(value="/edit/{id}")
	public String doEditPost(@Valid Post post, BindingResult result, Model model, @PathVariable("id")Long id){
		
		if(result.hasErrors()){
			return "editPost";
		}
		
		postService.edit(post);
		return "redirect:/ui/posts/";
	}
	
	
	@GetMapping(value="/delete/{id}")
	public String deletePost(Model model, @PathVariable("id")Long id){
		Post post = postService.findByID(id);
		if(post != null){
			postService.delete(post);
		}
		return "redirect:/ui/posts/";
	}
	
}

Corresponding views have been left out for briefty and can be retrieved from github. Next, let's set another service up, one that implements a local authentication scheme.