溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

SpringDataJpa怎么實(shí)體對(duì)象增強(qiáng)設(shè)計(jì)

發(fā)布時(shí)間:2021-10-18 14:19:16 來源:億速云 閱讀:125 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“SpringDataJpa怎么實(shí)體對(duì)象增強(qiáng)設(shè)計(jì)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“SpringDataJpa怎么實(shí)體對(duì)象增強(qiáng)設(shè)計(jì)”吧!

實(shí)體增強(qiáng)插件第一版

需求

在日常的 Java-web 開發(fā)過程中時(shí)常需要做一些單表的數(shù)據(jù)操作,常用操作有:單表的新增、單表根據(jù)ID查詢,單表根據(jù)ID刪除,單表根據(jù)ID修改。對(duì)于這四種單表的基本操作時(shí)常需要編寫很多重復(fù)代碼如何避免編寫重復(fù)編寫這類代碼成了一個(gè)問題。面對(duì)這樣的一個(gè)問題我們常規(guī)的解決方案有代碼生成器,代碼生成器可以通過數(shù)據(jù)庫建表語句直接得到Controoler、service、dao三者從而避免重復(fù)編寫。除此之外筆者思考了另一種處理方式,不通過代碼生成器通過一個(gè)注解來完成上述操作。

設(shè)計(jì)

首先需要考慮的是四種單表操作的API設(shè)計(jì),一般情況下筆者會(huì)定義這樣的幾個(gè)API,下面是關(guān)于新增的API筆者會(huì)設(shè)計(jì)出如下接口。

  • 以用戶做新增舉例

POST http://host:port/user
Content-Type: application/json

// 添加參數(shù)
{}
  • 以部門為例做新增舉例

POST http://host:port/dept
Content-Type: application/json

// 添加參數(shù)
{}

對(duì)于上述兩個(gè)API設(shè)計(jì)可以看到一些大同小異的地方,相同的有都是通過POST進(jìn)行請(qǐng)求,不同的是后面的路由地址和參數(shù),對(duì)于這樣兩組接口可以抽象為下面一個(gè)接口

  • 抽象后的新增接口

POST http://host:port/{entity_name}
Content-Type: application/json

// 添加參數(shù)
{}

同樣的其他3個(gè)操作也可以通過類似的方式進(jìn)行抽象。

  • 根據(jù)ID查詢接口

GET http://host:port/{entity_name}/{id}
  • 修改接口

PUT http://host:port/{entity_name}
Content-Type: application/json

// 修改參數(shù)
{}
  • 根據(jù)ID刪除接口

DELETE http://host:port/{entity_name}/{id}

基礎(chǔ)接口設(shè)計(jì)完成,可以先將基本的Controller代碼編寫完成。

@RestController
public class EntityPluginController {

  @GetMapping("/{entityPluginName}/{id}")
  public ResponseEntity<Object> findById(
      @PathVariable("entityPluginName") String entityPluginName,
      @PathVariable("id") String id
  ) {
    return null;
  }


  @PostMapping("/{entityPluginName}")
  public ResponseEntity<Object> save(
      @PathVariable("entityPluginName") String entityPluginName,
      @RequestBody Object insertParam
  ) {
    return null;
  }

  @PutMapping("/{entityPluginName}")
  public ResponseEntity<Object> update(
      @PathVariable("entityPluginName") String entityPluginName,
      @RequestBody Object updateParam
  ) {
	return null;
  }

  @DeleteMapping("/{entityPluginName}/{id}")
  public ResponseEntity<Object> deleteById(
      @PathVariable("entityPluginName") String entityPluginName,
      @PathVariable("id") String id
  ) {

    return null;
  }
}

本文使用JPA作為數(shù)據(jù)交互層,以JAP作為交互會(huì)有2個(gè)關(guān)鍵對(duì)象,第一個(gè)是數(shù)據(jù)庫實(shí)體,第二個(gè)是Repository接口。通常情況下會(huì)選擇CrudRepository接口來作為數(shù)據(jù)交互層的根對(duì)象,也有會(huì)選擇JpaRepository接口來作為數(shù)據(jù)交互的根對(duì)象,這兩種存在間接引用,類圖如下

SpringDataJpa怎么實(shí)體對(duì)象增強(qiáng)設(shè)計(jì)

