前段时间写过一篇通用属性系统设计与实现,这种属性设计被我广泛运用于各种复杂的系统设计之中,一切事物的特征均可使用属性来描述。而面对千变万化的业务系统,一套通用的属性体系会为我们减少难以估量的开发任务。甚至我们可以用一个通用的查询方法支持所有类型商品(或文章等)的查询。
这里再将前一篇博客的设计部分(以电商为例)移过来,方便后续理解。
设计思路如下:
1、可自定义的无限级商品类别。
2、各类别可自定义属性,属性的类型有:普通文本、数字、价格、单项选择、多项选择、日期、文本域、富文本、图片、布尔值等,添加商品时自动加载所需的组件。
3、支持公共属性。
4、支持属性继承,即子类别自动继承父类别的属性,并支持覆盖父类别同名属性。
5、支持属性值验证,添加商品时对必填项、正则表达式进行自动验证。
6、支持属性分组,添加商品时属性按照属性分组名进行分组。
模型设计:
Classify:类别表
Attribute:属性表。属性表还有一个关键字段在早期的模型图中没有画出,它是属性的Key。
AttributeOption:属性选项表,只有类别为“单项选择”和“多项选择”时,属性需要设置属性选项。
Product:商品表
ProductAttribute:商品属性关系表
关于查询的设计:
试想这样一个需求:我们有100种类型的商品,其筛选条件和需要查询出的属性各不相同,我们应该如何实现它?刚开始看到这个需求,我想很多小伙伴的内心是崩溃的,这妥妥的一个月以上的工作量啊。为了技(yi)术(lao)创(yong)新(yi)我们来仔细分析这个问题,不难发现其差异点只有两个:
1、筛选条件不同。筛选条件又分基础筛选条件和属性筛选条件,其中只有属性筛选条件不同;
2、查询的属性不同;
我们不妨再试想一下如果我们有一个超级方法,其接收这些不同的筛选条件与查询的属性Key,返回符合条件的数据并且自动组装了我们想要的属性,那么这一个月的工作量可能就只需要两个小时就能搞定了,这便有了属性系统万能查询方法的雏形。
查询的实现:
下面介绍该查询方法在java spring mvc + mybaits环境下的实现:
1、筛选条件封装
public class ProductQueryDTO extends PagedQueryParam { private String queryKey; private int classifyId; private int regionId; private String startTime; private String endTime; //最小价格 private int minValue; //最大价格 private int maxValue; private Map<String, FilterRule[]> attibuteFilters; public ProductQueryDTO() { attibuteFilters = new HashMap<>(); } }
其中attibuteFilters为属性筛选条件,其他为商品基础筛选条件。
属性筛选条件的定义如下:
public class FilterRule { private String key; private String operate; private String value; public FilterRule(){} public FilterRule(String key,String operate,String value) { this.key = key; this.operate = operate; this.value = value; } public FilterRule(String key,String value) { this(key, FilterOperate.EQUAL, value); } }
operate为操作符,其取值包括:
public interface FilterOperate { String AND = "and"; String OR = "or"; String EQUAL = "equal"; String NOTEQUAL = "notequal"; String LESS = "less"; String LESSOREQUAL = "lessorequal"; String GREATER = "greater"; String GREATEROREQUAL = "greaterorequal"; String STARTWITH = "startswith"; String ENDWITH = "endwith"; String CONTAINS = "contains"; }
基类中封装了分页以及排序的字段:
public class PagedQueryParam { private String sortField; private int sortDirection;//0:正序;1:倒序 private int pageIndex; private int pageSize; public PagedQueryParam(){} public PagedQueryParam(int pageIndex,int pageSize) { this(pageIndex, pageSize, "id"); } public PagedQueryParam(int pageIndex,int pageSize,String sortField) { this(pageIndex, pageSize, sortField, 1); } public PagedQueryParam(int pageIndex,int pageSize,String sortField,int sortDirection) { this.pageIndex = pageIndex; this.pageSize = pageSize; this.sortField = sortField; this.sortDirection = sortDirection; } }
2、返回的数据定义
返回的数据包括商品的基础数据,以及商品的属性数据,属性不固定。其定义如下:
public class ProductResultDTO { private Long id; private String name; private String cover; private String pcCover; private float price; private float originPrice; private int browseNo; private int praiseNo; private int commentNo; private int classifyId; private Map<String,String> attribute; private List<String> assets; public ProductResultDTO() { attribute = new HashMap<>(); } }
3、查询方法的实现:
查询方法的实现分为三步,先筛选符合条件的商品基础数据,再根据查询出的id集合查询所需的属性集合,最后组装。相关代码如下:
查询符合条件的商品基础数据:
public class QueryProductProvider { private static final Map<String, String> OperateMap; static { OperateMap = new HashMap<>(); OperateMap.put(FilterOperate.EQUAL, "="); OperateMap.put(FilterOperate.NOTEQUAL, "!="); OperateMap.put(FilterOperate.LESS, "<"); OperateMap.put(FilterOperate.LESSOREQUAL, "<="); OperateMap.put(FilterOperate.GREATER, ">"); OperateMap.put(FilterOperate.GREATEROREQUAL, ">="); } public String QueryProductBriefList(ProductQueryDTO query) { StringBuilder sql = new StringBuilder(); sql.append("SELECT Id AS id,Name AS name,Cover AS cover,PcCover as pcCover, Price AS price,[OriginPrice] AS originPrice,BrowsingNumber AS browseNo," + "PointOfPraise AS priseNo,CommentNo AS commentNo,CommodityScore as commodityScore," + "TotalScore as totalScore,ClassifyId as classifyId " + "FROM Product_Product AS P"); sql.append(where(query)); String sortField = "OrderNo"; int sortDirection = ListSortDirection.ASC;//默认正序 if (!StringHelper.isNullOrWhiteSpace(query.getSortField())) { sortField = StringHelper.toPascalCase(query.getSortField()); sortDirection = query.getSortDirection(); } sql.append(" ORDER BY " + sortField + ""); if (sortDirection == ListSortDirection.DESC) { sql.append(" DESC"); } int pageIndex = query.getPageIndex(); int pageSize = query.getPageSize(); if (pageIndex <= 0) pageIndex = 1; if (pageSize <= 0 || pageSize > 50) pageSize = 15;//一次查询最多获取50条数据,15为默认每页数量。 sql.append(" OFFSET " + (pageIndex - 1) * pageSize + " ROWS FETCH NEXT " + pageSize + " ROWS ONLY"); return sql.toString(); } private String where(ProductQueryDTO query) { StringBuilder sql = new StringBuilder(); sql.append(" WHERE IsOnShelf=1 AND IsDeleted=0"); int classifyId = query.getClassifyId(); if (classifyId > 0) { sql.append(" AND ClassifyId = #{classifyId}"); } String queryKey = query.getQueryKey(); if (!StringHelper.isNullOrWhiteSpace(queryKey)) { sql.append(" AND Name LIKE '%'+#{queryKey}+'%'"); } Integer minValue=query.getMinValue(); if(minValue>0){ sql.append(" AND Price>= #{minValue}"); } Integer maxValue=query.getMaxValue(); if(maxValue>0){ sql.append(" AND Price<= #{maxValue}"); } Integer regionId=query.getRegionId(); if(regionId>0){ sql.append(" AND Id in (select productId from Product_RegionMap where RegionId= #{regionId})"); } String startTime = query.getStartTime(); String endTime = query.getEndTime(); //如果开始时间与结束时间全都为空,则设置为当前时间 if (StringHelper.isNullOrWhiteSpace(startTime) && StringHelper.isNullOrWhiteSpace(endTime)) { String currentTime = DateHelper.getCurrentDateString(null); startTime = currentTime; endTime = currentTime; } if (!StringHelper.isNullOrWhiteSpace(startTime)) { sql.append(" AND OnShelfTime <= '" + startTime + "'"); } if (!StringHelper.isNullOrWhiteSpace(endTime)) { sql.append(" AND OffShelfTime >= '" + endTime + "'"); } Map<String, FilterRule[]> attributeMap = query.getAttibuteFilters(); for (String key : attributeMap.keySet()) { String ruleSql = ""; FilterRule[] rules = attributeMap.get(key); for (FilterRule rule : rules) { String value = rule.getValue(); if (StringHelper.isNullOrWhiteSpace(value)) continue; if (!OperateMap.containsKey(rule.getOperate())) { rule.setOperate(FilterOperate.EQUAL); } //以逗号包裹的值查询选项Id if (value.startsWith(",") && value.endsWith(",")) { ruleSql += " AND AttributeOptionIds like '%" + value + "%'"; } else { ruleSql += " AND value " + OperateMap.get(rule.getOperate()) + " '" + value + "'"; } } if (!StringHelper.isNullOrWhiteSpace(ruleSql)) { sql.append(" AND EXISTS (SELECT 1 FROM Product_ProductAttribute WHERE AttributeId IN (SELECT Id FROM Product_Attribute WHERE [Key] = '" + key + "') " + ruleSql + " AND ProductId = P.Id )"); } } return sql.toString(); } }
再根据查询出的id集合查询所需的属性集合:
public class QueryProductAttributeProvider extends AbstractMybatisProvider { public String QueryProductAttributes(long[] ids, String[] keys) { StringBuilder sql = new StringBuilder(); sql.append("SELECT PA.[ProductId] AS id,A.[Key] AS [key],PA.[Value] AS value " + "FROM [dbo].[Product_ProductAttribute] AS PA " + "LEFT JOIN [dbo].[Product_Attribute] AS A ON PA.[AttributeId]=A.[Id] " + "WHERE PA.ProductId IN (" + ExpandIdAndToString(ids) + ") AND A.[Key] IN (" + ExpandKeysAndToString(keys) + ")"); return sql.toString(); } }
组装:
/** * 通用的商品查询,支持属性自动组装 * * @param query 筛选条件 * @param attributeKeys 需要查询并自动组装的属性Key * @return */ public List<ProductResultDTO> queryProductList(ProductQueryDTO query, String[] attributeKeys) { List<ProductResultDTO> result = productMapper.QueryProductBriefList(query); Collection<Long> idList = CollectionHelper.init(result).select(p -> p.getId()); long[] ids = idList.stream().mapToLong(t -> t.longValue()).toArray(); if (ids.length > 0 && attributeKeys != null && attributeKeys.length > 0) { Map<Long, Map<String, String>> productAttributeMap = new HashMap<>(); List<AttributeValueDTO> attributes = productAttributeMapMapper.getProductAttributeValues(ids, attributeKeys); for (AttributeValueDTO attribute : attributes) { if (!productAttributeMap.containsKey(attribute.getId())) { productAttributeMap.put(attribute.getId(), getEmptyAttributeKeyMap(attributeKeys)); } productAttributeMap.get(attribute.getId()).put(StringHelper.toCamelCase(attribute.getKey()), StringHelper.trim(attribute.getValue(), ',')); } for (ProductResultDTO product : result) { Map<String, String> attributeMap = productAttributeMap.containsKey(product.getId()) ? productAttributeMap.get(product.getId()) : getEmptyAttributeKeyMap(attributeKeys); product.setAttribute(attributeMap); } } return result; }
以上记录了我在开发过程中的一点思考,编码不是机械的重复,更需要我们细致的思考。