使用AOP+反射實(shí)現(xiàn)自定義Mybatis多表關(guān)聯(lián)查詢
一、需求
目前使用的ORM框架是Mybatis Plus,是Mybatis的增強(qiáng)框架,基礎(chǔ)的CRUD的方法都集成了,開發(fā)起來很是方便。但是項(xiàng)目中總是需要多表關(guān)聯(lián)查詢。
Mybatis的多表關(guān)聯(lián)有兩種
一、在Mapper中使用@Result @One @Many注解
二、在xml文件中配置對應(yīng)的resultMap和關(guān)聯(lián)標(biāo)簽
使用起來很不方便。JPA倒是有多表關(guān)聯(lián)的注解實(shí)現(xiàn),但是不想再引入另一個ORM框架。
目前的需求是增強(qiáng)現(xiàn)有的查詢,使用簡單的注解即可實(shí)現(xiàn)多表關(guān)聯(lián)。
二、核心代碼
GitHub:https://github.com/sushengbuyu/mybatis-mapping-demo
實(shí)現(xiàn)該功能總共需要四個文件

兩個自定義注解,一個虛擬Mapper,一個切面處理類
源碼
MapTo
自定義映射注解,標(biāo)注需要映射處理的字段
import java.lang.annotation.*;
/**
* @author victor
* @desc 自定義多表關(guān)聯(lián)映射注解
* @date 2022/5/23
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MapTo {
/**
* 映射目標(biāo)
*/
Class<?> targetClass();
/**
* 執(zhí)行SQL
*/
String sql();
/**
* 嵌套處理
* 為true時,如果映射的對象類中有映射字段,也執(zhí)行映射操作
*/
boolean doDeep() default false;
}DoMap
自定義映射處理注解,標(biāo)注需要執(zhí)行映射的方法
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author victor
* @desc 標(biāo)注該需要執(zhí)行映射處理的方法
* @date 2022/5/23
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoMap {
/**
* 需要處理映射的類
* @return Class
*/
Class<?> targetClass();
/**
* spel表達(dá)式
* 默認(rèn)為空
* @return String
*/
String spel() default "";
}IDualMapper
虛擬Mapper,用來執(zhí)行自定義SQL
import java.util.List;
import java.util.Map;
/**
* @author victor
* @desc 虛擬Mapper
* @date 2022/5/23
*/
@Mapper
public interface IDualMapper {
/**
* 執(zhí)行自定義SQL
* @param sql sql
* @return List<Map<String, Object>>
*/
List<Map<String, Object>> executeSql(String sql);
}DualMapper
使用者自行實(shí)現(xiàn)DualMapper,解除mybatis強(qiáng)依賴
package sushengbuyu.maptodemo.sys.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import sushengbuyu.maptodemo.aop.IDualMapper;
import java.util.List;
import java.util.Map;
/**
* @author victor
* @desc 虛擬Mapper
* @date 2022/5/23
*/
@Mapper
public interface DualMapper extends IDualMapper {
/**
* 執(zhí)行自定義SQL
* @param sql sql
* @return List<Map<String, Object>>
*/
@Select("${sql}")
List<Map<String, Object>> executeSql(@Param("sql") String sql);
}DoMapAspect
切面處理類,核心代碼,執(zhí)行映射操作
package sushengbuyu.maptodemo.aop;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author victor
* @desc 自定義關(guān)聯(lián)映射切面
* @date 2022/5/23
*/
@Component
@Aspect
public class DoMapAspect {
private final static Logger log = LoggerFactory.getLogger(DoMapAspect.class);
/**
* 保存MapTo映射關(guān)系
* key 映射字段所在類
* value 映射字段集合
*/
private static final Map<Class<?>, Set<String>> MAPPING = new HashMap<>(8);
private final IDualMapper dualMapper;
public DoMapAspect(DualMapper dualMapper) {
this.dualMapper = dualMapper;
}
/**
* 初始化映射關(guān)系
* 掃描指定包下所有類,找出帶有MapTo注解的字段
* 存儲映射數(shù)據(jù)
*/
@PostConstruct
public void initMap() {
// 初始化所有MapTo對象
// 掃描所有類
Set<Class<?>> classes = ClassUtil.scanPackage("sushengbuyu.maptodemo");
int totalField = 0;
// 找出使用MapTo注解的對象
for (Class<?> c : classes) {
Field[] fields = c.getDeclaredFields();
for (Field f : fields) {
if (null != f.getAnnotation(MapTo.class)){
log.info("找到需要映射的字段: 類名:{} - 字段名:{}", c.getName(), f.getName());
// 保存映射關(guān)系
Set<String> set;
if (MAPPING.containsKey(c)) {
set = MAPPING.get(c);
} else {
set = new HashSet<>();
}
set.add(f.getName());
MAPPING.put(c, set);
totalField++;
}
}
}
log.info("總計(jì){}個映射類,{}個映射字段", MAPPING.size(), totalField);
}
/**
* 切點(diǎn)
* @param doMap 執(zhí)行映射注解
*/
@Pointcut("@annotation(doMap)")
public void point(DoMap doMap){}
/**
* 處理關(guān)聯(lián)映射
* @param point 切點(diǎn)
* @param doMap 映射處理配置
* @return Object
* @throws Throwable 異常
*/
@Around(value = "@annotation(doMap)")
public Object doMap(ProceedingJoinPoint point, DoMap doMap) throws Throwable {
// 執(zhí)行切面方法
Object obj = point.proceed();
try {
Object relObj = obj;
if (StringUtils.hasLength(doMap.spel())) {
// 如果使用了SPEL表達(dá)式,則從返回值中獲取處理對象
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(doMap.spel());
relObj = expression.getValue(obj);
}
// 獲取映射類
Class<?> c = doMap.targetClass();
// 映射處理
doMapping(c, relObj);
} catch (Exception e) {
log.error("映射異常", e);
}
log.info("返回對象:{}", obj);
return obj;
}
private void doMapping(Class<?> c, Object obj) throws Exception {
if (obj instanceof Collection) {
// 集合
Collection<?> co = (Collection<?>) obj;
for (Object o : co) {
mapping(c, o);
}
} else {
// 單個對象
mapping(c, obj);
}
}
private void mapping(Class<?> c, Object obj) throws Exception {
// 判斷是否有映射關(guān)系
if (MAPPING.containsKey(c)) {
log.info("處理映射類:{}", c.getName());
// 從緩存中獲取映射字段名稱
Set<String> filedNames = MAPPING.get(c);
for (String fieldName : filedNames) {
Field f = c.getDeclaredField(fieldName);
log.info("處理映射字段:{}", f.getName());
// 獲取映射注解
MapTo mapTo = f.getAnnotation(MapTo.class);
log.info("映射配置:{}", mapTo);
// 設(shè)置私有字段訪問權(quán)限
f.setAccessible(true);
// 執(zhí)行SQL
String sql = mapTo.sql();
// 處理SQL變量
List<String> res = ReUtil.findAll("\$\{(.*?)}", sql, 0);
log.info("SQL變量:{}", res);
for (String re : res) {
Field ff = obj.getClass().getDeclaredField(re.substring(2, re.length()-1));
ff.setAccessible(true);
Object o = ff.get(obj);
sql = sql.replace(re, o.toString());
}
log.info("最終SQL:{}", sql);
List<Map<String, Object>> results = dualMapper.executeSql(sql);
Object v = null;
if (Collection.class.isAssignableFrom(f.getType())) {
// 集合對象
if (results.size() > 0) {
v = results.stream()
.map(r -> mapToBean(r, mapTo.targetClass()))
.collect(Collectors.toList());
}
} else {
// 單個對象
if (results.size() > 1) {
log.error("預(yù)計(jì)返回一條數(shù)據(jù),實(shí)際返回多條數(shù)據(jù)。執(zhí)行SQL: {}", sql);
} else if (results.size() == 1) {
// 轉(zhuǎn)換結(jié)果,賦值
v = mapToBean(results.get(0), mapTo.targetClass());
}
}
if (v != null && mapTo.doDeep()) {
doMapping(mapTo.targetClass(), v);
}
f.set(obj, v);
}
}
}
/**
* Map對象轉(zhuǎn)Bean
* @param map map
* @param clazz bean
* @return bean
*/
private Object mapToBean(Map<?, ?> map, Class<?> clazz) {
try {
return BeanUtil.fillBeanWithMap(map, clazz.newInstance(), true);
} catch (InstantiationException | IllegalAccessException e) {
log.error("實(shí)例化異常", e);
return null;
}
}
}三、使用方法
測試類
SysUser
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.List;
import java.util.StringJoiner;
/**
* @author victor
* @desc 系統(tǒng)用戶
* @date 2022/5/17
*/
public class SysUser implements Serializable {
private static final long serialVersionUID = 4855472141572371097L;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 登錄用戶名
*/
private String username;
/**
* 登錄密碼
*/
private String password;
/**
* 昵稱
*/
private String nickName;
@MapTo(targetClass = SysRole.class
, doDeep = true
, sql = "SELECT * FROM sys_role WHERE user_id=${id}")
@TableField(exist = false)
private SysRole sysRole;
@MapTo(targetClass = SysRole.class
, doDeep = true
, sql = "SELECT * FROM sys_role WHERE user_id=${id}")
@TableField(exist = false)
private List<SysRole> roleList;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public SysRole getSysRole() {
return sysRole;
}
public void setSysRole(SysRole sysRole) {
this.sysRole = sysRole;
}
public List<SysRole> getRoleList() {
return roleList;
}
public void setRoleList(List<SysRole> roleList) {
this.roleList = roleList;
}
@Override
public String toString() {
return new StringJoiner(", ", SysUser.class.getSimpleName() + "[", "]")
.add("id=" + id)
.add("username='" + username + "'")
.add("password='" + password + "'")
.add("nickName='" + nickName + "'")
.add("sysRole=" + sysRole)
.add("roleList=" + roleList)
.toString();
}
}SysRole
package sushengbuyu.maptodemo.sys.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import sushengbuyu.maptodemo.aop.MapTo;
import java.util.List;
import java.util.StringJoiner;
/**
* @author victor
* @desc 說明
* @date 2022/5/23
*/
public class SysRole {
@TableId
private Long id;
private Long userId;
private String name;
@MapTo(targetClass = SysPermission.class
, sql = "SELECT p.* FROM sys_permission p " +
"LEFT JOIN sys_role_permission rp ON p.id = rp.perm_id " +
"WHERE rp.role_id = ${id}")
@TableField(exist = false)
private List<SysPermission> permissionList;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public List<SysPermission> getPermissionList() {
return permissionList;
}
public void setPermissionList(List<SysPermission> permissionList) {
this.permissionList = permissionList;
}
@Override
public String toString() {
return new StringJoiner(", ", SysRole.class.getSimpleName() + "[", "]")
.add("id=" + id)
.add("userId=" + userId)
.add("name='" + name + "'")
.toString();
}
}SysPermission
package sushengbuyu.maptodemo.sys.po;
import java.util.StringJoiner;
/**
* @author victor
* @desc 說明
* @date 2022/5/25
*/
public class SysPermission {
private Long id;
private String name;
private Integer type;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
@Override
public String toString() {
return new StringJoiner(", ", SysPermission.class.getSimpleName() + "[", "]")
.add("id=" + id)
.add("name='" + name + "'")
.add("type=" + type)
.toString();
}
}SysUserService
測試用例就常見的三種, 查單個,查列表,查分頁
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.util.List;
/**
* @author victor
* @desc 說明
* @date 2022/5/17
*/
@Service
public class SysUserService extends ServiceImpl<SysUserMapper, SysUser> {
@DoMap(targetClass = SysUser.class)
@Override
public SysUser getById(Serializable id) {
return super.getById(id);
}
@DoMap(targetClass = SysUser.class)
public List<SysUser> listAll() {
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
return baseMapper.selectList(wrapper);
}
/**
* 從Page中取records作為處理對象
*/
@DoMap(targetClass = SysUser.class, spel = "records")
public Page<SysUser> page() {
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
Page<SysUser> p = new Page<>(1, 10);
return baseMapper.selectPage(p, wrapper);
}
}DoMapTests
import cn.hutool.json.JSONUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
//@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DoMapTests {
@Autowired
private SysUserService service;
@Test
void single() {
System.out.println(JSONUtil.toJsonPrettyStr(service.getById(1)));
}
@Test
void list() {
System.out.println(JSONUtil.toJsonPrettyStr(service.listAll()));
}
@Test
void page() {
System.out.println(JSONUtil.toJsonPrettyStr(service.page()));
}
}測試數(shù)據(jù)




