Mybatis 征途:我的福利方案产品创建接口,如何搞定多重校验与关联插入?

我们再次切换到 Mybatis 阵营,来详细剖析和记录 createSolutionProduct 这个复杂创建接口的实现过程。

使用 Mybatis 实现这个接口,意味着我们将拥有对 SQL 的完全控制权,尤其是在处理多对多关联的插入时,可以有更灵活的选择。


🚀 Mybatis 征途:我的产品创建接口,如何搞定多重校验与关联插入?

你好,我是坚持哥!在后端开发中,一个“创建”接口的复杂性,往往不亚于一个复杂的查询接口。它需要处理权限校验、数据关联、唯一性检查、事务管理等一系列挑战。

今天,我们将用 Mybatis 这位“SQL 工程师”,来从零到一构建福利小程序项目中的创建福利方案产品 (createSolutionProduct) 接口。我们将深入探讨如何通过手写 SQL 和 Mybatis 的强大功能,来优雅地处理多重校验和复杂的数据关联插入。准备好了吗?让我们开始这场 Mybatis 的实战之旅!

📝 接口功能速览 (Mybatis 版)

接口名称POST /api/solution/product/create
核心功能为指定管理员的福利方案,引入一个新的产品。
数据隔离 (Multi-tenancy)手动 SQL 实现:在校验关联数据时,WHERE 子句中显式加入 admin_id 过滤。
数据关联多重关联:一个创建动作,同时关联了 Admin, SolutionBrand, Product (主数据), 以及多个 SolutionProductCategory
唯一性校验手动 SQL 实现:通过 SELECT COUNT(*) 查询,检查同一 SolutionBrand 下是否已引入该 Product
事务管理@Transactional:确保整个创建过程的原子性,包括对 solution_productsolution_product_category_relation 两张表的插入。
健壮性✅ 完善的参数校验 (JSR 303)、业务异常处理 (IllegalArgumentException) 和数据转换 (Payload -> Entity -> VO)。
技术栈Spring Boot, Mybatis, Mybatis-Spring-Boot-Starter, MySQL, Lombok, Swagger。

🛠️ 核心组件与技术栈 (Mybatis 特色)

  1. SolutionProductController:API (Application Programming Interface) 入口,与 JPA 版相同。
  2. SolutionProductService:业务逻辑核心,负责编排整个创建流程,但底层调用 Mybatis Mapper。
  3. SolutionProductMapperMybatis 核心组件。一个 Java 接口,负责定义与 SolutionProduct 相关的数据库操作方法。
  4. SolutionProductMapper.xml手写 SQL 的地方。包含 INSERTsolution_product 表和 solution_product_category_relation 表的 SQL 语句。
  5. SolutionBrandMapper & SolutionProductCategoryMapper:用于在创建前,校验关联的品牌和分类是否存在且属于当前管理员。
  6. SolutionProductCreatePayload:DTO (Data Transfer Object - 数据传输对象),与 JPA 版相同。
  7. SolutionProductVO:视图对象 (View Object),与 JPA 版相同。

🗺️ 工作原理:一个产品创建请求的 Mybatis 之旅

当管理员在后台点击“添加产品”并提交表单时,一个创建请求是如何在 Mybatis 的世界里流转并最终入库的呢?

Service 内部处理流程
`sessionAdminId` 为空 ❌
`sessionAdminId` 存在 ✅
1. 校验 `adminId` 有效性
Service 处理业务逻辑 ⚙️
(SolutionProductService.createSolutionProduct)
2. 调用 Mapper 校验 `SolutionBrand` 归属
3. 调用 Mapper 校验 `Product` (主数据) 存在
4. 调用 Mapper 校验唯一性
5. 调用 Mapper 校验 `SolutionProductCategory` 归属
6. 构建 `SolutionProduct` 实体
7. 调用 Mapper 执行保存操作 💾
(productMapper.insertProduct)
(productMapper.batchInsertCategoryRelations)
前端发起请求 🌐
POST /api/solution/product/create
(带 Payload 数据)
Controller 接收请求 🎯
(SolutionProductController)
获取 `sessionAdminId`
(从 HTTP Session 中)
返回 `NOLOGIN` 错误 🚫
Mybatis 解析 Mapper XML 📄
执行 `INSERT` SQL 语句
数据库返回新创建的实体 ID 📦
Service 转换实体为 VO 🔄
Controller 返回 `BaseResult.success` 响应 🎉
前端展示成功提示 📱

🤝 交互时序:多重校验与事务原子性

createSolutionProduct 方法的 @Transactional 注解依然至关重要,它确保了对 solution_product 表和 solution_product_category_relation 表的插入操作是原子性的。

