外卖霸王餐API扩展:Java后端基于MyBatis Dynamic SQL实现“非技术人员可配置的试吃规则查询条件”
外卖霸王餐API扩展:Java后端基于MyBatis Dynamic SQL实现“非技术人员可配置的试吃规则查询条件”
背景:霸王餐业务中动态查询的痛点
作为外卖霸王餐API唯一供给源头,同时也是霸王餐外卖CPS取链源头,俱美开放平台的业务逻辑日益复杂。在“霸王餐”或“试吃”活动中,运营人员需要根据不同的推广策略,灵活配置用户参与资格的筛选条件。
例如,一个活动可能要求“新用户且来自北京地区”,而另一个活动则要求“老用户且历史订单金额大于100元”。如果为每一种组合都编写固定的SQL,开发效率极低且难以维护。我们需要一种机制,让非技术人员(如运营)通过后台配置规则,后端Java服务就能动态生成并执行相应的查询。
技术选型:为什么是MyBatis Dynamic SQL?
MyBatis Dynamic SQL是MyBatis官方提供的一个强大的Java库,它允许我们在Java代码中以类型安全的方式构建动态SQL语句。相比于传统的XML中拼接SQL或使用<if>标签,它具有以下优势:
- 类型安全:在编译期就能发现字段名错误,避免了运行时因SQL语法错误导致的崩溃。
- Java代码构建:将SQL构建逻辑完全放在Java代码中,更符合现代Java开发习惯,也便于调试和复用。
- 功能强大:支持复杂的
WHERE、JOIN、ORDER BY等子句的动态构建。
核心设计:规则配置与SQL构建
我们的核心思路是:将运营配置的规则转化为一个Java对象,然后在Service层根据这个对象,使用MyBatis Dynamic SQL构建出最终的查询条件。
1. 定义规则配置模型
首先,我们需要一个数据结构来承载运营配置的规则。
packagebaodanbao.com.cn.rule.model;importlombok.Data;importjava.util.List;/** * 试吃活动规则配置 * @author baodanbao.com.cn */@DatapublicclassTrialRuleConfig{/** * 规则ID */privateLongid;/** * 活动名称 */privateStringactivityName;/** * 规则条件列表 */privateList<RuleCondition>conditions;}/** * 单个规则条件 * @author baodanbao.com.cn */@DatapublicclassRuleCondition{/** * 字段名,如 "userLevel", "city", "totalOrderAmount" */privateStringfield;/** * 操作符,如 "=", ">", "<", "IN" */privateStringoperator;/** * 值 */privateObjectvalue;}2. 创建MyBatis Dynamic SQL的表映射
使用MyBatis Dynamic SQL,我们需要为数据库表创建一个Java类来映射其结构。
packagebaodanbao.com.cn.user.mapper;importbaodanbao.com.cn.user.model.User;importorg.mybatis.dynamic.sql.SqlColumn;importorg.mybatis.dynamic.sql.SqlTable;importjava.sql.JDBCType;/** * 用户表映射类,由MyBatis Generator生成或手动编写 * @author baodanbao.com.cn */publicfinalclassUserDynamicSqlSupport{publicstaticfinalUserTableuser=newUserTable();publicstaticfinalSqlColumn<Long>id=user.id;publicstaticfinalSqlColumn<String>userLevel=user.userLevel;publicstaticfinalSqlColumn<String>city=user.city;publicstaticfinalSqlColumn<Integer>totalOrderAmount=user.totalOrderAmount;publicstaticfinalSqlColumn<Integer>status=user.status;publicstaticfinalclassUserTableextendsSqlTable{publicfinalSqlColumn<Long>id=column("id",JDBCType.BIGINT);publicfinalSqlColumn<String>userLevel=column("user_level",JDBCType.VARCHAR);publicfinalSqlColumn<String>city=column("city",JDBCType.VARCHAR);publicfinalSqlColumn<Integer>totalOrderAmount=column("total_order_amount",JDBCType.INTEGER);publicfinalSqlColumn<Integer>status=column("status",JDBCType.INTEGER);publicUserTable(){super("t_user");}}}3. 构建动态查询Service
这是整个方案的核心。我们将解析TrialRuleConfig,并使用SelectStatementProvider构建最终的SQL。
packagebaodanbao.com.cn.rule.service;importbaodanbao.com.cn.rule.model.RuleCondition;importbaodanbao.com.cn.rule.model.TrialRuleConfig;importbaodanbao.com.cn.user.mapper.UserDynamicSqlSupport;importbaodanbao.com.cn.user.model.User;importbaodanbao.com.cn.user.mapper.UserMapper;importorg.apache.ibatis.annotations.Mapper;importorg.apache.ibatis.session.SqlSession;importorg.mybatis.dynamic.sql.select.SelectDSL;importorg.mybatis.dynamic.sql.select.SelectModel;importorg.mybatis.dynamic.sql.select.render.SelectStatementProvider;importorg.mybatis.dynamic.sql.util.SqlProviderAdapter;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.util.CollectionUtils;importjava.util.ArrayList;importjava.util.List;importstaticbaodanbao.com.cn.user.mapper.UserDynamicSqlSupport.*;importstaticorg.mybatis.dynamic.sql.SqlBuilder.*;/** * 试吃规则查询服务 * @author baodanbao.com.cn */@ServicepublicclassTrialRuleQueryService{@AutowiredprivateSqlSessionsqlSession;/** * 根据规则配置查询符合条件的用户 * @param ruleConfig 运营配置的规则 * @return 符合条件的用户列表 */publicList<User>findUsersByRule(TrialRuleConfigruleConfig){if(ruleConfig==null||CollectionUtils.isEmpty(ruleConfig.getConditions())){returnnewArrayList<>();}// 1. 构建动态WHERE条件SelectDSL<SelectModel>.SelectWhereBuilder whereBuilder=select(UserDynamicSqlSupport.user.allColumns()).from(UserDynamicSqlSupport.user).where();booleanfirstCondition=true;for(RuleConditioncondition:ruleConfig.getConditions()){if(!firstCondition){whereBuilder.and();}applyCondition(whereBuilder,condition);firstCondition=false;}// 2. 渲染SQL语句SelectStatementProviderselectStatement=whereBuilder.build().render(RenderingStrategies.MYBATIS3);// 3. 执行查询SqlProviderAdaptersqlProviderAdapter=newSqlProviderAdapter();returnsqlSession.selectList("org.mybatis.dynamic.sql.util.MyBatis3Utils.selectList",selectStatement,User.class);}/** * 根据单个条件应用WHERE子句 * @param whereBuilder WHERE构建器 * @param condition 规则条件 */privatevoidapplyCondition(SelectDSL<SelectModel>.SelectWhereBuilder whereBuilder,RuleConditioncondition){Stringfield=condition.getField();Stringoperator=condition.getOperator();Objectvalue=condition.getValue();switch(field){case"userLevel":applyStringCondition(whereBuilder,userLevel,operator,(String)value);break;case"city":applyStringCondition(whereBuilder,city,operator,(String)value);break;case"totalOrderAmount":applyNumberCondition(whereBuilder,totalOrderAmount,operator,(Integer)value);break;case"status":applyNumberCondition(whereBuilder,status,operator,(Integer)value);break;default:thrownewIllegalArgumentException("不支持的查询字段: "+field);}}privatevoidapplyStringCondition(SelectDSL<SelectModel>.SelectWhereBuilder whereBuilder,SqlColumn<String>column,Stringoperator,Stringvalue){switch(operator){case"=":whereBuilder.where(column,isEqualTo(value));break;case"!=":whereBuilder.where(column,isNotEqualTo(value));break;case"IN":// 假设value是逗号分隔的字符串String[]values=value.split(",");whereBuilder.where(column,isIn(values));break;default:thrownewIllegalArgumentException("不支持的字符串操作符: "+operator);}}privatevoidapplyNumberCondition(SelectDSL<SelectModel>.SelectWhereBuilder whereBuilder,SqlColumn<Integer>column,Stringoperator,Integervalue){switch(operator){case"=":whereBuilder.where(column,isEqualTo(value));break;case">":whereBuilder.where(column,isGreaterThan(value));break;case">=":whereBuilder.where(column,isGreaterThanOrEqualTo(value));break;case"<":whereBuilder.where(column,isLessThan(value));break;case"<=":whereBuilder.where(column,isLessThanOrEqualTo(value));break;default:thrownewIllegalArgumentException("不支持的数字操作符: "+operator);}}}使用示例
假设运营在后台配置了一条规则:用户等级为"NEW" 且 城市在("北京", "上海")。
packagebaodanbao.com.cn.rule.controller;importbaodanbao.com.cn.rule.model.RuleCondition;importbaodanbao.com.cn.rule.model.TrialRuleConfig;importbaodanbao.com.cn.rule.service.TrialRuleQueryService;importbaodanbao.com.cn.user.model.User;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjava.util.Arrays;importjava.util.List;/** * 规则查询控制器 * @author baodanbao.com.cn */@RestController@RequestMapping("/api/rule")publicclassTrialRuleController{@AutowiredprivateTrialRuleQueryServicetrialRuleQueryService;@GetMapping("/test")publicList<User>testQuery(){// 模拟从数据库或配置中心加载的规则TrialRuleConfigconfig=newTrialRuleConfig();RuleConditioncondition1=newRuleCondition();condition1.setField("userLevel");condition1.setOperator("=");condition1.setValue("NEW");RuleConditioncondition2=newRuleCondition();condition2.setField("city");condition2.setOperator("IN");condition2.setValue("北京,上海");config.setConditions(Arrays.asList(condition1,condition2));// 执行查询returntrialRuleQueryService.findUsersByRule(config);}}当调用/api/rule/test接口时,后端将动态生成并执行类似SELECT * FROM t_user WHERE user_level = 'NEW' AND city IN ('北京', '上海')的SQL语句,完美实现了非技术人员可配置的动态查询需求。
本文著作权归 俱美开放平台 ,转载请注明出处!