树形数据主从表扩展字段设计实现


今天跟大家主要是分享一个设计思路:树形数据,扩展属性存储、展示设计。
一、树形、扩展属性关系说明
我这里是将空间、楼栋、单元、楼层、房5类数据抽取相同属性、可一致的字段到一张表,设计为树形。以属性字段data_type区分数据类型。表结构:

CREATE TABLE `space_reach_house_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `data_type` int DEFAULT NULL COMMENT '数据类型:1空间,2楼栋,3单元,4楼层,5房',
  `data_status` int DEFAULT NULL COMMENT '数据状态:1未发布,2已发布',
  `house_type` int DEFAULT NULL COMMENT '房源类型,1分布式公寓,2集中式公寓',
  `name` varchar(300) DEFAULT NULL COMMENT '名称',
  `customize_name` varchar(300) DEFAULT NULL COMMENT '自定义名称',
  `code` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '编码',
  `parent_id` bigint DEFAULT NULL COMMENT '父级id',
  `parent_ids` varchar(4000) DEFAULT NULL COMMENT '父级ids,以,隔开',
  `data_level` int DEFAULT NULL COMMENT '数据层级',
  `sort_num` int DEFAULT NULL COMMENT '排序号',
  `subject_id` int DEFAULT NULL COMMENT '项目id',
  `org_id` int DEFAULT NULL COMMENT '公司id',
  `longitude` double(10,6) DEFAULT NULL COMMENT '经度',
  `latitude` double(10,6) DEFAULT NULL COMMENT '维度',
  `memo` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '备注',
  `principal_id` bigint DEFAULT NULL COMMENT '负责人id',
  `principal_mobile` varchar(20) DEFAULT NULL COMMENT '负责人电话',
  `create_by` int DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` int DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `is_deleted` int DEFAULT NULL COMMENT '是否删除,0否,1是',
  PRIMARY KEY (`id`),
  KEY `org_id_idx` (`org_id`) USING BTREE COMMENT '公司id索引',
  KEY `subject_id_idx` (`subject_id`) USING BTREE COMMENT '项目id索引',
  KEY `parent_id_idx` (`parent_id`) USING BTREE COMMENT '父级id索引',
  KEY `data_type_idx` (`data_type`) USING BTREE COMMENT '数据类型索引'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='空间以及房产信息表';

因为每类数据扩展字段肯定是差异化的,而且取值与字段类型都可能不一样,如果全设置在这张主表,有些数据看到某个字段会感觉莫名其妙,而且表字段也会特别多。所以我这里的思路:差异化字段存储在扩展信息表,有业务逻辑的与前端沟通好设置的字段名定死,后期如果扩展字段用的比较多,做数据过滤频繁再考虑迁移到主表(实际就在从表也是可以过滤查询,就是会有点麻烦,一会见我后面的内容),从表表结构:

CREATE TABLE `space_reach_house_extend` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `space_reach_house_id` bigint DEFAULT NULL COMMENT '空间房源主表id',
  `en_name` varchar(50) DEFAULT NULL COMMENT '字段key',
  `cn_name` varchar(200) DEFAULT NULL COMMENT '字段中文名',
  `val` varchar(200) DEFAULT NULL COMMENT '字段值',
  PRIMARY KEY (`id`),
  KEY `space_reach_house_id_idx` (`space_reach_house_id`) USING BTREE COMMENT '空间房源主记录id索引',
  KEY `en_name_idx` (`en_name`) USING BTREE COMMENT '扩展属性字符串索引'
) ENGINE=InnoDB AUTO_INCREMENT=165 DEFAULT CHARSET=utf8;

从表的设计就是设置成键值对,与主表主键关联。
二、查询思路
我这里是如果是扩展字段做过滤查询,那么传参就要前端把en_name、val传参到后端,看看我的postman请求示例:

{
     
    "spaceReachHouseInfo": {
     
        "subjectId": 91, 
        "orgId": 41, 
        "parentId": 20, 
        "houseType": 1 
    },
    "extendList": [ //扩展信息,不在主表上的数据,传enName、val,必传
        {
     
            "enName": "atFloor",
            "val": "1"
        },
        {
     
            "enName": "houseRoomType",
            "val": "3"
        },
        {
     
            "enName": "houseHallType",
            "val": "2"
        },
        {
     
            "enName": "houseToiletType",
            "val": "1"
        }
    ]
}

