一、设计理念
 在互联网项目或者其他传统Web项目的开发过程中,对数据库的操作可以说是项目的核心和根本。而数据库操作无非就是增删改查,而对所有表的增删改查操作其实大同小异,非常相似。如果我们能够将对单表的增删改查操作抽象出来放在父类中去实现,让所有的子类继承这个已实现了增删改查操作的父类,那么我们的工作效率无疑会大大地提升,也会使我们从这些简单无聊但又不得不去做的工作中解脱出来,从而将有限的精力投入到较为复杂的业务实现中去。

二、实现思路
 有了想法,就要着手去做。
 虽说表的操作都是增删改查、但要将不同的表的增删改查操作用同一个类去完成,就必须让这个类有一个特性:它必须能同时代表其他的所有表。学过Java的都知道,可以使用类的继承来实现——只需要让所有的表对应的实体继承自同一个父类即可。没错,就是这么简单。
 现在我们已经知道如何用一个实体来代表其他的所有实体——通过继承。但是具体到真正的增删改查操作的时候,又怎么让程序知道具体操作的是哪一个实体对应的表呢?这个可以利用泛型来解决,我们在继承父类时给定具体的泛型类即可。
 你可能会觉得在项目中使用到泛型的场景不是很多,但是我想在你看了这个专栏之后,会有所改观。其实在Java项目的开发中,如果能够合理的利用泛型和反射,尤其是对反射的利用,是可以解决几乎所有的问题的,而且解决的方式异常简单。

三、编码过程
 有了实现的思路,下面就可以开始编码了。如上所述,我们需要一个公共的实体的父类,然后让其他的需要映射为表的实体都继承自该父类。那么这个父类该怎么设计呢?最简单的就是编写一个任何属性都没有的基类,但这样做就有点太浪费了,我们可以把一些共有的属性都提取到父类中来,比如:id、createTime、lastUpdateTime、delFlag(作为逻辑删除的标记)等。我将这个父类命名为BaseEntity,其设计如下:

package com.rbl.basement.base.base;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.format.annotation.DateTimeFormat;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Data
@MappedSuperclass
public abstract class BaseEntity implements Serializable {

    @Id
    @GeneratedValue(generator = "idGenerator")
    @GenericGenerator(name = "idGenerator", strategy = "uuid")
    private String id;

    private Boolean delFlag;

    @Temporal(TemporalType.TIMESTAMP)
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    @Temporal(TemporalType.TIMESTAMP)
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date lastUpdateTime;
}

 这是一个抽象类,为什么要将其设计为一个抽象类呢?因为这个公共基类是不需要映射为表的,而且也不需要创建它的任何对象,它仅仅是作为父类来继承而已,起到一个象征作用。而且将其设计为抽象类还有其他的用途,这个我们后面会提到。
 这里使用到了lombok插件,在build.gradle中已经引入了相关依赖,有了lombok我们可以使用几个简单的注解来实现getter和setter等方法,而且还支持链式调用等,这样就免去了手动编写基本代码的麻烦。关于lombok的使用和配置可以参考:IDEA配置Lombok
 这里对使用到的几个注解做一下说明:
  1️⃣@MappedSuperclass:指明这个父类的属性可被子类实体继承并可生成对应的表字段,若不加该注解,子类继承这个类后生成的表中是不会有id、del_flag、create_time和last_update_time字段的
  2️⃣@Id:指明其标注的属性对应的字段是表的主键
  3️⃣@GeneratedValue和@GenericGenerator:这两个注解共同实现了主键的生成策略,这里使用的是hibernate提供的uuid的方式,当然也支持其他的方式或者自定义生成主键的策略,需要注意的是generator和name属性的值要一致
 仅有BaseEntity当然是不够的,我们在基于SpringBoot或SpringMVC的Web项目开发中,除了要定义实体,还要定义controller、service和repository,下面就说一下BaseController、BaseService和BaseRepository的设计。在上面提到过,我们在具体的增删改查操作时如何明确的知道操作的是哪个类的对象需要使用泛型,因此这些Base类的设计都离不开泛型。
 首先看一下BaseController的设计。在BaseController中我们除了需要定义所有controller都必须实现的基本操作外,还需要定义的是数据的响应方式和响应数据的格式,因为controller层是一个应用的后台与前端交互的媒介。在前后端分离的开发模式中,数据响应的格式大多是JSON,我们也采用这种格式,这没什么可说的。这里想说的是我们应该设计一种简便的响应方式,让其他的controller在继承这个BaseController后能够以最简便的方式给前端做出响应,这就涉及到响应类的设计。
 后台在给前端做出响应的时候,应该有一个响应码来标识响应的类型(成功、失败还是异常等)、响应的信息(主要是响应异常或失败时的提示信息)和响应数据(对于查询操作等在响应成功时是需要响应数据的),我们的响应类RespDto的设计如下:

package com.rbl.basement.base.response;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Setter;
import lombok.experimental.Accessors;

