要實現無代碼、純配置的業務界面展示和常規數據操作,最佳的方式是通過實體-屬性-值的設計方式,也就是常説的EAV模式,通過動態構建實體類型、動態構建對應的屬性列表,以及根據類型的不同對屬性值進行存儲,從而構建一系列的處理規則,實現業務模塊的動態化,本篇隨筆探討一下,如何在Python開發中實現無代碼、純配置的業務界面展示,以及實現常規數據操作的過程,拋磚引玉,共同探討。
1、何為實體-屬性-值的設計方式
EAV(Entity-Attribute-Value)模型,我們先來了解一下。
EAV 把所有業務抽象成:
數據結構示例,如下所示:
Entity: Customer Attributes: name string phone string level enum Values: (001, name, "張三") (001, phone, "138****") (001, level, "VIP")
分別 包括定義實體表,對應的屬性類別(名稱、類型等),以及每個屬性的值記錄。
一句話説明就是:每個實體都有唯一的標識符,每個實體都可以有多個屬性與之關聯,每個屬性都有唯一的標識符,每個屬性都可以具有多個值。
我們對屬性值表基於數據類型進行分割,每個不同的數據類型拆為一個單獨的表,同時通過 屬性表(Attribute) 添加 類型決定去哪裏存取數據。
我們可以借鑑magento的eav模型,它是EAV設計的最優參考了。Magento 2中的EAV屬性類型有下面這些表:
- eav_entity_int
- eav_entity_varchar
- eav_entity_text
- eav_entity_decimal
- eav_entity_datetime
屬性元數據驅動 UI 控件選擇,我們也根據屬性的類型進行定義,如可以定義不同的輸入控件。
一句話,EAV + 元數據 + 通用UI + CRUD引擎 = 無代碼業務系統
本質就是:
-
業務結構:EAV建模
-
界面構建:屬性→控件自動映射
-
數據交互:通用CRUD
-
規則校驗:DSL配置
-
查詢過濾:條件模板驅動
2、在Python開發中實現無代碼、純配置的業務界面展示和常規數據操作
有了上面的EAV知識的介紹,我們可以來進一步探討在Python開發中實現無代碼、純配置的業務界面展示和常規數據操作過程了。
我們先來看一個界面下效果。
對於這樣一個常規的列表展示界面,包括有條件查詢、分頁列表記錄展示,屬性類型,包括文本、整數、浮點小數、日期、備註長文本等類型錄入,以及對應不同的數據類型,有下拉列表(固定的、動態字典的)、複選框、評分、彈出選擇、映射關聯屬性等多種方式的錄入處理,也是比較常見的情況。
而對於條件,可以展開多個條件,展開效果如下所示。
而對於數據的錄入,有彈出界面處理方式,也有對應直接編輯列表的方式,直接編輯輸入比較快捷,如果能夠豐富錄入的控件處理,那麼也是非常好的一種數據編輯方式。
因此我們直接在列表中進行數據的編輯處理,提供不同類型、不同方式的輸入處理,如下面是動態字典,通過下拉列表或者單選框的方式進行錄入。
而對於一些系統用户、角色、機構,我們應該也可以彈出來選擇記錄,並更新關聯的字段信息
列表界面一般為了方便,會提供相關的右鍵菜單,提供常規的操作處理。
而有些業務表是主從表的方式進行展示,如對應報價單、訂單等常規的數據,通過主從表的方式會更加合適。
主表可以直接錄入外,明細表也通過直接錄入的方式,通過選擇產品,可以快速的實現數據記錄的選擇,以及對相關字段屬性值的複製,非常方便。
3、配置業務界面處理
如果要實現上面業務界面的展示處理,那麼我們需要如何配置業務界面元素呢。
通過上面的EAV介紹
那麼我們至少圍繞上面幾個信息來定義和存儲數據內容,在更高的層次上定義好相關的信息:
1)實體類是否分頁、是否有主從表關係。
2)屬性定義,需要包括是否可以查詢(作為條件)、是否可用(顯示與否)、屬性類型(決定存儲位置)、控件輸入方式(日期、數值、文本),而其中文本最為靈活,可能是通過配置字典(動態或者固定列表),選擇系統表方式獲取,選擇動態業務表對象,通過編碼規則生成編碼等方式,數值可能是常規數值輸入、評分輸入、或者複選框等方式。另外還有是否必填、是否只讀,排序順序等關鍵定義
3)屬性值的存儲,根據不同的數據類型,存儲在不同的表中,提高處理效率的同時不會降低精度。
4)屬性信息的提取,這個非常關鍵,如果把這些數據每次組合起來,那麼常規的做法就是關聯多個表來實現數據的聯合,但是效率會非常低下。好的做法可以利用NoSQL的動態文檔的特點,對數據的組合通過MogoDB的方式實現快速檢索處理,存儲的時候,一份完整的記錄存儲在MongoDB,另外一份數據寫入具體的屬性值表中,必要時可以隨時實現同步即可。
有了上面的幾點介紹,我們來看看具體在Python中如何管理這些內容。
如上面頂部為實體(或實體類型)的定義信息,主要包括名稱、模型類名、是否分頁幾個屬性。
下面是對應實體類型的屬性列表,其中屬性名稱、模型類型名、存儲類型,為核心信息,其他必填、排序、字典類型、只讀、隱藏、可查詢 等等屬性定義為一些構建界面必須的相關屬性。
這兩個表可以通過直接編輯模式進行快速錄入,從而方便動態定義實體類型和相關的屬性列表。
另外通過定義從表,可以從系統的動態定義實體類型中選擇業務表作為從表信息,如下對於訂單或者報價單的業務,通過主從表的方式顯示的,定義界面如下所示。
而對於一些屬性字段的輸入類型,我們提供一些內置的選項供選擇。
如前面介紹的選擇用户方式,就從基礎用户表中選擇記錄,更新關聯的字段信息。
而如果選擇類似系統業務編碼的,那麼也提供一個編碼生成的方式(結合業務編碼模塊規則生成編碼),如訂單中的訂單編碼記錄,新增的時候,提供一個按鈕可以結合訂單編碼規則生成編碼。
而對於常規的字典,我們可以通過配置字典類型,就可以實現字段和系統字典項目的關聯了。
這樣就可以在實際記錄的界面中選擇字典項目了,如下所示對於支付方式的字典項目,可以從中選擇。
而對於選擇用户表、角色表、機構表,以及選擇動態EAV生成的表,那麼也需要進行一些屬性值的關聯複製處理,那麼需要通過映射源字段和目標字段的名稱,實現關聯。
這樣就可以再選擇產品信息的記錄的時候,把它的屬性值帶到目標記錄上。
如在訂單明細記錄中,通過產品選擇的方式,可以帶過來對應的屬性值。
3、數據的查詢和MongoDB
使用EAV(Entity-Attribute-Value)模式來存儲完整的數據結構信息以及NoSQL數據庫來存儲完整的記錄是一種靈活的方法,特別適用於需要存儲動態結構數據的場景。
EAV的常規關係型數據庫表存儲常規的設計表,如實體類型、屬性定義、屬性值(多個)表的相關信息,而利用MongoDB數據庫的大數據處理靈活性和高性能的響應,能夠存儲我們實際變化的文檔信息。在檢索的時候,並提供了常規關係型數據庫的聯合查詢、JSON查詢無法得到的靈活性和高性能。
有了字段的定義,我們就可以在業務列表中顯示相關的字段,並從MongoDB總檢索指定類型的數據,由於MongoDB本身支持非常好的查詢處理,因此對於查詢來説非常簡單。
表的數據在MongoDB中存儲的,如下界面所示。
對於在Python界面中展示相關的記錄,我們根據配置,構建一個窗體界面,適配條件動態展示、列表展示、是否分頁,以及對於各種屬性定義好對應的前端輸入類型,就能很好的實現數據的展示和直接錄入的處理了。
由於我們前端後端通過WebAPI進行交互,後端在FastAPI服務中提供對應MongoDB的數據常規CRUD的接口來處理數據的增刪改查操作,如下所示。
對於條件查詢的處理,這個和我們常規方式設計業務表,生成的查詢接口類似,如下所示。
@router.post( "/mongo-list-post", response_model=AjaxResponse[PagedResult[dict] | None], summary="分頁獲取實體類型的記錄", dependencies=[DependsJwtAuth], ) async def mongo_get_list_post( request: Request, input: Annotated[EAVPagedDto, Body(description="查詢條件")], db: AsyncSession = Depends(get_db), ): logger.info(f"EAVPagedDto:{input.model_dump()}") item = await attribute_crud.mongo_get_list(db, input) return AjaxResponse(item)
而在Python的前端,我們這裏以PySide6的界面實現為例,通過實體類型的定義和對應屬性的列表信息,我們可以進行界面的動態構建,如下是PySide6的界面實現代碼。
async def _create_content_panel(self) -> QWidget: """創建右側主要內容面板""" # 創建一個主面板 panel = QWidget(self) # 創建一個垂直佈局 main_layout = QVBoxLayout() # 創建一個摺疊的查詢條件框 search_bar = self._create_search_bar(panel) main_layout.addWidget(search_bar) # 創建顯示數據的表格 table_widget = self._create_grid(panel) main_layout.addWidget(table_widget, 1) # 拉伸佔用高度 # 如果是分頁,創建一個分頁控件 if self.ispaging: self.pager_bar = ctrl.MyPager(panel, self.items_per_page, self.update_grid) main_layout.addWidget(self.pager_bar) # 創建從表數據表格 sub_table_widget = await self._create_sub_content(panel) if sub_table_widget is not None: main_layout.addWidget(sub_table_widget, 1) # 拉伸佔用高度 # 設置佈局 panel.setLayout(main_layout) return panel
在查詢條件的展示處理中,我們根據屬性列表來統一構建顯示的控件信息,如下代碼所示。
def CreateConditionsWithSizer(self, parent: QWidget = None) -> QGridLayout: """子類可重寫該方法,創建摺疊面板中的查詢條件,包括佈局 QGridLayout""" layout = QGridLayout() layout.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) layout.setSpacing(2) # 增加間距 # 統一處理查詢條件控件的添加,使用默認的佈局方式 cols = self.CONDITIONS_PER_ROW * 2 # 每行顯示的條件數,*2表示包含標籤和輸入控件 self.condition_widgets = list = EAVUtil.CreateConditions(self.attribute_list, parent) for i in range(len(list)): control = list[i] # print(type(control)) if not isinstance(control, QWidget): print("錯誤!control 不是 QWidget:", control) break control.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)) layout.addWidget(control, i // cols, i % cols) return layout
而對於數據的直接編輯處理,我們通過控件屬性的定義,統一轉換為對應自定義視圖委託的配置信息,從而構建不同的輸入控件方式。
config = EAVUtil.convert_attributes_to_config(self.attribute_list, self.entity_type_info) # 設置QTableView視圖的委託處理對象 self.delegate = CustomDelegate(config, self.table_model, self.table_view) #對特殊字段進行自定義的處理,如彈出選擇對話框等 self.delegate.customTriggered.connect(self.on_custom_triggered)
其中的config例子可能是其中下面的定義。
#實際字段測試 config = { "ProductName": {"type": "text"}, "Color": {"type": "combo", "dict_type_name":"產品顏色"}, #字典類型名稱 "Size": {"type": "int"}, "Style": {"type": "combo", "dict_type_name":"產品款式"}, "Height": {"type": "double", "min": 0, "max": 1000, "decimals": 2, "format": "{0} cm"}, "Width": {"type": "double", "min": 0, "max": 1000, "decimals": 2, "format": "{0} cm"}, "Note": {"type": "text"}, "Test": {"type": "text"}, "Tag": {"type": "text"}, "CreateDate": {"type": "datetime", "format": "yyyy-MM-dd HH:mm:ss"}, "Price": {"type": "double", "decimals": 2,"format": "{0:C2}"}, "Status": {"type": "check", "true": "1", "false": "0"}, "Dealer": {"type": "radio", "dict_type_name":"送貨區域", "width": 280}, "Rating": {"type": "rating"}, "Tag": {"type": "text"}, "User": {"type": "text", "readonly":True}, "Organ": {"type": "text", "readonly":True}, "Role": {"type": "text", "readonly":True}, "Attach": {"type": "text", "readonly":True}, }
這需要我們根據實際的配置信息進行構建config即可。
我們為了支持直接錄入多種控件類型,那麼需要自定義視圖委託。
class CustomDelegate(QStyledItemDelegate):
它支持的類型有:
這個我曾經在文章《在PySide6/PyQt6的開發框架中,增加對錶格多種格式錄入的處理,以及主從表的數據顯示和保存操作》有所介紹。
表格中多種格式錄入的效果示例如下。
以上就是對於如何在Python開發中實現無代碼、純配置的業務界面展示和常規數據操作的處理分析過程,其中設計到EAV相關基礎表的設計,MongoDB數據的查詢處理、FastAPI接口數據的封裝、前端界面的設計和對於多種輸入控件的支持等方面內容,通過整合這些,可以快速的、彈性的實現多種業務記錄信息的存儲和展示。
以上思路具體實現的過程,拋磚引玉,希望大家有所感悟,並分享一下自己的寶貴經驗和思路。