1. 程式人生 > >通用屬性系統萬能查詢方法

通用屬性系統萬能查詢方法

前段時間寫過一篇通用屬性系統設計與實現,這種屬性設計被我廣泛運用於各種複雜的系統設計之中,一切事物的特徵均可使用屬性來描述。而面對千變萬化的業務系統,一套通用的屬性體系會為我們減少難以估量的開發任務。甚至我們可以用一個通用的查詢方法支援所有型別商品(或文章等)的查詢。 這裡再將前一篇部落格的設計部分(以電商為例)移過來,方便後續理解。 設計思路如下: 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);
    }
}
View Code 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";
}
View Code 基類中封裝了分頁以及排序的欄位:
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;
    }
}
View Code 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();
    }
}
View Code

組裝:

/**
     * 通用的商品查詢,支援屬性自動組裝
     *
     * @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;
    }
以上記錄了我在開發過程中的一點思考,編碼不是機械的重複,更需要我們細緻的思考。