@Data
@AllArgsConstructor
public class RespDto {
    private String code;
    private String message;
    private Object data;

    @Setter
    @Accessors(chain = true)
    public static class Builder {
    	// 默认状态:成功
        private String code = RespCode.SUCCESS;
        private String message = "success";
        private Object data;

        public RespDto build() {
            return new RespDto(this.code, this.message, this.data);
        }
    }
}

 这里用到了静态内部类,这个设计比较值得玩味——通过静态内部类对象的方法来构建一个外部类的对象,它的妙用就是使后台响应异常简便,这个在后面会深有体会。
 除了这个响应类,我们还需要一个响应代码的常量类(既然是常量类我们不妨将其定义为interface)来标准化我们的响应码,让前端知道哪个响应码表示成功、哪个响应码表示失败或异常,形成一个共同的约定,也便于前端统一对响应码做出处理。响应码的设计如下:

public interface RespCode {
    /**
     * 成功
     */
    String SUCCESS = "M200";
    /**
     * 失败
     */
    String FAILURE = "M400";
    /**
     * 异常
     */
    String ERROR = "M500";
}

 下面是我们的重点——BaseController,其中有对通用的方法的提取也有对响应的具体实现:对于BaseController来说,对于响应方式的实现更为重要和有意义,因为后面的所有controller都会直接调用这些方法来做出响应

package com.rbl.basement.base.base;

import com.rbl.basement.base.response.RespCode;
import com.rbl.basement.base.response.RespDto;

import java.util.List;

public abstract class BaseController<T extends BaseEntity> {

    /**
     * 新增或修改
     *
     * @param t
     * @return
     */
    public abstract RespDto saveOrUpdate(T t);

    /**
     * 批量新增或修改
     *
     * @param list
     * @return
     */
    public abstract RespDto saveOrUpdateBatch(List<T> list);

    /**
     * 删除
     *
     * @param t
     * @return
     */
    public abstract RespDto delete(T t);

    /**
     * 批量删除
     *
     * @param t
     * @return
     */
    public abstract RespDto deleteBatch(List<T> t);

    /**
     * 据主键查询
     *
     * @param t
     * @return
     */
    public abstract RespDto findById(T t);

    /**
     * 按条件查询一个
     *
     * @param t
     * @return
     */
    public abstract RespDto findOne(T t);

    /**
     * 条件查询
     *
     * @param t
     * @return
     */
    public abstract RespDto findByEntity(T t);

    /**
     * 查询所有
     *
     * @param t
     * @return
     */
    public abstract RespDto queryAll(T t);

    /**
     * 分页查询
     *
     * @param t
     * @return
     */
    public abstract RespDto queryByPage(T t);

    /**
     * 响应成功:无响应数据
     *
     * @return
     */
    public RespDto success() {
        return new RespDto.Builder().build();
    }

    /**
     * 响应成功:有响应数据
     *
     * @param data
     * @return
     */
    public RespDto success(Object data) {
        return new RespDto.Builder().setData(data).build();
    }

    /**
     * 响应失败:自定义失败消息
     *
     * @param message
     * @return
     */
    public RespDto failure(String message) {
        return new RespDto.Builder().setCode(RespCode.FAILURE).setMessage(message).build();
    }

    /**
     * 响应异常:自行义异常消息
     *
     * @param message
     * @return
     */
    public RespDto error(String message) {
        return new RespDto.Builder().setCode(RespCode.ERROR).setMessage(message).build();
    }
}

 可以看到,BaseControler中的所有方法的返回值类型都是我们定义的RespDto类型,其实严格地说,以后所有的controller方法的返回值类型都必须是RespDto,这是一种规范。
 BaseService的设计如下:其实就是对常用基本操作的抽象

package com.rbl.basement.base.base;

import org.springframework.data.domain.Page;

import java.util.List;

public interface BaseService<T extends BaseEntity> {

    T saveOrUpdate(T t);

    Iterable<T> saveOrUpdateBatch(List<T> list);

    void delete(T t);

    void deleteBatch(List<T> list);

    T findById(T t);

    T findOne(T t);

    Iterable<T> findByEntity(T t);

    Iterable<T> queryAll(T t);

    Page<T> queryByPage(T t);
}

 而对于BaseRepository的设计就要简单的多了,因为我们是基于JPA的,可以直接继承JPA的相关接口,JPA已为我们实现了具体的增删改查等操作,我们也可以使用JPA来实现我们自定义的数据库操作,下面是BaseRepository的代码:

package com.rbl.basement.base.base;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface BaseRepository<T extends BaseEntity> extends PagingAndSortingRepository<T, String>, JpaSpecificationExecutor<T> {

}

 至此,我们底层的设计基本上就完成了。需要注意一个细节,就是这些类的泛型都是基于我们创建的BaseEntity的,这一点非常重要。
 完成底层的设计其实意义并不大,最重要的是要提供默认的实现,这样各个子类才无需自行实现,也是这个项目的最核心的意义所在,接下来讲述的就是增删改查操作在父类中的具体实现。