# 竞价系统-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_ID
    JoinWrapper<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字段,如果都需要返回,那么可以按下面这样标注
    @Table(alias="a")
    private String isShow;
    
    @Table(alias="b", suffix="2")
    private String isShow2;
    
    则对应的SQL为
    SELECT a.IS_SHOW, b.IS_SHOW IS_SHOW2
    
    这就做到了同时返回两个数据表的同名字段
  • field
    真实数据库字段名称(用于与实体类字段名称不一致的情况),甚至配置为数据库函数,如SUM(A)。
    例如,下面DTO的detailStatus,其实在数据库字段为STATUS, 而bidAmountSum则需要返回聚合函数SUM(BID_AMOUNT)的值。
    @Table(field="STATUS")
    private String detailStatus;
    
    @Table(field="SUM(BID_AMOUNT)")
    private String bidAmountSum;
    
    其对应的SQL为
    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;
    
Last Updated: 2/5/2021, 9:41:58 AM