了解了常用的JPA操作對(duì)象后來看一個(gè)Entity對(duì)象

@Entity
@Table(name = "oauth_client", schema = "shands_uc_3_back", catalog = "")
public class OauthClientEntity {

  private Long id;
  private String clientId;
  private String clientSecurity;
  private String redirectUri;
  private Long version;
    // 省略getter&setter
}

在單表開發(fā)過程中我們做的所有行為都是圍繞這個(gè)數(shù)據(jù)庫實(shí)體進(jìn)行操作,比如在新增的時(shí)候?qū)⑿略鰠?shù)轉(zhuǎn)換成數(shù)據(jù)庫對(duì)象,在更新的是將更新參數(shù)轉(zhuǎn)換成數(shù)據(jù)庫對(duì)象,在根據(jù)ID查詢的時(shí)候?qū)⒉樵兘Y(jié)果(數(shù)據(jù)庫對(duì)象)轉(zhuǎn)換為返回結(jié)果對(duì)象,總共存在三種數(shù)據(jù)庫對(duì)象的轉(zhuǎn)換,這三種轉(zhuǎn)換是必不可少的,當(dāng)然也可以用一個(gè)數(shù)據(jù)庫對(duì)象直接來滿足這個(gè)操作從而減少代碼量(不建議這么做),對(duì)于這三種轉(zhuǎn)換先來定義一個(gè)接口,該接口表示了三種對(duì)象的轉(zhuǎn)換過程。

  • 數(shù)據(jù)庫對(duì)象的三種轉(zhuǎn)換

public interface EntityConvert<InsType, UpType, ResType, EntityType> {

  /**
   * convert data from insert param db entity
   *
   * @param insType insert param
   * @return db entity
   */
  EntityType fromInsType(InsType insType);

  /**
   * convert data from update param to db entity
   *
   * @param upType update param
   * @return db entity
   */
  EntityType fromUpType(UpType upType);

  /**
   * convert data from db entity to response entity
   *
   * @param entityType db entity
   * @return response entity
   */
  ResType fromEntity(EntityType entityType);

}

EntityConvert 接口中定義了4個(gè)泛型,含義如下

  1. InsType:新增時(shí)的參數(shù)類型

  2. UpType:修改時(shí)的參數(shù)類型

  3. ResType:返回時(shí)的參數(shù)類型

  4. EntityType:數(shù)據(jù)庫實(shí)體類型

完成接口定義后需要將這個(gè)接口的實(shí)現(xiàn)類和實(shí)體對(duì)象綁定,最簡單的一種綁定模式就是通過注解來表示,注解定義如下

@java.lang.annotation.Target({ElementType.TYPE})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Documented
@java.lang.annotation.Inherited
public @interface EntityPlugin {

  /**
   * name
   *
   * @return name
   */
  String name();

  /**
   * {@link EntityConvert} class
   *
   * @return class
   */
  Class<? extends EntityConvert> convertClass() default EntityConvert.class;
}

注解兩個(gè)函數(shù)的含義:

  1. name表示實(shí)體名稱,

  2. convertClass表示轉(zhuǎn)換器實(shí)現(xiàn)類

下面將注解和實(shí)體類進(jìn)行綁定,具體代碼如下

@EntityPlugin(name = "oauthClient")
@Entity
@Table(name = "oauth_client", schema = "shands_uc_3_back", catalog = "")
public class OauthClientEntity {}

注意:筆者在這里沒有自定義實(shí)現(xiàn) EntityConvert 接口,采用的是默認(rèn)方式,即參數(shù)等于數(shù)據(jù)庫對(duì)象

在完成實(shí)體對(duì)象和轉(zhuǎn)換對(duì)象之間的關(guān)系綁定后后需要做到的事情是如何調(diào)用JPA框架將數(shù)據(jù)插入。解決這個(gè)問題首先需要從JPA接口入手,在JPA接口中都需要定義兩個(gè)泛型,第一個(gè)泛型是實(shí)體對(duì)象,第二個(gè)泛型是ID類型,我們需要通過實(shí)體對(duì)象來獲取前文所編寫的注解信息,使用ID泛型為根據(jù)ID查詢提供參數(shù)支持。下面是存儲(chǔ)上述信息的對(duì)象。

public class EntityPluginCache {

