# 竞价系统-JoinWrapper
# 概述
由于MybatisPlus不支持联表查询,而在日常的开发需求中,又常常需要进行联表查询, 因此在遇到需要联表时,原来只能在mapper中通过@Select(SQL语句)的方式来实现。 但这种方式有几个问题:
- 使用不方便,整段SQL都需要手写,部分SQL语句需要重复添加(如权限判断)
- 后期不方便维护,甚至少了一个空格都会引发错误
- 可读性差
- 每个联表语句都要写一个单独的方法,无法重用 为了解决以上问题,模仿MybatisPlus的QueryWrapper封装了JoinWrapper。 其本质上还是在利用Mybatis的@Select注解,然后将JoinWrapper作为参数(ew),通过它生成SQL的各个部分。
SELECT ${ew.sqlSelect} FROM ${ew.tableName} ${ew.tableAlias} ${ew.joinPart} ${ew.customSqlSegment}
其中各个部分的意义:
- ${ew.sqlSelect} 返回的字段
- ${ew.tableName} 查询表的表名
- ${ew.tableAlias} 查询表的别名
- ${ew.joinPart} 联表部分SQL
- ${ew.customSqlSegment} 查询条件(包括WHERE、ORDER BY、GROUP BY、HAVING)
# 使用方法
JoinWrapper的功能涵盖了QueryWrapper的功能,除此之外还添加了xxxJoin系列(如,leftJoin、rightJoin等)的方法, 用于实现联表。
Mapper层,需要继承BaseJoinMapper,如
public interface CoOrderMainMapper extends BaseJoinMapper<CoOrderMain> {
}
DAO层,调用baseMapper的joinXXXX系列方法
JoinWrapper<CoOrderMain> wrapper = new JoinWrapper<>(CoOrderMain.class);
//左连接cp_bid_order
wrapper.leftJoin("ORDER_MAIN_ID", "ORDER_MAIN_ID", CpBidOrder.class, item -> {
//为cp_bid_order设置别名
item.setTableAlias("cbo");
//报价状态为Y
item.eq("BID_STATUS", "Y");
return item;
});
//执行SQL得到结果
List<CoOrderMain> orderMainList = baseMapper.joinSelectList(wrapper);
# 构造方法
一下只介绍两种常用的构造方法
//常用构造方法,其中传入class为数据表实体类(CoOrderMain -> co_order_main)
JoinWrapper<CoOrderMain> wrapper = new JoinWrapper(CoOrderMain.class);
//参数依次为数据表实体类,接收DTO类
JoinWrapper<OrderMainDto> wrapper = new JoinWrapper(CoOrderMain.class, OrderMainDto.class);
# 设置别名
联表往往要对查询的表起别名,JoinWrapper提供了两种设置别名的机制:
- 自动设置别名
- JoinWrapper会根据表名自动生成,如co_order_main -> com,cp_bid_order -> cbo, 即,取下划线隔开的每一部分的首字母.
- JoinWrapper在内部维护了一个别名map,用于防止别名冲突,如果发现该别名已经存在, 则在别名后面缀上数字1;如果别名依然存在,则改为缀数字2,依次类推。
- 手动设置别名 调用wrapper.setTableAlias可以手动设置别名,这时就需要调用者自己确定此别名没有被使用过 (包括自动设置的别名)
# 联一个表
参数依次为,联表字段(当前表),联表字段(要联的表),数据库实体类(要联的表),设置联表查询条件的lamda表达式
//基本用法
wrapper.leftJoin("ORDER_MAIN_ID", "ORDER_MAIN_ID", CpBidOrder.class, item -> item);
//进阶用法:联表需要查询条件或者手动设置别名
wrapper.leftJoin("ORDER_MAIN_ID", "ORDER_MAIN_ID", CpBidOrder.class, item -> {
//为cp_bid_order设置别名
item.setTableAlias("cbo");
//cp_bid_order中报价状态为Y的记录
item.eq("BID_STATUS", "Y");
return item;
});
# 联多个表
如果是A表分别需要联B、C表,则分别调用join方法即可
//联cp_bid_order
wrapper.leftJoin("ORDER_MAIN_ID", "ORDER_MAIN_ID", CpBidOrder.class, item -> item);
//联co_order_detail
wrapper.leftJoin("ORDER_MAIN_ID", "ORDER_MAIN_ID", CoOrderDetail.class, item -> item);
如果A表需要联B表,B表又要联C表,则有两种方式
- 方式1:通过指定联表字段的别名
首先指定B的别名为b,随后将联表字段(本数据表)前加上前缀(b.)。就像下面这个例子, 先指定co_user_role的别名为cur,随后再将联表字段指定为cur.ROLE_IDJoinWrapper<CoUser> wrapper = new JoinWrapper<>(CoUser.class); //co_user需要联co_user_role表,翻译为SQL:LEFT JOIN co_user_role cur ON cu.USER_ID = cur.USER_ID wrapper.leftJoin("USER_ID", "USER_ID", CoUserRole.class, item -> item.setTableAlias("cur")); //co_user_role表需要联co_role表,翻译为SQL:LEFT JOIN co_role cr ON cur.ROLE_ID = cr.ROLE_ID wrapper.leftJoin("cur.ROLE_ID", "ROLE_ID", CoRole.class, item -> item);
- 方式2:嵌套调用join方法
在join方法的lamda表达式中再次调用Join方法。这种方法的优点在于不需要指定别名,缺点是降低了可读性。
JoinWrapper<CoUser> wrapper = new JoinWrapper<>(CoUser.class); //co_user联co_user_role表 wrapper.leftJoin("USER_ID", "USER_ID", CoUserRole.class, item -> { //item为co_user_role表的查询构造器 //co_user_role表联co_role表 item.leftJoin("ROLE_ID", "ROLE_ID", CoRole.class, item2 -> item2); return item; });
# 需要联多个字段
当联表的数据表为复合主键时(主键数量是两个或者两个以上),则此时联表字段就应该涵盖多个主键。 此时可以使用join的重载方法,使用Map<String, String>来传递多个字段的对应关系。就像下面这个例子, co_user表的主键为COLLEGE_ID和USER_ID,而co_user_role的主键为COLLEGE_ID、USER_ID和ROLE_ID
JoinWrapper<CoUser> wrapper = new JoinWrapper<>(CoUser.class);
//co_user联co_user_role表
//翻译为SQL:LEFT JOIN co_user_role cur ON cu.COLLEGE_ID = cur.COLLEGE_ID AND cu.USER_ID = cur.USER_ID
wrapper.leftJoin(new HashMap<String, String>(2){{
put("COLLEGE_ID", "COLLEGE_ID");
put("USER_ID", "USER_ID");
}}, CoUserRole.class, item -> item);
# 使用常量值来联表
如果需要生成使用常量值来联表的SQL,形如
LEFT JOIN cp_bid_order cbo ON cbo.ORDER_MAIN_ID = com.ORDER_MAIN_ID AND cbo.BID_STATUS = 'Y'
则可以使用以下方式完成,将联表字段(要联的表)设置为plain:Y,程序会自动取出plain:后面的常量值。 而该值对应的字段如果是当前表,则不用加别名;如果是要联的表,则需要加别名 (如下面的cp_bid_order的BID_STATUS就要加别名)。
wrapper.leftJoin(new HashMap<String, String>(2){{
put("ORDER_MAIN_ID", "ORDER_MAIN_ID");
put("cbo.BID_STATUS", "plain:Y");
}}, CpBidOrder.class, item -> item.setTableAlias("cbo));
注意:此用法只能用于联表字段(要联的表),即联表map中的value,以下为错误用法
wrapper.leftJoin(new HashMap<String, String>(2){{
put("ORDER_MAIN_ID", "ORDER_MAIN_ID");
//错误用法
put("plain:1", "BID_STATUS");
}}, CpBidOrder.class, item -> item);
其实也不一定要用常量值联表来解决此类需求,上面这个SQL其实可以等效于下面这个 (不等效:一个是on连表条件一个是where条件筛选)
LEFT JOIN cp_bid_order cbo ON cbo.ORDER_MAIN_ID = com.ORDER_MAIN_ID WHERE cbo.BID_STATUS = 'Y'
通过程序实现为
wrapper.leftJoin("ORDER_MAIN_ID", "ORDER_MAIN_ID", CpBidOrder.class,
item -> item.eq("BID_STATUS", "Y"));
# 需要返回联表字段
BaseJoinMapper中定义的方法,只能返回CoOrderMain,也就是说,其适用于通过联表作为筛选条件, 但不需要返回联表字段的情况。如果需要返回联表字段,则此时就需要在对应的Mapper中添加返回指定DTO的方法。
@Select(JoinWrapper.SELECT_TEMPLATE)
List<ContractDraftDto> getContractList(Page<ContractDraftDto> page, @Param("ew") JoinWrapper<ContractDraftDto> wrapper);
其中,联表字段需要加@Table(alias=xxx)注解,表示该字段是属于哪个表的,没有标注该注解时,认为是主表字段。
@Data
@Accessors(chain = true)
public class ContractDraftDto {
@Table(alias = "cm")
@ApiModelProperty(value = "主表ID(不展示)")
private String orderMainId;
@Table(alias = "cm")
@ApiModelProperty(value = "申购单号")
private String orderCode;
@ApiModelProperty(value = "明细ID")
private String detailId;
}
像上面这个DTO生成的SQL语句会是,其中ORDER_MAIN_ID和ORDER_CODE均被添加了指定的cm别名, 而没有注解的DETAIL_ID,则被认为是主表字段,添加了主表的别名(cod)
SELECT cm.ORDER_MAIN_ID, cm.ORDER_CODE, cod.DETAIL_ID FROM co_order_detail cod ...
注意:使用这个DTO作为返回DTO时,要记得联别名为cm的表,否则SQL会报错。
# @Table注解
为了方便返回字段的控制,引入了@Table注解,可以标注在DTO的类上或者DTO的字段上,它的可配置项如下:
- alias
联表字段数据表别名,表示该字段属于指定别名的数据表 - suffix
后缀(用于联表重名时,通过在字段后添加后缀避免冲突), 例如A(别名a)、B(别名b)两个表都有IS_SHOW字段,如果都需要返回,那么可以按下面这样标注则对应的SQL为@Table(alias="a") private String isShow; @Table(alias="b", suffix="2") private String isShow2;
这就做到了同时返回两个数据表的同名字段SELECT a.IS_SHOW, b.IS_SHOW IS_SHOW2
- field
真实数据库字段名称(用于与实体类字段名称不一致的情况),甚至配置为数据库函数,如SUM(A)。
例如,下面DTO的detailStatus,其实在数据库字段为STATUS, 而bidAmountSum则需要返回聚合函数SUM(BID_AMOUNT)的值。其对应的SQL为@Table(field="STATUS") private String detailStatus; @Table(field="SUM(BID_AMOUNT)") private String bidAmountSum;
SELECT STATUS DETAIL_STATUS, SUM(BID_AMOUNT) BID_AMOUNT_SUM
- ignore
类型为Boolean,因此值为true/false。 当DTO需要某个字段,而数据库中又没有对应字段时,需要在该字段上标注@Table(ignore=true), 否则SQL查询会报错。@Table(ignore=true) private String notExist;