# 01.後端規範 ###### tags: `開發規範`. ## 1.目錄結構 * ykkap-pro專案底下會有固定server與clent模組加上ykk系統功能模組。 * server模組:後端程式系統主要模組,負責整個系統的設定如控管。 * client模組:前端程式系統主要模組,負責前端專案所有事項。 * adm模組:ykk adm功能模組負責adm模組功能、共用功能。 * 其餘模組: 負責各模組的repository、service、controller與vm開發。 ``` ├── adm │   └── src │   └── main/java/com/ykkap/pro │ └── common │      │ └── domain │      │ └── annotation │      │ └── aop │      │ └── utils │      │ └── vm │     └── adm │      └── repository │      └── service │      └── controller │      └── vm ├── bom │   └── src │   └── main/java/com/ykkap/pro │     └── bom │      └── repository │      └── service │      └── controller │      └── vm ├── ......各個功能模組 ├── client │   ├── public │   └── src │       ├── App.vue │       ├── assets │       ├── common │       │   └── js │       │   └── mixins │       │   ├── commonMixin.js │       │   └── index.js │       ├── components │       ├── config │       ├── router │       ├── store │       ├── styles │       └── views │           └── adm │           │ └── lookups-manage │           │ ├── LookupsManage.vue │          │ └── LookupsManageDetailF.vue │           └── ....各個功能模組 │           └── Home.vue └── server     └── src     └── main/java/com/ykkap/pro         ├── server         │ ├── ServerApplication.java         │ ├── ServletInitializer.java         │ ├── config         │ │   └── security         │ │   ├── KeycloakSecurityConfig.java         │ │   └── MyKeycloakAuthenticationProcessingFilter.java         │ ├── constants         │ │   └── WebErrorCodeEnum.java         │ ├── handler         │ │   └── GlobalExceptionHandler.java         │ └── vm         │ └── WebRespVM.java         └── resources         ├── application-dev.yml         └── application.yml ``` ## 2.命名規則 * **Domain(Entity):** 以Table命名,每個單字第一個字母大寫區分,若有下底線則移除。 如:以Table Name:AUTH_ORG_USERS ,則Domain Class Name:AuthOrgUsers.java * **Repository(DAO):** Domain名 + Repository * **Service(服务层):** 功能名稱 + Service * **Controller(控制器层):** 功能名稱 + Controller * **URL Peth:** *URL請求中不採用大小寫混合的駝峰命名方式,採用蛇形命名法一律小寫單字,如果需要連接多個單字,則採用連接符-連接單字。(備註:不採用底線_連接符號,是因為與Html的超連結樣式重疊容易混淆) *路徑組成規範:api/模組名稱/功能名稱/method名稱。 * **Util(工具层):** 功能名稱 + Utils ## 3.Repository 開發規範 ### 3.1.JPQL使用規範 * 使用時機:單一Table查詢,查詢結果與Where條件無使用到其他Domain物件。 ```java @Query(value = "select name,author,price from Book b where b.price>:price1") List<Book> findByPrice(@Param("price1") long price1); ``` ### 3.2.Native SQL Query使用規範 * 使用時機:除了JPQL使用規範外,其餘查詢方式一律使用Native SQL,SQL語法一律小寫。 ```java @Query(nativeQuery = true , value = "select EAM.SUBJECT,\n" + " EAM.AUTHOR,\n" + " AU.DISPLAY_NAME,\n" + " EAM.DEPT_CODE,\n" + " EAM.DEPT_NAME,\n" + " TO_CHAR(EAM.PUBLIC_DATE, 'YYYY-MM-DD'),\n" + " TO_CHAR(EAM.EXPIRE_DATE, 'YYYY-MM-DD'),\n" + " TO_CHAR(EAM.CREATED_DATE, 'YYYY-MM-DD'),\n" + " EAM.B_TYPE,\n" + " EAM.S_TYPE\n" + " from EIP_ANN_MAIN EAM,\n" + " AUTH_USERS AU \n" + " where EAM.AUTHOR=AU.USER_ACCOUNT " " and EAM.ANN_ID=:annId") List<Object[]> getEipAnnInfo(@Param("annId") int annId); ``` ### 3.3.其餘規則 * 參數:repository中一律使用@Param引用参数 * 分頁:分頁查詢的repository function 名稱以P結尾,以利辨識該功能為分頁查詢。 * 不要使用SPEL語法 ## 4.一般規範 ### 4.1.物件間的轉換 #### 4.1.1.VM to Domin(Object to Object)轉換: 透過org.springframework.beans.BeanUtils中的copyProperties功能 ```java= log.info("copy before A1:{} B:{}",a,b ); BeanUtils.copyProperties(a,b); log.info("copy after A1:{} B:{}",a,b ); ``` 執行結果: copy before A1:A(name=eric, addr=TW) B:B(name=ken, phone=01111) copy after A1:A(name=eric, addr=TW) B:B(name=eric, phone=01111) #### 4.1.2. List<Object[]> to List<Map<String,Object>> ```java= private final String[] delegateFields = new String[]{"assigneeName", "attorneyName", "creatorName" }; List<Lookups> lookList = lookupsRepository.findByLookupType("PAYMENT"); Page<Map<String, Object>> list = ObjectExtensions.mapping(closeRepository.findByClosePageList(counterList, paymentToolCode,pageable),CLOSE_LIST_COLUMNS); ``` 第一行:須根據Repository查詢結果順序定義欄位名稱,備註查詢結果的順序一定要一致。 第三行:透過ObjectExtensionUtils工具包的mapping功能進行轉換 #### 4.1.3. List<Map<String,Object>> to Object(VM,Domain) 夠過 com.fasterxml.jackson.databind.ObjectMapper中的convertValue功能 ```java= Map<String, Object> fooMap = om.convertValue(foo, Map.class); Foo fooMapFoo = om.convertValue(fooMap, Foo.class); log.info("fooMapFoo: {}", fooMapFoo); ``` ```java= Map<String, String > reportParameter = new HashMap<>(); reportParameter.put("commissionNo", "G210001"); reportParameter.put("projectNo", "A210003"); reportParameter.put("...", "..."); . . . ReportUtils.Download("保固票退還申請書",reportParameter); ``` ### 4.2.One to Many相關操作 #### 4.2.1. Master 類別 Domain設定 ```java= @Data @NoArgsConstructor @Entity @Table(name = "CITY") @JsonInclude(JsonInclude.Include.NON_NULL) public class City { @Id @Column(name = "CITY") private String city; @Column(name = "CITY_CH") private String cityCh; @Column(name = "CITY_ENG") private String cityEng; @OneToMany(mappedBy = "cityObject", fetch = FetchType.LAZY) @JsonManagedReference private List<CityZip> cityZipList; } ``` * 第17行: mappedBy:指出是要對應到那一個類別。 FetchType.LAZY:懒加载,@OneToMany預設的Fetch模式是FetchType.LAZY,除非真正要使用到該屬性的值,否則不會真正將資料從表格中載入物件。 **Detail 類別 Domain設定** ```java= @Data @NoArgsConstructor @Entity @Table(name = "CITY_ZIP") @EntityListeners(AuditingEntityListener.class) @JsonInclude(JsonInclude.Include.NON_NULL) public class CityZip { @Column(name = "CITY") private String city; @Id @Column(name = "ZIP") private String zip; @Column(name = "ZIP_CH") private String zipCh; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CITY" ,insertable = false, updatable = false)//, @JsonBackReference private City cityObject; } ``` * 第10行:需定義city屬性,這就能直接對CityZipy做CRUD操作。 * 第20行: @JsonManagedReference and @JsonBackReference. 成對使用目的解決無限迴圈。 @JsonManagedReference標記在父類對子類的引用變數上。 @JsonBackReference標記在子類對父類的引用變數上。 #### 4.2.2. 查詢 可透過查詢Master直接取得Detail資料,只有需要用到Detail資料的時候,才會去資料出抓取資料載入所以FetchType必須設定為LAZY(fetch = FetchType.LAZY) #### 4.2.3. 刪除 可透過刪除Master物件時直接連相關的Detail資料耶刪除,再見建立資料庫FK 需加上ON DELETE CASCADE較件, 如下: ```sql CREATE TABLE UI_DEMO.CITY_ZIP ( CITY VARCHAR2(3 BYTE) NOT NULL, ZIP VARCHAR2(5 BYTE) NOT NULL, ZIP_CH VARCHAR2(30 BYTE) NULL, CONSTRAINT PK_CITY_ZIP PRIMARY KEY(ZIP) NOT DEFERRABLE VALIDATE ) ; ALTER TABLE UI_DEMO.CITY_ZIP ADD ( CONSTRAINT FK_CITY_ZIP FOREIGN KEY(CITY) REFERENCES UI_DEMO.CITY(CITY) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE VALIDATE ) ; ``` #### 4.2.4. 新增/修改 Master物件只修改Master物件資料,在Master物件資料中修改Deatal資料不會有作用, 若要修改或新增Deatal資料直接在Deatal物件進行操作。 ### 4.3.Call Procedures資料庫 所有模組所有功能需要呼叫資料庫Procedures,統一使用EntityManager.createStoredProcedureQuery呼叫 寫法如下 如下: ```java @AllArgsConstructor @Service @Slf4j public class ActRuDelegateService { @PersistenceContext private EntityManager entityManager; public void call() { StoredProcedureQuery store = this.entityManager.createStoredProcedureQuery("UPDATE_CHANGED_COST_TO_Q3"); store.registerStoredProcedureParameter("p_commission_no", String.class, ParameterMode.IN); store.registerStoredProcedureParameter("p_commission_ver", Integer.class, ParameterMode.IN); store.registerStoredProcedureParameter("p_status", String.class, ParameterMode.OUT); store.registerStoredProcedureParameter("p_status2", String.class, ParameterMode.OUT); store.setParameter("p_commission_no","rrrr"); store.setParameter("p_commission_ver",1); store.execute(); log.info("store getOutputParameterValue p_status:{} p_status2:{}",store.getOutputParameterValue("p_status"),store.getOutputParameterValue("p_status2")); } } ``` ### 4.4.Select API 相關規格 ![](https://i.imgur.com/LqhplpM.png) api接口名稱規則:加上原接口名稱-select api回傳格式:統一使用SelectionVM 如下: ```java @Data @NoArgsConstructor //@AllArgsConstructor public class SelectionVM { private String label; private String value; private boolean inactive;//非必要,可不設定值 public SelectionVM(String label,String value){ this.label= label; this.value= value; } public SelectionVM(String label,String value,boolean inactive){ this.label= label; this.value= value; this.inactive=inactive; } } ``` ### 4.5.LOV Service規格 ![](https://i.imgur.com/W5Va7cl.png) * 所有此類型查詢接口一律需要分頁功能,所以必須為可分頁查詢。(避免畫面一次將所有資料一次撈出,產生效能問題) * 所有功能有必須有filterVal參數:其目的為了提供所有結果欄位的模糊Like查詢(可排除日期欄位) * api接口名稱規則:/page/find-color-lov 範例 ```java @GetMapping("/page/find-color-lov") public Page<SelectionVM> findColorLOVPage(String filterVal,String type, String cordno, String itemno, String partno, Pageable pageable) { log.info("getColorListP filterVal:{}",filterVal); if (StringUtils.isBlank(filterVal)){ filterVal=""; }else{ filterVal=filterVal.toLowerCase(); } return comboLOVService.getColorListPage(filterVal,type,cordno,itemno,partno,pageable); } ``` * service function名稱規則:function+LOV+Page 範例 ```java public Page<SelectionVM> findColorLOVPage(String filterVal,String type, String cordno, String itemno, String partno,Pageable pageable) { if(!StringUtils.isBlank(cordno) && (StringUtils.isBlank(type) || "B_ADD".equals(type))){ return partRepository.getColorByCordP(cordno,filterVal,pageable).map(color -> new SelectionVM( color, color)); }else { return partRepository.getColorByMPartItemP(partno,filterVal,pageable).map(color -> new SelectionVM( color, color)); } } ``` * repository function 名稱規則:function+LOV+Page 範例 ```java @Query(nativeQuery = true , value = "select distinct COLOR\n" + " from cord_color@Q3 \n" + " where cordno=:cordno and LOWER(COLOR) like %?2%" ,countQuery ="select count(a.*) from (select distinct COLOR\n" + " from cord_color@Q3 \n" + " where cordno=:cordno and LOWER(COLOR) like %:filterVal%) a") public Page<String> findColorLOVPage(@Param("cordno") String cordno,@Param("filterVal") String filterVal, Pageable pageable); ``` ### 4.6 service @Transactional用法