  private String name;
  private Class<? extends EntityConvert> convertClass;
  private CrudRepository crudRepository;
  private Class<?> self;
  private Class<?> idClass;
}
  1. name表示注解EntityPlugin的name屬性

  2. convertClass 表示EventConvert實(shí)現(xiàn)類的類型

  3. crudRepository 表示JPA數(shù)據(jù)庫操作對(duì)象

  4. self 表示實(shí)體類類型

  5. idClass 表示實(shí)體類的ID數(shù)據(jù)類型

完成這些后我們需要解決的問題就是如何從JPA接口提取類和ID類型,如下面代碼所示,我們需要提取CrudRepository的兩個(gè)泛型

@Repository
public interface OauthClientRepo extends CrudRepository<OauthClientEntity ,Long> {

}

這里需要使用反射,具體操作代碼如下:

public class InterfaceReflectUtils {

  private InterfaceReflectUtils() {

  }

  public static List<Class<?>> getInterfaceGenericLasses(Class<?> check, Class<?> targetClass) {

    if (check == null || targetClass == null) {
      return Collections.emptyList();
    }
    List<Class<?>> res = new ArrayList<>();

    Class<?> cur = check;

    while (cur != null && cur != Object.class) {
      Type[] types = cur.getGenericInterfaces();
      for (Type type : types) {

        // todo: 修改為可以根據(jù)類型進(jìn)行推論
        if (type.getTypeName().contains(targetClass.getName())) {
          Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
          for (Type typeArgument : typeArguments) {
            if (typeArgument instanceof Class) {
              res.add((Class<?>) typeArgument);
            }
          }
          break;

        }
      }
      Class<?>[] interfaces = cur.getInterfaces();
      if (interfaces != null) {
        for (Class<?> inter : interfaces) {
          List<Class<?>> result = getInterfaceGenericLasses(inter, targetClass);
          if (result != null) {
            res.addAll(result);
          }
        }
      }
      cur = cur.getSuperclass();
    }

    return res;
  }


}

在得到兩個(gè)泛型數(shù)據(jù)后需要進(jìn)行數(shù)據(jù)解析和對(duì)象組裝并將數(shù)據(jù)存儲(chǔ),數(shù)據(jù)存儲(chǔ)對(duì)象如下

public class EntityPluginCacheBean {

  public Map<String, EntityPluginCache> getCacheMap() {
    return cacheMap;
  }

  private final Map<String, EntityPluginCache> cacheMap = new ConcurrentHashMap<>(64);

}

接口解析代碼如下:

@Component
public class EntityPluginRunner implements ApplicationRunner, ApplicationContextAware, Ordered {

  private static final Logger log = LoggerFactory.getLogger(EntityPluginRunner.class);
  @Autowired
  private ApplicationContext context;
  @Autowired
  private EntityPluginCacheBean entityPluginCacheBean;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    Map<String, CrudRepository> crudRepositoryMap
        = context.getBeansOfType(CrudRepository.class);


    crudRepositoryMap.forEach((k, v) -> {
      Class<?>[] repositoryInterfaces = AopProxyUtils.proxiedUserInterfaces(v);
      for (Class<?> repositoryInterface : repositoryInterfaces) {
        List<Class<?>> interfaceGenericLasses = InterfaceReflectUtils
            .getInterfaceGenericLasses(repositoryInterface,
                CrudRepository.class);
        if (!CollectionUtils.isEmpty(interfaceGenericLasses)) {
          // entity class
          Class<?> entityClass = interfaceGenericLasses.get(0);
          EntityPlugin annotation = entityClass.getAnnotation(EntityPlugin.class);
          if (annotation != null) {

            Map<String, EntityPluginCache> cacheMap = entityPluginCacheBean.getCacheMap();
            EntityPluginCache value = new EntityPluginCache();
            value.setName(annotation.name());
            value.setSelf(entityClass);
            value.setIdClass(interfaceGenericLasses.get(1));
            value.setConvertClass(annotation.convertClass());
            value.setCrudRepository(v);
            if (cacheMap.containsKey(annotation.name())) {
              try {
                if (log.isErrorEnabled()) {
                  log.error("不允許出現(xiàn)相同的EntityPlugin名稱 ,entity = [{}]", entityClass);
                }
                throw new Exception("不允許出現(xiàn)相同的EntityPlugin名稱");
              } catch (Exception e) {
                e.printStackTrace();
              }
            }
            cacheMap.put(annotation.name(), value);
          }
        }
      }

    });
  }

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.context = applicationContext;
  }

  @Override
  public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE;
  }
}