ControllerServiceMappers数据库FrontendcreateSolutionProduct(adminId, payload)1. **多重校验**- selectBrandByIdAndAdminId- selectProductById- countProductByBrandAndProduct- countCategoriesByIdsAndAdminId返回校验结果如果任何一步校验失败,抛出 IllegalArgumentException,事务回滚,操作终止。2. 构建 SolutionProduct 实体3. **数据插入**a. `productMapper.insertProduct(entity)`b. `productMapper.batchInsertCategoryRelations(...)`a. INSERT INTO solution_product ...b. INSERT INTO solution_product_category_relation ... (批量)所有操作在一个事务中执行返回持久化的实体 ID返回 savedProduct转换为 VO 并返回返回成功响应ControllerServiceMappers数据库Frontend

🌳 实体关系与所有权:POJO 的纯粹

在 Mybatis 中,实体类是纯粹的 POJO (Plain Old Java Object),不依赖 JPA 注解。admin_id 字段依然存在于需要隔离的实体中,作为数据隔离的物理基础。

"拥有"
1
0..*
"创建"
1
0..*
"包含"
1
0..*
"被引入为"
1
0..*
"关联"
0..*
0..*
Admin
+int id
SolutionBrand
+int id
+Admin admin
Product
+int id
SolutionProductCategory
+int id
+Admin admin
SolutionProduct
+int id
+SolutionBrand solutionBrand
+Product product
+Set<SolutionProductCategory> categories
ER 图 (Entity Relationship Diagram):数据库层面的关系

数据库结构与 JPA 版完全一致。

ADMINintidPKSOLUTION_BRANDintidPKintadmin_idFKPRODUCTintidPKSOLUTION_PRODUCT_CATEGORYintidPKintadmin_idFKSOLUTION_PRODUCTintidPKintsolution_brand_idFKintproduct_idFKSOLUTION_PRODUCT_CATEGORY_RELATIONintsolution_product_idFKintcategory_idFK拥有创建包含被引入为关联到被关联

💻 Mybatis 核心代码实现

1. SolutionProductMapper.java (Mybatis Mapper 接口)
package com.productQualification.suitselection.mapper;

import com.productQualification.suitselection.domain.SolutionProduct;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Set;

@Mapper
public interface SolutionProductMapper {

    /**
     * 插入新的福利方案产品。
     * @param product 产品实体
     * @return 影响的行数
     */
    int insertProduct(SolutionProduct product);

    /**
     * 批量插入产品与分类的关联关系。
     * @param productId 产品ID
     * @param categoryIds 分类ID集合
     * @return 影响的行数
     */
    int batchInsertCategoryRelations(@Param("productId") Integer productId, @Param("categoryIds") Set<Integer> categoryIds);

    /**
     * 根据小程序品牌ID和主数据产品ID,统计已存在的记录数量。
     * 用于唯一性校验。
     * @param solutionBrandId 小程序品牌ID
     * @param productId 主数据产品ID
     * @return 记录数量
     */
    long countBySolutionBrandIdAndProductId(@Param("solutionBrandId") Integer solutionBrandId, @Param("productId") Integer productId);