測試結(jié)果
single
{
"nickName": "aa11",
"roleList": [
{
"permissionList": [
{
"type": 0,
"name": "add",
"id": 1
},
{
"type": 0,
"name": "query",
"id": 2
}
],
"userId": 1,
"name": "r1",
"id": 11
},
{
"permissionList": [
{
"type": 0,
"name": "del",
"id": 3
}
],
"userId": 1,
"name": "r2",
"id": 12
}
],
"password": "123456",
"id": 1,
"username": "a1"
}list
[
{
"nickName": "aa11",
"roleList": [
{
"permissionList": [
{
"type": 0,
"name": "add",
"id": 1
},
{
"type": 0,
"name": "query",
"id": 2
}
],
"userId": 1,
"name": "r1",
"id": 11
},
{
"permissionList": [
{
"type": 0,
"name": "del",
"id": 3
}
],
"userId": 1,
"name": "r2",
"id": 12
}
],
"password": "123456",
"id": 1,
"username": "a1"
},
{
"sysRole": {
"userId": 2,
"name": "r3",
"id": 13
},
"nickName": "aa22",
"roleList": [
{
"userId": 2,
"name": "r3",
"id": 13
}
],
"password": "123456",
"id": 2,
"username": "a2"
}
]page
{
"optimizeCountSql": true,
"records": [
{
"nickName": "aa11",
"roleList": [
{
"permissionList": [
{
"type": 0,
"name": "add",
"id": 1
},
{
"type": 0,
"name": "query",
"id": 2
}
],
"userId": 1,
"name": "r1",
"id": 11
},
{
"permissionList": [
{
"type": 0,
"name": "del",
"id": 3
}
],
"userId": 1,
"name": "r2",
"id": 12
}
],
"password": "123456",
"id": 1,
"username": "a1"
},
{
"sysRole": {
"userId": 2,
"name": "r3",
"id": 13
},
"nickName": "aa22",
"roleList": [
{
"userId": 2,
"name": "r3",
"id": 13
}
],
"password": "123456",
"id": 2,
"username": "a2"
}
],
"searchCount": true,
"total": 0,
"current": 1,
"size": 10,
"orders": [
]
}到此這篇關(guān)于使用AOP+反射實(shí)現(xiàn)自定義Mybatis多表關(guān)聯(lián)的文章就介紹到這了,更多相關(guān)Mybatis多表關(guān)聯(lián)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot項(xiàng)目打包成jar包的圖文教程
有時候我們會用IDEA來開發(fā)一些小工具,需要打成可運(yùn)行的JAR包,這篇文章主要給大家介紹了關(guān)于springboot項(xiàng)目打包成jar包的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06
Mybatis-Plus的多數(shù)據(jù)源你了解嗎
這篇文章主要為大家詳細(xì)介紹了Mybatis-Plus的多數(shù)據(jù)源,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03
Spring?Boot統(tǒng)一接口返回及全局異常處理
這篇文章主要介紹了Spring?Boot統(tǒng)一接口返回及全局異常處理,文章圍繞主題展開相關(guān)資料,具有一定的參考價值需要的小伙伴可以參考一下2022-04-04
java 實(shí)現(xiàn)MD5加密算法的簡單實(shí)例
這篇文章主要介紹了java 實(shí)現(xiàn)MD5加密算法的簡單實(shí)例的相關(guān)資料,這里提供實(shí)例幫助大家應(yīng)用這樣的加密算法,需要的朋友可以參考下2017-09-09
Jmeter參數(shù)化實(shí)現(xiàn)原理及過程解析
這篇文章主要介紹了Jmeter參數(shù)化實(shí)現(xiàn)原理及過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-07-07
SpringBoot+Jpa項(xiàng)目配置雙數(shù)據(jù)源的實(shí)現(xiàn)
本文主要介紹了SpringBoot+Jpa項(xiàng)目配置雙數(shù)據(jù)庫源的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12
Java基于API接口爬取商品數(shù)據(jù)的示例代碼
Java作為一種流行的編程語言,可以用于編寫程序來調(diào)用這些API接口,從而獲取商品數(shù)據(jù),本文將介紹如何使用Java基于API接口爬取商品數(shù)據(jù),包括請求API、解析JSON數(shù)據(jù)、存儲數(shù)據(jù)等步驟,并提供相應(yīng)的代碼示例,感興趣的朋友跟隨小編一起看看吧2023-10-10