注意本例中只支持CrudRepository接口暫時(shí)不支持它的子類接口,子類接口實(shí)現(xiàn)會(huì)作為后續(xù)開發(fā)方向。

至此數(shù)據(jù)準(zhǔn)備都已經(jīng)完成,接下來就是將Controller開發(fā)完成,首先定義一個(gè)對(duì)應(yīng)Controller的Service

public interface EntityPluginCoreService {

  Object findById(String entityPluginName, String id);

  Object save(String entityPluginName, Object insertParam);

  Object update(String entityPluginName, Object updateParam);

  Boolean deleteById(String entityPluginName, String id);

}

該Service對(duì)應(yīng)了四種操作模式,下面以保存作為一個(gè)實(shí)例進(jìn)行說明。保存的Controller相關(guān)代碼如下

@PostMapping("/{entityPluginName}")
public ResponseEntity<Object> save(
    @PathVariable("entityPluginName") String entityPluginName,
    @RequestBody Object insertParam
) {
  EntityPluginCache entityPluginCache = entityPluginCacheBean.getCacheMap().get(entityPluginName);
  Class<? extends EntityConvert> convertClass = entityPluginCache.getConvertClass();
  if (convertClass != EntityConvert.class) {
    Object save = coreService.save(entityPluginName, insertParam);
    return ResponseEntity.ok(save);
  } else {
    Object o = gson.fromJson(gson.toJson(insertParam), entityPluginCache.getSelf());
    Object save = coreService.save(entityPluginName, o);
    return ResponseEntity.ok(save);

  }
}

在Controller這段代碼中可以看到有兩個(gè)分支,這里兩個(gè)分支的判斷是注解EntityPlugin中的convertClass屬性是否為EntityConvert.class,如果是說明沒有轉(zhuǎn)換過程,即數(shù)據(jù)庫對(duì)象就是參數(shù)對(duì)象,因此可以直接做出下面的轉(zhuǎn)換,請(qǐng)求參數(shù)轉(zhuǎn)換成JSON字符串,再通過JSON字符串轉(zhuǎn)換成實(shí)體類本身,如果不是則進(jìn)入核心實(shí)現(xiàn)類。核心實(shí)現(xiàn)類的相關(guān)代碼如下

@Override
public Object save(String entityPluginName, Object insertParam) {
  EntityPluginCache entityPluginCache = entityPluginCacheBean.getCacheMap().get(entityPluginName);
  CrudRepository crudRepository = entityPluginCache.getCrudRepository();

  Class<? extends EntityConvert> convertClass = entityPluginCache.getConvertClass();
  if (convertClass == EntityConvert.class) {
    return crudRepository.save(insertParam);
  }
  // 存在轉(zhuǎn)換類的情況下
  if (convertClass != null) {

    String[] beanNamesForType = context.getBeanNamesForType(convertClass);
    // 在 Spring 中能夠搜索到
    if (beanNamesForType.length > 0) {
      String beanName = beanNamesForType[0];
      EntityConvert bean = context.getBean(beanName, convertClass);
      // 轉(zhuǎn)換成數(shù)據(jù)庫實(shí)體對(duì)象
      Object insertDbData = bean.fromInsType(insertParam);
      // 執(zhí)行插入
      return crudRepository.save(insertDbData);
    }
    // 不能再 Spring 容器中搜索
    else {
      EntityConvert entityConvert;
      try {
        entityConvert = newInstanceFromEntityConvertClass(
            convertClass);
      } catch (Exception e) {
        if (log.isErrorEnabled()) {
          log.error("無參構(gòu)造初始化失敗,{}" + e);
        }
        return null;
      }
      Object insertDbData = entityConvert.fromInsType(insertParam);
      return crudRepository.save(insertDbData);
    }
  }
  // 如果不存在轉(zhuǎn)換器類直接進(jìn)行插入
  else {
    return crudRepository.save(insertParam);
  }
}

