多对多结构的通用处理
前言:本文基于 Mybatis-Plus Lombok 编写
在日常开发过程中,经常会遇到一些多对多的结构,如图:

上图表示了一个经典的RBAC
安全结构的一部分,用户和角色是一个多对多的关系。在关系型数据库中,通常的做法是创建一个链接键表,存储二者之间的链接关系。一个项目中往往存在多个多对多关系,也就有多个这样的链接键表,虽然表不一样,对应的外键字段也不一样,准备的Mapper
也不一样,但是其操作不能说毫无差别吧,只能是一模一样——无非是查找对应的数据和修改对应的数据。
所以,我们可以类似 Mybatis-Plus 的做法,定义并实现通用接口,再让对应的Service
继承即可。
一、设计接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
|
public interface LkService<A extends BaseEntity, B extends BaseEntity, LK extends BaseEntity & LkEntity> {
void createLink(Long aId, Long bId);
List<A> getAList(Long bId);
List<B> getBList(Long aId);
List<Long> getAIdList(Long bId);
List<Long> getBIdList(Long aId);
List<LK> getLkListByAId(Long aId);
List<LK> getLkListByBId(Long bId);
void updateALinks(Long aId, List<Long> bIds);
void updateBLinks(Long bId, List<Long> aIds); }
@Data public class BaseEntity implements Serializable { @Serial private static final long serialVersionUID = 1L;
@TableId private Long id; }
public interface LkEntity { Long getAId();
void setAId(Long aId);
Long getBId();
void setBId(Long bId); }
|
二、通用实现
/(ㄒoㄒ)/ ~~ 比较头疼的是,这里使用了六个泛型,分别对应了链接键的两个端点实体类和链接键实体类,以及它们的Mapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
|
@Slf4j @AllArgsConstructor public class LkServiceImpl<A extends BaseEntity, B extends BaseEntity, LK extends BaseEntity & LkEntity, AMapper extends BaseMapper<A>, BMapper extends BaseMapper<B>, LKMapper extends BaseMapper<LK>> extends ServiceImpl<LKMapper, LK> implements LkService<A, B, LK>, IService<LK> { private final AMapper aMapper; private final BMapper bMapper; private final Class<LK> clazz;
@Override public void createLink(Long aId, Long bId) { try { LK lk = clazz.getDeclaredConstructor().newInstance(); lk.setAId(aId); lk.setBId(bId); save(lk); } catch (InstantiationException e) { throw new RuntimeException("链接键是抽象类或接口,或者链接键没有无参构造函数"); } catch (IllegalAccessException e) { throw new RuntimeException("链接键构造函数私有化"); } catch (InvocationTargetException e) { throw new RuntimeException("链接键构造函数内部异常"); } catch (NoSuchMethodException e) { throw new RuntimeException("链接键没有符合要求的构造函数"); } }
@Override public List<A> getAList(Long bId) { List<Long> aIdList = getAIdList(bId); if (aIdList.isEmpty()) return new ArrayList<>(); return aMapper.selectByIds(aIdList); }
@Override public List<B> getBList(Long aId) { List<Long> bIdList = getBIdList(aId); if (bIdList.isEmpty()) return new ArrayList<>(); return bMapper.selectByIds(bIdList); }
@Override public List<Long> getAIdList(Long bId) { List<LK> lkList = getLkListByBId(bId); var aIds = new ArrayList<Long>(); lkList.forEach(lk -> aIds.add(lk.getAId())); return aIds; }
@Override public List<Long> getBIdList(Long aId) { List<LK> lkList = getLkListByAId(aId); var bIds = new ArrayList<Long>(); lkList.forEach(lk -> bIds.add(lk.getBId())); return bIds; }
@Override public List<LK> getLkListByAId(Long aId) { return list(new QueryWrapper<LK>().eq(getFieldName("A"), aId)); }
@Override public List<LK> getLkListByBId(Long bId) { return list(new QueryWrapper<LK>().eq(getFieldName("B"), bId)); }
@Override public void updateALinks(Long aId, List<Long> bIds) { removeByIds(getLkListByAId(aId)); bIds.forEach(bId -> createLink(aId, bId)); }
@Override public void updateBLinks(Long bId, List<Long> aIds) { removeByIds(getLkListByBId(bId)); aIds.forEach(aId -> createLink(aId, bId)); }
private String getFieldName(String target) { for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(LkField.class)) { LkField lkField = field.getAnnotation(LkField.class); if (lkField.position().equals(target)) { String fieldName = lkField.fieldName(); if (fieldName.equals("no.set")) fieldName = field.getName(); return LocalStringUtils.convertToUnderscore(fieldName); } } } throw new IllegalArgumentException("未使用 @LkField 注解标明链接端点字段"); } }
|
使用例:
注意:需要在构造函数处注入并传参
1 2 3 4 5 6 7 8
| @Service public class LkUserRoleServiceImpl extends LkServiceImpl<User, Role, ILkUserRole, UserMapper, RoleMapper, LkUserRoleMapper> implements LkUserRoleService {
@Autowired public LkUserRoleServiceImpl(UserMapper userMapper, RoleMapper roleMapper) { super(userMapper, roleMapper, ILkUserRole.class); } }
|
三、解释部分重要的底层代码
- 创建链接记录:
因为泛型在编译阶段会被擦除,所以不能直接用其创建对象,需要在构造函数注入的阶段,传入一个对应的Class对象用作生成。
关键代码:
1
| LK lk = clazz.getDeclaredConstructor().newInstance();
|
@LkField
注解解释与配置:
在上面的实现中,编写了一个私有方法,解析了@LkField
这个注解。我们注意到,对于链接键表,其端点字段均不同,如例子中的为user_id
和role_id
。为了解决这个问题,我们需要在实体类上对应的属性加上注解,标明哪些是端点字段。
我们规定,fieldName
为字段名,若未设置,默认解析类的属性名的下划线格式为字段名;position
为对应位置,只传入A
或B
,对应链接键的两个端点。
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
private String getFieldName(String target) { for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(LkField.class)) { LkField lkField = field.getAnnotation(LkField.class); if (lkField.position().equals(target)) { String fieldName = lkField.fieldName(); if (fieldName.equals("no.set")) fieldName = field.getName(); return LocalStringUtils.convertToUnderscore(fieldName); } } } throw new IllegalArgumentException("未使用 @LkField 注解标明链接端点字段"); }
|
注解代码:
1 2 3 4 5 6
| @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface LkField { String fieldName() default "no.set"; String position(); }
|
使用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
@EqualsAndHashCode(callSuper = true) @TableName(value = "lk_user_role") @Data public class ILkUserRole extends BaseEntity implements LkEntity {
@LkField(position = "A") private Long userId;
@LkField(position = "B") private Long roleId; @Override public Long getAId() { return userId; } @Override public void setAId(Long aId) { userId = aId; } @Override public Long getBId() { return roleId; } @Override public void setBId(Long bId) { roleId = bId; } }
|