    // ... 其他查询和更新方法 ...
}
2. SolutionProductMapper.xml (Mybatis SQL 映射文件)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.productQualification.suitselection.mapper.SolutionProductMapper">

    <!-- 插入产品 -->
    <insert id="insertProduct" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO solution_product (
            created_date, last_modified_date, solution_brand_id, product_id,
            app_ranks, app_max_stock, app_min_order_quantity,
            app_comparable_price, app_group_buy_price, app_introduction,
            app_display_status, app_expiration_date
        ) VALUES (
            NOW(), NOW(), #{solutionBrand.id}, #{product.id},
            #{appRanks}, #{appMaxStock}, #{appMinOrderQuantity},
            #{appComparablePrice}, #{appGroupBuyPrice}, #{appIntroduction},
            #{appDisplayStatus}, #{appExpirationDate}
        )
    </insert>

    <!-- 批量插入产品与分类的关联关系 -->
    <insert id="batchInsertCategoryRelations">
        INSERT INTO solution_product_category_relation (solution_product_id, category_id)
        VALUES
        <foreach collection="categoryIds" item="categoryId" separator=",">
            (#{productId}, #{categoryId})
        </foreach>
    </insert>

    <!-- 唯一性校验查询 -->
    <select id="countBySolutionBrandIdAndProductId" resultType="long">
        SELECT COUNT(id) FROM solution_product
        WHERE solution_brand_id = #{solutionBrandId} AND product_id = #{productId}
    </select>

</mapper>
3. SolutionProductService.java (Service 层)
package com.productQualification.suitselection.service;

import com.productQualification.resource.domain.Product;
import com.productQualification.resource.mapper.ProductMapper; // 引入 PC 端 Product Mapper
import com.productQualification.suitselection.domain.SolutionBrand;
import com.productQualification.suitselection.domain.SolutionProduct;
import com.productQualification.suitselection.mapper.SolutionBrandMapper; // 引入 SolutionBrand Mapper
import com.productQualification.suitselection.mapper.SolutionProductCategoryMapper; // 引入产品分类Mapper
import com.productQualification.suitselection.mapper.SolutionProductMapper; // 引入产品Mapper
import com.productQualification.suitselection.payload.SolutionProductCreatePayload;
import com.productQualification.suitselection.vo.SolutionProductVO;
import com.productQualification.user.domain.Admin;
import com.productQualification.user.service.AdminCacheService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 产品查询与管理服务 (Mybatis 实现)。
 */
@Service
public class SolutionProductService {

    @Autowired
    private SolutionProductMapper productMapper;
    @Autowired
    private SolutionBrandMapper solutionBrandMapper;
    @Autowired
    private ProductMapper originalProductMapper;
    @Autowired
    private SolutionProductCategoryMapper productCategoryMapper;
    @Autowired
    private AdminCacheService adminCacheService;

    @Transactional
    public SolutionProductVO createSolutionProduct(Integer adminId, SolutionProductCreatePayload payload) {
        // 1. 校验并获取 Admin 对象
        Admin currentAdmin = adminCacheService.findById(adminId)
                .orElseThrow(() -> new RuntimeException("无效的管理员ID: " + adminId));

        // 2. 校验关联的 SolutionBrand 是否存在且属于当前管理员
        SolutionBrand solutionBrand = solutionBrandMapper.selectByIdAndAdminId(payload.getSolutionBrandId(), currentAdmin.getId())
                .orElseThrow(() -> new IllegalArgumentException("关联的小程序品牌不存在或不属于您的管理范围"));

        // 3. 校验关联的 Product (PC 端主数据产品) 是否存在
        Product originalProduct = originalProductMapper.selectById(payload.getProductId())
                .orElseThrow(() -> new IllegalArgumentException("关联的主数据产品不存在"));

        // 4. 校验唯一性:确保同一个 SolutionBrand 下不能重复引入同一个 Product
        if (productMapper.countBySolutionBrandIdAndProductId(solutionBrand.getId(), originalProduct.getId()) > 0) {
            throw new IllegalArgumentException("该品牌下已引入此产品,请勿重复添加");
        }

        // 5. 校验关联的产品分类 (SolutionProductCategory) 是否存在且属于当前管理员
        if (payload.getCategoryIds() != null && !payload.getCategoryIds().isEmpty()) {
            long validCategoryCount = productCategoryMapper.countByIdsAndAdminId(payload.getCategoryIds(), currentAdmin.getId());
            if (validCategoryCount != payload.getCategoryIds().size()) {
                throw new IllegalArgumentException("提交的产品分类ID中包含不存在或不属于您的分类");
            }
        }

        // 6. 构建 SolutionProduct 实体
        SolutionProduct newSolutionProduct = new SolutionProduct();
        BeanUtils.copyProperties(payload, newSolutionProduct);
        newSolutionProduct.setSolutionBrand(solutionBrand);
        newSolutionProduct.setProduct(originalProduct);

        // 7. 保存主实体
        productMapper.insertProduct(newSolutionProduct); // Mybatis 会将自增ID回填到 newSolutionProduct 对象中

        // 8. 保存多对多关联关系
        if (payload.getCategoryIds() != null && !payload.getCategoryIds().isEmpty()) {
            productMapper.batchInsertCategoryRelations(newSolutionProduct.getId(), payload.getCategoryIds());
        }

        // 9. 转换成 VO 并返回 (需要手动查询一次以获取完整数据)
        // 或者直接用 newSolutionProduct 组装 VO
        return convertToVO(newSolutionProduct, originalProduct, solutionBrand);
    }

    // ... 其他方法 ...
}

🧠 总结思维导图

在这里插入图片描述


这个 Mybatis 版本的实现,充分展示了其对 SQL 的强大控制力。虽然需要手动处理更多的细节(如分步插入),但也因此获得了更高的灵活性和性能潜力。希望这篇博客能帮助你更好地掌握 Mybatis 在复杂业务场景下的应用!Happy coding! 🎉

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值