前段時間寫過一篇
通用屬性系統設計與實現,這種屬性設計被我廣泛運用於各種復雜的系統設計之中,一切事物的特征均可使用屬性來描述。而面對千變萬化的業務系統,一套通用的屬性體系會為我們減少難以估量的開發任務。甚至我們可以用一個通用的查詢方法支持所有類型商品(或文章等)的查詢。
這里再將前一篇博客的設計部分(以電商為例)移過來,方便后續理解。
設計思路如下:
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\n" + "FROM [dbo].[Product_ProductAttribute] AS PA \n" + "LEFT JOIN [dbo].[Product_Attribute] AS A ON PA.[AttributeId]=A.[Id]\n" + "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; }
以上記錄了我在開發過程中的一點思考,編碼不是機械的重復,更需要我們細致的思考。