如何用动态字段实现系统的扩展性?
设计系统架构时,我们总是希望一个架构或者说是模型能支持多种业务场景,当然场景是系统范围内的。比如说做商品系统,商品有多种属性,不同类型的商品的属性不一样,怎么用关系型数据库去保存这些不同的属性呢?再比如做CMS时,除了基本的背景、位置、类型、状态等属性一致外,页面不同地方需要的数据结构却是不一样的。
再比如做广告时,不同地方的广告需要用不同属性去区别,而它们又有一些共有的属性,比如类型,名称。
经过思考和查询,知道有如下几种方法来存储不同数据。 动态添加属性字段、 JSON 格式保存、 预留字段、 属性字段行存储。以用java为编程,mysql数据库为例,分别说下这几种方式如何实现动态字段。
1. 动态添加属性字段。
意思是,在需要添加一个属性时。先在数据库的表里添加一个字段,比如给新上的商品有"产地“这个属性,就给product表添加一个"产地"字段。那么要用这个字段时,还要改相应的java程序。显然这种方式不利于灵活扩展,而且在一张大表添加一个字段需要较长时间,这也不适合7*24的服务。
2.JSON 格式保存。
就是说把需扩展的一组字段都到到一个字段里,各个字段用json的方式组成一个大的字符串。比如:{"clsTypeName":"java.lang.Integer","srcDes":"支持数字或数字区间,如10,2-5","storeCol":"field2","storeName":"ord","viewName":"排名"}。这种方式,添加、修改、删除问题都不大,但查询不方便,比如要查询viewName='排名'的,就要遍历所有数据才行。如果有复杂查询,比如group
by
viewName,这将是个噩梦。所以这种方式只能用于数据量很少的方式,比如就10行数据,且没有复杂查询。曾经我设计了这么一张表,业务上需json中的一个字段为唯一键。那么要实现这个功能,就要添加时遍历所有数据。更不用说后面的查询了,于是我立即弃用了这个设计。
3.预留字段。
就是先给表定义几个扩展字段,还是以商品表为例。给product添加field1,field2,field3三个varchar(200)类型的字段,这样任何数据类型都可以解析为字符串,把编码后的数据存进去就行了。但这还需要对应关系。比如家电类(type=1)的,field1表示颜色,field2表示型号,field3表示功率。而图书类(type=2)的,field1表示出版商,field2开本,field3表示版次。这种设计可以解决一些查询问题,比如我要查询”机械工业出版社"的,那么条件就是type=2
and field1='机械工业出版社'。上面我那个唯一键的需求也可用类似的方法来实现。
它的缺点是字段是公用的,不能顾名思义,得先查对应关系。另外扩展字段的数量无法精确定义,也存在数据量大时添加字段卡顿的风险。因为我刚做了这样的一个应用,再详细说下代码实现。
a.新增。
前台页面输入时并不知道需要录入那几个字段,上面的家电需要field1、field2、field3三个输入,这需要查定义。
不同的类型需要添加的字段名也不一样,比如上面的家电类的,需先有个对应关系,如下:
{"clsTypeName":"java.lang.Integer","srcDes":"RGB值","storeCol":"field1","storeName":"color","viewName":"颜色"}
storeCol代表真实的字段名,而storeName代表逻辑上的字段名。在添加时,如果页面上的input
标签name="真实字段名(field1)",那么在进行唯一值判断时就需要转换。比如要颜色是唯一键,但页面上只有field1,我们并不知道field1代表什么。但有了上面的对应关系,我们就可以写代码了,如果input的name对应的storeName字段值为color,表示要对这个字段(field1)进行查找来实现唯一键的功能。mybatis存储时,最好也只存储上面从定义查出来的的三个字段。这就需要在Product里定义一个Map属性来存放这些扩展属性,以便动态扩展。mybatis的xml可这么写:
b.查询。
我这里的查询只是把数据友好的显示在一个表格中,表格需要表头与内容。因为这里涉及到一个动态字段的问题,我们并不知道显示的表格有多少个动态字段。所以需要先从定义表查出有多少个字段,还要根据列名合并。比如家电类产品有颜色、型号、功率三个属性,而个人护理/化妆类作品有颜色、厂家两个属性,那么我们展示的属性有颜色、型号、功率、厂家四个属性,颜色属性就合并了。所以要先从定义表里取出这么个结构Map<类型ID,Map<显示字段名,存储字段名(storeCol)>>。这样从这结构里取出所有不同的显示字段名,每一行得到类型ID后,再根据当前列名取出存储字段名,再用当前行对象.存储字段名(obj.field1)就可取出真实值了。
4.属性字段行存储
这种方式与3有很一些类似的解决办法,最大的好处是扩展字段时不需要修改表结构,而不好的主要是查询变困难了。新建一个有如下图3列的表,主键(product_id),属性名(property_name),属性值(property_value)
a.新增与删除。
与3一样需要一个定义属性的地方,但不需要添加field1,field2这样不知什么意思的字段名。直接把上面的storeName的值做为property_name的值。如上面的color。这样再扩展字段时,就添加一行就行。这样输入时的input为storeName(如color)。insert时需插入两张表,先插入主表得到id,再插属性表,属性表的插入行数不能确定,可由输入的属性数来确定。删除时也要删除两张表。
b.查询。
我目前是这样的,select
distinct 主表.* from 主表 left join 属性表 on 主表.id=属性表.主表id where
property_name=?
property_value=?。先这样查出主表信息,兼顾了属性表的条件。再根据这个sql查出的主表id去查属性表相应的属性。
当然要组合这两块数据,还得一个中间的关联结构,如上面的Map<类型ID,Map<显示字段名,存储字段名(storeName)>>。