在這段代碼中處理流程如下:

  1. 情況一:注解EntityPlugin中的convertClass屬性是EntityConvert.class,直接進(jìn)JPA相關(guān)操作,注意此時(shí)的參數(shù)已經(jīng)被Controller轉(zhuǎn)換成實(shí)際的數(shù)據(jù)庫對(duì)象。

  2. 情況二:注解EntityPlugin中的convertClass屬性不是EntityConvert.class,此時(shí)可能存在兩種分類,第一種convertClass 交給Spring管理,第二種convertClass 不是Spring管理,對(duì)應(yīng)這兩種情況分別做出如下兩種操作:

    1. 從Spring中根據(jù)類型找到所有的bean取第一個(gè)作為EntityConvert接口的實(shí)現(xiàn)類,通過得到的bean進(jìn)行數(shù)據(jù)轉(zhuǎn)換在調(diào)用JPA相關(guān)操作。

    2. 直接通過反射創(chuàng)建EntityConvert實(shí)現(xiàn)類,注意必須要有一個(gè)無參構(gòu)造,本例使用無參構(gòu)造進(jìn)行創(chuàng)建,創(chuàng)建EntityConvert實(shí)例對(duì)象后調(diào)用JPA相關(guān)操作。

其他代碼編寫同理,其他實(shí)現(xiàn)可以查看這個(gè)倉庫:https://gitee.com/pychfarm_admin/entity-plugin

測(cè)試

完成了各類編寫后進(jìn)入測(cè)試階段。

新增API測(cè)試

  • 新增API測(cè)試

POST http://localhost:8080/oauthClient
Content-Type: application/json
// 參數(shù)
{
    "clientId":"asa",
    "clientSecurity":"123"
}
  • 返回結(jié)果

{
    "id": 10,
    "clientId": "asa",
    "clientSecurity": "123",
    "redirectUri": null,
    "version": null
}
  • 數(shù)據(jù)庫結(jié)果

SpringDataJpa怎么實(shí)體對(duì)象增強(qiáng)設(shè)計(jì)

修改API測(cè)試

  • 修改API測(cè)試

PUT http://localhost:8080/oauthClient
Content-Type: application/json
// 參數(shù)
{
    "id": 10,
    "clientId": "asa",
    "clientSecurity": "123123123",
    "redirectUri": null,
    "version": null
}
  • 返回結(jié)果

{
    "id": 10,
    "clientId": "asa",
    "clientSecurity": "123123123",
    "redirectUri": null,
    "version": null
}
  • 數(shù)據(jù)庫結(jié)果

SpringDataJpa怎么實(shí)體對(duì)象增強(qiáng)設(shè)計(jì)

查詢API測(cè)試

  • 修改API測(cè)試

GET http://localhost:8080/oauthClient/10
  • 返回結(jié)果

{
    "id": 10,
    "clientId": "asa",
    "clientSecurity": "123123123",
    "redirectUri": null,
    "version": null
}

刪除API測(cè)試

  • 修改API測(cè)試

DELETE http://localhost:8080/oauthClient/10
  • 返回結(jié)果

true
  • 數(shù)據(jù)庫結(jié)果

SpringDataJpa怎么實(shí)體對(duì)象增強(qiáng)設(shè)計(jì)

通過上述的測(cè)試用例對(duì)于數(shù)據(jù)庫對(duì)線作為參數(shù)的開發(fā)已經(jīng)符合測(cè)試用例后續(xù)還有一些其他規(guī)劃將在后續(xù)進(jìn)行開發(fā),具體計(jì)劃如下

后續(xù)計(jì)劃

  1. EntityConvert 接口使用完善,目前只支持?jǐn)?shù)據(jù)庫對(duì)象直接使用,后續(xù)對(duì)EntityConvert 接口進(jìn)行更好的應(yīng)用。

  2. 驗(yàn)證器相關(guān)接入,沒以前還未做數(shù)據(jù)驗(yàn)證相關(guān)操作,后續(xù)會(huì)接入驗(yàn)證API。

  3. 緩存相關(guān),目前對(duì)于數(shù)據(jù)還未使用緩存,后續(xù)接入reds-hash組件

    1. 緩存接入后對(duì)序列化進(jìn)行自定義。

到此,相信大家對(duì)“SpringDataJpa怎么實(shí)體對(duì)象增強(qiáng)設(shè)計(jì)”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI