zoukankan      html  css  js  c++  java
  • 通用属性系统万能查询方法

    前段时间写过一篇通用属性系统设计与实现,这种属性设计被我广泛运用于各种复杂的系统设计之中,一切事物的特征均可使用属性来描述。而面对千变万化的业务系统,一套通用的属性体系会为我们减少难以估量的开发任务。甚至我们可以用一个通用的查询方法支持所有类型商品(或文章等)的查询。
     
    这里再将前一篇博客的设计部分(以电商为例)移过来,方便后续理解。
    设计思路如下:
    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
    " +
                    "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();
        }
    }
    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;
        }
     
    以上记录了我在开发过程中的一点思考,编码不是机械的重复,更需要我们细致的思考。
     
     
  • 相关阅读:
    六. 异常处理5.多重catch语句的使用
    六. 异常处理4.try和catch的使用
    六. 异常处理3.未被捕获的异常
    六. 异常处理2.异常类型
    对mysql数据库中字段为空的处理
    mysql 中实现多条数据同时更新
    java 用PDFBox 删除 PDF文件中的某一页
    java7 java MethodHandle解析
    【十四】jvm 性能调优实例
    【十三】jvm 性能调优工具之 jstack
  • 原文地址:https://www.cnblogs.com/liuyh/p/7251549.html
Copyright © 2011-2022 走看看