导读
在目前接触过的项目中大多数的项目都会涉及到: crud相关的操作, 哪如何优雅的编写crud操作呢?
带着这个问题,我们发现项目中大量的操作多是 创建实体 、删除实例、 修改实体、 查询单个实体、 分页查询多个实体, 我们有没有好的方式解决呢?
下面我给出crud编写的四种方式 循序渐进 ,并分析其优势劣势,希望有一种能适合你,如果你有其他方式可以留言讨论,在此权当抛砖引玉。
以下内容基于Spring Boot 、Spring MVC、 Spring Data JPA 如果你使用的也是相同的技术栈可以继续往下阅读,如果不是可以当作参考。
crud编写的四种方式
1 裸写crud
最简单最粗暴也是使用最多的一种方式,在写的多了之后可以用生成工具生成。
import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
/**
* @author yangrd
* @date 2019/3/4
**/
@AllArgsConstructor
@RestController
@RequestMapping("/api/banner")
public class BannerController {
private BannerRepository repository;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Banner save(Banner banner) {
return repository.save(banner);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
repository.deleteById(id);
}
@PutMapping("/{id}")
public void update(@PathVariable("id") Banner db, @RequestBody Banner banner) {
BeanUtils.copyProperties(banner, db);
repository.save(db);
}
@GetMapping
public Page<Banner> findAll(Pageable pageable) {
return repository.findAll(pageable);
}
@GetMapping("/{id}")
public Banner finOne(@PathVariable("id") Banner banner) {
return banner;
}
}
优势:能控制到代码的每一行并非所有的增删改查都需要
劣势:在业务简单的情况下会编写大量的类似代码 这个时候我们可以用泛型与继承解决 引出 AbstractCrudController
2 extend BaseCrudController
使用抽象的能力,通过抽象类对相同的代码进行封装,减少子类继续编写重复的代码。
import com.st.cms.common.spring.jpa.AbstractEntity;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
/**
* @author yangrd
* @date 2019/3/1
**/
public abstract class BaseCrudController<T extends AbstractEntity, D extends JpaRepository<T, String>> {
@Autowired
protected D repository;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public T save(@RequestBody T t) {
return repository.save(t);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable("id") String id) {
repository.deleteById(id);
}
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void update(@PathVariable("id") T dbData, @RequestBody T t) {
BeanUtils.copyProperties(t, dbData,"id");
repository.saveAndFlush(dbData);
}
@GetMapping("/{id}")
public T get(@PathVariable("id") T t) {
return t;
}
}
-
import com.st.cms.common.spring.mvc.BaseCrudController;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author yangrd
* @date 2019/3/4
**/
@AllArgsConstructor
@RestController
@RequestMapping("/api/banner")
public class BannerController extends BaseCrudController<Banner,BannerRepository> {
}
优势:在简单的crud操作中通过泛型与继承减少编写大量增删改查的方法
劣势:在findAll方法中入参数不好控制,通过HttpServletRequest可以解决这个问题 但有会引入大量的获取值的方法 因此BaseCrudController中不提供 findAll 方法 由用户编写
3 spring data rest
引入spring-boot-starter-data-rest,crud操作可以直接http调用 ,感兴趣的可以翻看 官方文档
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
优势:spring 家的东西 可以很好的与spring boot 整合 只需引入一个依赖即可
劣势:和之前业务中返回的数据格式内容不同 (此处也是好处 更统一规范 ,如果一开始前后端约定好数据格式就没有什么太大的问题)
4 ControllerHelper
重点来了哈哈 直接上代码
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.*;
import javax.persistence.EntityNotFoundException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import static org.springframework.beans.BeanUtils.getPropertyDescriptor;
import static org.springframework.beans.BeanUtils.getPropertyDescriptors;
/**
* @author yangrd
* @date 2019/3/1
**/
@Slf4j
@RestController
@RequestMapping("/api")
public class ControllerHelper implements ApplicationContextAware {
private MappingManager mappingManager;
@PostMapping("/{repository}")
public ResponseEntity create(@PathVariable String repository, @RequestBody String reqJSON) {
return mappingManager.getJpaRepository(repository).map(repo -> {
Object object = mappingManager.getEntityObj(repository);
Object req = JSON.parseObject(reqJSON, object.getClass());
BeanUtils.copyProperties(req, object);
return ResponseEntity.status(HttpStatus.CREATED).body(repo.saveAndFlush(object));
}).
orElseGet(() -> ResponseEntity.notFound().build());
}
@DeleteMapping("/{repository}/{id}")
public ResponseEntity delete(@PathVariable String repository, @PathVariable Long id) {
return mappingManager.getJpaRepository(repository).map(repo -> {
repo.deleteById(id);
return ResponseEntity.noContent().build();
}).
orElseGet(() -> ResponseEntity.notFound().build());
}
@PutMapping("/{repository}/{id}")
public ResponseEntity update(@PathVariable String repository, @PathVariable Long id, @RequestBody String reqJSON) {
return mappingManager.getJpaRepository(repository).map(repo -> {
repo.findById(id).ifPresent(db -> {
Object req = JSON.parseObject(reqJSON, db.getClass());
BeanUtils.copyProperties(req, db);
repo.saveAndFlush(db);
});
return ResponseEntity.noContent().build();
}).
orElseGet(() -> ResponseEntity.notFound().build());
}
@GetMapping("/{repository}/{id}")
public ResponseEntity get(@PathVariable String repository, @PathVariable Long id) {
return mappingManager.getJpaRepository(repository).map(repo -> ResponseEntity.ok(repo.findById(id))).
orElseGet(() -> ResponseEntity.notFound().build());
}
@GetMapping("/{repository}")
public ResponseEntity get(@PathVariable String repository, Pageable pageable) {
return mappingManager.getJpaRepository(repository).map(repo -> ResponseEntity.ok(repo.findAll(pageable))).
orElseGet(() -> ResponseEntity.notFound().build());
}
class MappingManager {
private Map<String, JpaRepository> entity4Repositories = new HashMap<>();
private Map<String, Class> entity4Class = new HashMap<>();
MappingManager(ApplicationContext applicationContext) {
Map<String, JpaRepository> repositoryBeans = applicationContext.getBeansOfType(JpaRepository.class);
repositoryBeans.forEach((repositoryName, repositoryBean) -> {
Class entityClass = MappingSupport.getEntityClass(repositoryBean);
String entityClassName = MappingSupport.getEntityName(entityClass);
entity4Repositories.put(entityClassName, repositoryBean);
entity4Class.put(entityClassName, entityClass);
});
}
public Optional<JpaRepository> getJpaRepository(String repository) {
return Optional.ofNullable(entity4Repositories.get(repository));
}
public Object getEntityObj(String repository) {
return Optional.ofNullable(entity4Class.get(repository)).map(clazz -> {
try {
return clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}).orElseThrow(EntityNotFoundException::new);
}
}
static class MappingSupport {
static Class getEntityClass(JpaRepository jpaRepository) {
Type[] jpaInterfacesTypes = jpaRepository.getClass().getGenericInterfaces();
Type[] type = ((ParameterizedType) ((Class) jpaInterfacesTypes[0]).getGenericInterfaces()[0]).getActualTypeArguments();
return (Class) type[0];
}
static String getEntityName(Class clazz) {
String simpleName = clazz.getSimpleName();
return simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1);
}
}
/**
* @author yangrd
* @date 2018/8/30
* @see org.springframework.beans.BeanUtils#copyProperties(Object, Object, Class, String...)
**/
static class BeanUtils {
public static <T> T map(Object source, Class<T> targetClass) {
T targetObj = null;
try {
targetObj = targetClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
copyProperties(source, targetObj);
return targetObj;
}
/**
* 只拷贝不为null的属性
*
* @param source the source bean
* @param target the target bean
* @throws BeansException if the copying failed
*/
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, null, (String[]) null);
}
/**
* 只拷贝不为null的属性
*
* @param source the source bean
* @param target the target bean
* @param editable the class (or interface) to restrict property setting to
* @param ignoreProperties array of property names to ignore
* @throws BeansException if the copying failed
* @see BeanWrapper
*/
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null) ? Arrays.asList(ignoreProperties) : null;
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreProperties == null || (!ignoreList.contains(targetPd.getName())))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (value != null) {
//只拷贝不为null的属性 by zhao
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
} catch (Throwable ex) {
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Assert.notNull(applicationContext, "");
this.mappingManager = new MappingManager(applicationContext);
}
}
-
优势:对spring data rest弱势的补充可以在不改变 之前习惯的数据格式的情况下狠方便的前移, 最重要的是相比于 Abstract 方法可以被覆盖 如 findAll 如果你使用 如 api/user/{id} spring mvc会优先匹配它 而不是 api/{repository}/{id} ,后期可以根据自身业务需要打成jar包放在私服上面方便其他项目使用