如何在Python开发中实现无代码、纯配置的业务界面展示和常规数据操作的处理分析过程
要实现无代码、纯配置的业务界面展示和常规数据操作,最佳的方式是通过实体-属性-值的设计方式,也就是常说的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接口数据的封装、前端界面的设计和对于多种输入控件的支持等方面内容,通过整合这些,可以快速的、弹性的实现多种业务记录信息的存储和展示。
以上思路具体实现的过程,抛砖引玉,希望大家有所感悟,并分享一下自己的宝贵经验和思路。
专注于代码生成工具、.Net/Python 框架架构及软件开发,以及各种Vue.js的前端技术应用。著有Winform开发框架/混合式开发框架、微信开发框架、Bootstrap开发框架、ABP开发框架、SqlSugar开发框架、Python开发框架等框架产品。
转载请注明出处:撰写人:伍华聪 http://www.iqidi.com
浙公网安备 33010602011771号