基於Java的泛型和反射實現通用的增刪改查(2)——通用增刪改查操作的底層設計

一、設計理念
 在互聯網項目或者其他傳統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的,這一點非常重要。
 完成底層的設計其實意義並不大,最重要的是要提供默認的實現,這樣各個子類才無需自行實現,也是這個項目的最核心的意義所在,接下來講述的就是增刪改查操作在父類中的具體實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章