如何避免双向关联下Domain与DAO对象映射的循环调用问题?
问题:双向关联对象映射时的循环调用问题
项目结构规则:
- Domain对象负责执行业务逻辑
- DAO对象仅用于数据库读写
- 从数据库读取DAO后,由Mapper转换为Domain对象;Domain转DAO也通过Mapper完成
Domain对象示例
public class User { UserId userId; String name; ShoppingCart shoppingCart; ... } public class ShoppingCart { ShoppingCartId shoppingCartId; float price; User user; ... }
DAO对象示例
public class UserDAO { Long id; String name; ShoppingCartDAO shoppingCart; ... } public class ShoppingCartDAO { Long id; float price; UserDAO user; ... }
循环调用问题场景
当存在双向关联(比如User和ShoppingCart是@OneToOne双向关联)时,Mapper的全对象映射会触发无限循环:
UserDAOMapper.toDAO(User)会调用ShoppingCartDAOMapper.toDAO(ShoppingCart)- 而
ShoppingCartDAOMapper.toDAO(ShoppingCart)又会反过来调用UserDAOMapper.toDAO(User),最终导致栈溢出
现有Mapper示例:
@NoArgsConstructor(access = AccessLevel.PRIVATE) public class UserDAOMapper { public static UserDAO toDAO(User user) { return UserDAO.builder() .id(user.getUserId().getId()) .name(user.getName()) .shoppingCart(ShoppingCartDAOMapper.toDAO(user.getShoppingCart())) .build(); } }
解决方案:保持代码简洁的前提下避免循环调用
方案1:拆分基础映射与关联映射
为每个Mapper提供两类方法,手动控制关联映射的深度:
- 基础映射:只转换当前对象的非关联字段,不处理关联对象
- 全量映射:在基础映射基础上,仅映射关联对象的基础信息,避免触发反向关联
示例实现:
// UserDAOMapper @NoArgsConstructor(access = AccessLevel.PRIVATE) public class UserDAOMapper { // 基础映射:只处理User自身字段 private static UserDAO toBasicDAO(User user) { return UserDAO.builder() .id(user.getUserId().getId()) .name(user.getName()) .build(); } // 全量映射:仅映射ShoppingCart的基础信息,不触发其对User的反向映射 public static UserDAO toDAO(User user) { UserDAO userDAO = toBasicDAO(user); if (user.getShoppingCart() != null) { userDAO.setShoppingCart(ShoppingCartDAOMapper.toBasicDAO(user.getShoppingCart())); } return userDAO; } } // ShoppingCartDAOMapper @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ShoppingCartDAOMapper { private static ShoppingCartDAO toBasicDAO(ShoppingCart shoppingCart) { return ShoppingCartDAO.builder() .id(shoppingCart.getShoppingCartId().getId()) .price(shoppingCart.getPrice()) .build(); } public static ShoppingCartDAO toDAO(ShoppingCart shoppingCart) { ShoppingCartDAO cartDAO = toBasicDAO(shoppingCart); if (shoppingCart.getUser() != null) { cartDAO.setUser(UserDAOMapper.toBasicDAO(shoppingCart.getUser())); } return cartDAO; } }
方案2:基于上下文的重复映射拦截
如果需要保留完整的关联关系(比如序列化场景),可以通过上下文容器记录已映射的对象,避免重复转换:
// UserDAOMapper @NoArgsConstructor(access = AccessLevel.PRIVATE) public class UserDAOMapper { public static UserDAO toDAO(User user) { return toDAO(user, new IdentityHashMap<>()); } private static UserDAO toDAO(User user, Map<Object, Object> mappedObjects) { // 检查是否已映射过该对象,直接返回避免循环 if (mappedObjects.containsKey(user)) { return (UserDAO) mappedObjects.get(user); } UserDAO userDAO = UserDAO.builder() .id(user.getUserId().getId()) .name(user.getName()) .build(); mappedObjects.put(user, userDAO); // 映射关联对象时传入上下文 if (user.getShoppingCart() != null) { userDAO.setShoppingCart(ShoppingCartDAOMapper.toDAO(user.getShoppingCart(), mappedObjects)); } return userDAO; } } // ShoppingCartDAOMapper @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ShoppingCartDAOMapper { public static ShoppingCartDAO toDAO(ShoppingCart shoppingCart) { return toDAO(shoppingCart, new IdentityHashMap<>()); } private static ShoppingCartDAO toDAO(ShoppingCart shoppingCart, Map<Object, Object> mappedObjects) { if (mappedObjects.containsKey(shoppingCart)) { return (ShoppingCartDAO) mappedObjects.get(shoppingCart); } ShoppingCartDAO cartDAO = ShoppingCartDAO.builder() .id(shoppingCart.getShoppingCartId().getId()) .price(shoppingCart.getPrice()) .build(); mappedObjects.put(shoppingCart, cartDAO); if (shoppingCart.getUser() != null) { cartDAO.setUser(UserDAOMapper.toDAO(shoppingCart.getUser(), mappedObjects)); } return cartDAO; } }
方案3:使用成熟映射框架简化实现
如果不想手动编写Mapper逻辑,可以用MapStruct这类原生支持循环引用处理的框架,仅需简单配置即可解决问题:
// UserDAOMapper(MapStruct版) @Mapper public interface UserDAOMapper { UserDAOMapper INSTANCE = Mappers.getMapper(UserDAOMapper.class); @Mapping(target = "shoppingCart.user", ignore = true) // 忽略反向关联避免循环 UserDAO toDAO(User user); } // ShoppingCartDAOMapper(MapStruct版) @Mapper public interface ShoppingCartDAOMapper { ShoppingCartDAOMapper INSTANCE = Mappers.getMapper(ShoppingCartDAOMapper.class); @Mapping(target = "user.shoppingCart", ignore = true) // 忽略反向关联避免循环 ShoppingCartDAO toDAO(ShoppingCart shoppingCart); }
内容的提问来源于stack exchange,提问作者Adrian
相关产品推荐
相关产品推荐

