通用属性系统万能查询方法

前段时间写过一篇通用属性系统设计与实现,这种属性设计被我广泛运用于各种复杂的系统设计之中,一切事物的特征均可使用属性来描述。而面对千变万化的业务系统,一套通用的属性体系会为我们减少难以估量的开发任务。甚至我们可以用一个通用的查询方法支持所有类型商品(或文章等)的查询。
 
这里再将前一篇博客的设计部分(以电商为例)移过来,方便后续理解。
设计思路如下:
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;
    }
 
以上记录了我在开发过程中的一点思考,编码不是机械的重复,更需要我们细致的思考。
 
 
posted @ 2017-07-31 09:05  _liuxx  阅读(3170)  评论(7编辑  收藏  举报