使用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),但是不想再引入另一個(gè)ORM框架。
目前的需求是增強(qiáng)現(xiàn)有的查詢,使用簡單的注解即可實(shí)現(xiàn)多表關(guān)聯(lián)。
二、核心代碼
GitHub:https://github.com/sushengbuyu/mybatis-mapping-demo
實(shí)現(xiàn)該功能總共需要四個(gè)文件
兩個(gè)自定義注解,一個(gè)虛擬Mapper,一個(gè)切面處理類
源碼
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時(shí),如果映射的對象類中有映射字段,也執(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ì){}個(gè)映射類,{}個(gè)映射字段", 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 { // 單個(gè)對象 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 { // 單個(gè)對象 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
測試用例就常見的三種, 查單個(gè),查列表,查分頁
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包的圖文教程
有時(shí)候我們會用IDEA來開發(fā)一些小工具,需要打成可運(yùn)行的JAR包,這篇文章主要給大家介紹了關(guān)于springboot項(xiàng)目打包成jar包的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06Mybatis-Plus的多數(shù)據(jù)源你了解嗎
這篇文章主要為大家詳細(xì)介紹了Mybatis-Plus的多數(shù)據(jù)源,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03Spring?Boot統(tǒng)一接口返回及全局異常處理
這篇文章主要介紹了Spring?Boot統(tǒng)一接口返回及全局異常處理,文章圍繞主題展開相關(guān)資料,具有一定的參考價(jià)值需要的小伙伴可以參考一下2022-04-04java 實(shí)現(xiàn)MD5加密算法的簡單實(shí)例
這篇文章主要介紹了java 實(shí)現(xiàn)MD5加密算法的簡單實(shí)例的相關(guān)資料,這里提供實(shí)例幫助大家應(yīng)用這樣的加密算法,需要的朋友可以參考下2017-09-09Jmeter參數(shù)化實(shí)現(xiàn)原理及過程解析
這篇文章主要介紹了Jmeter參數(shù)化實(shí)現(xiàn)原理及過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07SpringBoot+Jpa項(xiàng)目配置雙數(shù)據(jù)源的實(shí)現(xiàn)
本文主要介紹了SpringBoot+Jpa項(xiàng)目配置雙數(shù)據(jù)庫源的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12Java基于API接口爬取商品數(shù)據(jù)的示例代碼
Java作為一種流行的編程語言,可以用于編寫程序來調(diào)用這些API接口,從而獲取商品數(shù)據(jù),本文將介紹如何使用Java基于API接口爬取商品數(shù)據(jù),包括請求API、解析JSON數(shù)據(jù)、存儲數(shù)據(jù)等步驟,并提供相應(yīng)的代碼示例,感興趣的朋友跟隨小編一起看看吧2023-10-10