看看我的扩展表数据可能更直观,这里给一条看下:
树形数据主从表扩展字段设计实现_第1张图片
这是主表id为43的扩展字段信息,键值对。这里特殊字段是冗余了一份xxName字段存储的,在新增、修改时同步处理,补充到前台传参的这种只有id、value值的扩展字段extendList里面进行冗余保存。
三、mybatis的过滤sql
上面可以看到将要过滤的字段、字段值都放在extendList里传过来了,我这里全部时做的=过滤。其实如果要做其他过滤,可以扩展一个filterType,例如传1:=过滤、2:like过滤,3:<过滤等等。看我的这个列表查询sql:

<select id="querySpaceHouseDtoData"
            parameterType="com.fillersmart.fsihouse.data.vo.spacereachhouse.SpaceReachHouseInfoPageVo"
            resultMap="SpaceReachHouseDtoSubjectParentMap">
        SELECT r.*
                FROM (
                SELECT
        <if test="extendList != null and extendList.size() != 0">
            <foreach collection="extendList" item="item">
                (SELECT #{item.val}
                 FROM space_reach_house_extend e
                 WHERE e.space_reach_house_id = s.id
                   AND e.en_name = #{item.enName}) AS #{item.enName},
            </foreach>
        </if>
        s.*
                FROM space_reach_house_info s
                WHERE 1 = 1
        <if test="spaceReachHouseInfo.orgId != null">
            AND s.org_id = #{spaceReachHouseInfo.orgId}
        </if>
        <if test="spaceReachHouseInfo.subjectId != null">
            AND s.subject_id = #{spaceReachHouseInfo.subjectId}
        </if>
        <if test="spaceReachHouseInfo.houseType != null">
            AND s.house_type = #{spaceReachHouseInfo.houseType}
        </if>
        <if test="spaceReachHouseInfo.dataType != null">
            AND s.data_type = #{spaceReachHouseInfo.dataType}
        </if>
        <if test="spaceReachHouseInfo.parentIds != null and spaceReachHouseInfo.parentIds != ''">
            AND s.parent_ids Like CONCAT('', #{spaceReachHouseInfo.parentIds}, '%')
        </if>
        AND s.is_deleted = 0
        <if test="spaceReachHouseInfo.principalId != null">
            AND s.principal_id = #{spaceReachHouseInfo.principalId}
        </if>
        <if test="spaceReachHouseInfo.dataStatus != null">
            AND s.data_status = #{spaceReachHouseInfo.dataStatus}
        </if>
        <if test="spaceReachHouseInfo.code != null and spaceReachHouseInfo.code != ''">
            AND s.code LIKE CONCAT('%'
                    , #{spaceReachHouseInfo.code}
                    , '%')
        </if>
        ) r
                WHERE 1 = 1
        <if test="extendList != null and extendList.size() != 0">
            <foreach collection="extendList" item="item">
                AND r.${item.enName} = #{item.val}
            </foreach>
        </if>
    </select>

核心就在2个extendList的遍历处理,一个在字段处拼接子查询、一个在最外层的条件处拼接过滤条件,都是依赖传参的en_name、val。
思路大家可以好好体会,这里要说的是:
1、内层一定要将主表的一些过滤条件拼接
2、子表最好是将主表id、en_name的索引建上
以上实际是为了提高性能,其实是不建议搞这种某个字段子查询的,大厂都是说要做表连接查询,不做子查询,但是这里我也确实只想到了这个巧方法,如果后期有性能瓶颈,那肯定是某个字段不适合存储在扩展表了,那就要迁移到主表上了。
这样java代码部分还是可以使用pageHelper分页查询,就分享到这里,如果大家有好的设计思路可以告诉我一声,共同进步。
谢谢大家。

你可能感兴趣的:(数据库,业务设计思路,树形表结构设计,树形扩展信息表设计,主从树形设计,扩展字段键值对设计,扩展字段行存储设计)