MyBatis 精确控制:“确保币种存在”接口的SQL智慧与幂等性实践 ✨

🛡️ MyBatis 精确控制:“确保币种存在”接口的SQL智慧与幂等性实践 🪙✨

Hello,各位热衷于数据持久化探索的开发者们!👋 在构建健壮的系统时,确保基础数据的存在性和一致性是一个常见的需求。我们之前探讨了如何使用 Spring Data JPA (Jakarta Persistence API, Jakarta 持久化应用程序接口) 实现一个幂等的 ensureCurrency 接口。今天,我们将把目光转向 MyBatis,看看如何利用其对 SQL (Structured Query Language, 结构化查询语言) 的精细控制能力,来实现同样的功能:接收币种代码,检查数据库,若币种已存在则返回,若不存在则根据后端预定义的标准信息自动创建。 这次,我们将深入 MyBatis 的 Mapper XML (Extensible Markup Language, 可扩展标记语言) 和接口定义,体验用 SQL (Structured Query Language, 结构化查询语言) 的方式来保障数据操作的准确性和幂等性。准备好迎接一场 MyBatis 的数据保障之旅了吗?让我们开始吧!🚀

📖 ensureCurrency 接口核心特性与技术概览 (MyBatis 版)

特性/方面描述关键技术/模式 (MyBatis 版)
🎯 核心功能接收币种代码 (code),检查数据库中是否存在。若存在,返回现有币种信息;若不存在,根据后端预定义的标准数据创建新币种并返回。MyBatis Mapper 接口与 XML (Extensible Markup Language, 可扩展标记语言) 映射文件,手动编写 SQL (Structured Query Language, 结构化查询语言) SELECT, INSERT。DTO (Data Transfer Object, 数据传输对象) 模式。
幂等性多次使用相同的币种代码调用此接口,效果与调用一次相同(即不会重复创建币种)。Service 层先查询后创建的逻辑,通过 MyBatis Mapper 执行。
⚙️ 数据源对于不存在的币种,其详细信息(名称、符号、汇率等)从后端预定义的数据源(如硬编码的 Map、配置文件或初始化数据)获取。工具类/常量类 (CurrencyData) 存储预定义币种信息。
🛡️ 数据准确性新创建的币种信息基于后端标准数据,保证了数据的一致性和准确性。后端控制币种核心属性的创建。
API (Application Programming Interface, 应用程序编程接口) 交互前端通过 POST /api/v1/admin/currencies/ensure 发送包含币种代码的 EnsureCurrencyPayload DTO (Data Transfer Object, 数据传输对象)。后端返回包含 CurrencyDtoBaseResult@RequestBody, @Valid (DTO (Data Transfer Object, 数据传输对象) 校验), DTO (Data Transfer Object, 数据传输对象) 转换。
⏱️ 时间戳管理使用 MyBatis 时,实体的 createdDatelastModifiedDate 字段需要手动在 Service 层或 SQL (Structured Query Language, 结构化查询语言) 中设置new Date() 设置时间,并在 INSERTUPDATE (如果适用) 语句中包含这些字段。
🧩 技术栈核心Java (一种面向对象的编程语言), Spring Boot, MyBatis, MySQL (一种关系型数据库管理系统) (或其它), Lombok (一个Java库,可以通过简单的注解形式来帮助消除样板式代码)。

🪙 场景设定回顾 (与 JPA (Jakarta Persistence API, Jakarta 持久化应用程序接口) 版一致)

ensureCurrency 接口的核心目标是提供一个规范的入口,确保业务流程中使用的币种在系统中是存在的、标准的。

🛠️ MyBatis 实现 ensureCurrency 接口的关键步骤

1. 前端“信使”:EnsureCurrencyPayload.java (DTO (Data Transfer Object, 数据传输对象) - 保持不变)

前端依然只传递核心的币种代码。

// EnsureCurrencyPayload.java
@Data
public class EnsureCurrencyPayload {
    @NotBlank @Pattern(regexp = "^[A-Z]{3}$")
    private String code;
}

2. 后端的“知识库”:预定义币种信息 (CurrencyData.java - 保持不变)

这个工具类或常量类,用于存储标准币种的完整信息,在 MyBatis 版本中同样重要。

// CurrencyData.java (内容同上一篇博客)
public class CurrencyData {
    // ... PREDEFINED_CURRENCIES Map 和 getInfo() 方法 ...
    // PredefinedCurrencyInfo 内部类
}

3. 定义 MyBatis Mapper XML (Extensible Markup Language, 可扩展标记语言): CurrencyMapper.xml

这里是 SQL (Structured Query Language, 结构化查询语言) 的主战场。

<?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.coupon.mapper.CurrencyMapper"> <!-- 替换为你的 Mapper 接口路径 -->

    <resultMap id="CurrencyResultMap" type="com.productQualification.coupon.domain.Currency">
        <id property="id" column="id"/>
        <result property="code" column="code"/>
        <result property="name" column="name"/>
        <result property="symbol" column="symbol"/>
        <result property="exchangeRate" column="exchange_rate"/>
        <result property="isDefault" column="is_default"/>
        <result property="status" column="status"/>
        <result property="createdDate" column="created_date"/>
        <result property="lastModifiedDate" column="last_modified_date"/>
    </resultMap>

    <select id="findByCode" resultMap="CurrencyResultMap">
        SELECT * FROM currency WHERE code = #{code}
    </select>

    <select id="findDefaultCurrency" resultMap="CurrencyResultMap">
        SELECT * FROM currency WHERE is_default = 1 LIMIT 1
    </select>

    <insert id="insertCurrency" parameterType="com.productQualification.coupon.domain.Currency" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO currency (code, name, symbol, exchange_rate, is_default, status, created_date, last_modified_date)
        VALUES (#{code}, #{name}, #{symbol}, #{exchangeRate}, #{isDefault}, #{status}, #{createdDate}, #{lastModifiedDate})
    </insert>

    <update id="updateCurrencyDefaultStatus" parameterType="com.productQualification.coupon.domain.Currency">
        UPDATE currency
        SET is_default = #{isDefault},
            last_modified_date = #{lastModifiedDate}
        WHERE id = #{id}
    </update>

</mapper>

XML (Extensible Markup Language, 可扩展标记语言) 关键点

  • CurrencyResultMap: 将数据库查询结果映射到 Currency 实体。
  • findByCode: 根据币种代码查询。
  • findDefaultCurrency: 查找当前默认的币种。
  • insertCurrency: 插入新的币种记录,包含手动设置的时间戳字段。useGeneratedKeys="true" keyProperty="id" 用于获取自增主键。
  • updateCurrencyDefaultStatus: 用于更新币种的 isDefault 状态和 lastModifiedDate

4. 定义 MyBatis Mapper 接口: CurrencyMapper.java

package com.productQualification.coupon.mapper; // 替换为你的 Mapper 接口路径

import com.productQualification.coupon.domain.Currency;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Optional; // MyBatis 返回 Optional 可能需要额外配置或特定版本

@Mapper
public interface CurrencyMapper {
    Optional<Currency> findByCode(@Param("code") String code);
    Optional<Currency> findDefaultCurrency(); // 查找 is_default = 1 的记录
    int insertCurrency(Currency currency);
    int updateCurrencyDefaultStatus(Currency currency); // 用于取消旧的默认币种
}

5. Service 层核心逻辑:CurrencyService.ensureCurrencyExists (MyBatis 版)

Service 层现在使用 CurrencyMapper 来执行数据库操作。

流程图解 (ensureCurrencyExists - MyBatis 版)

是 (已存在)
否 (不存在)
开始 ensureCurrencyExists
Controller 接收
EnsureCurrencyPayload (含code)
Service: 将 code 转为大写
(MyBatis) 调用 Mapper.findByCode(codeUpper)
数据库中是否存在
该 code 的币种?
加载现有 Currency 实体
调用 convertToDto(existingCurrency)
返回 CurrencyDto 给 Controller
结束
记录日志: 币种未找到, 尝试创建
从 CurrencyData 工具类
获取预定义的币种信息
predefinedInfo = CurrencyData.getInfo(codeUpper)
预定义信息是否存在?
抛出异常
“无法识别的币种代码”
处理默认币种逻辑
predefinedInfo.isDefault == 1?
(MyBatis) 调用 Mapper.findDefaultCurrency()
找到当前默认币种
且与当前code不同?
currentDefault.setIsDefault((byte)0)
手动设置 lastModifiedDate
(MyBatis) 调用 Mapper.updateCurrencyDefaultStatus(currentDefault)
创建新的 Currency 实体
根据 predefinedInfo
设置新实体的属性
手动设置 createdDate
和 lastModifiedDate
(MyBatis) 调用 Mapper.insertCurrency(newCurrency)
获取保存后的 savedCurrency
(ID已由MyBatis回填)
调用 convertToDto(savedCurrency)

CurrencyService.java (MyBatis 版本):

package com.productQualification.coupon.service;

import com.productQualification.common.exception.MyRuntimeException;
import com.productQualification.coupon.domain.Currency;
import com.productQualification.coupon.dto.EnsureCurrencyPayload;
import com.productQualification.coupon.dto.CurrencyDto;
import com.productQualification.coupon.mapper.CurrencyMapper; // 引入 MyBatis Mapper
import com.productQualification.coupon.util.CurrencyData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // 事务注解

import java.util.Date;
import java.util.Optional;
// ... (其他需要的 import)

@Service
public class CurrencyService {

    private static final Logger logger = LoggerFactory.getLogger(CurrencyService.class);
    private final CurrencyMapper currencyMapper; // 使用 MyBatis Mapper

    @Autowired
    public CurrencyService(CurrencyMapper currencyMapper) {
        this.currencyMapper = currencyMapper;
    }

    private CurrencyDto convertToDto(Currency entity) {
        if (entity == null) return null;
        CurrencyDto dto = new CurrencyDto();
        dto.setId(entity.getId());
        dto.setCode(entity.getCode());
        dto.setName(entity.getName());
        dto.setSymbol(entity.getSymbol());
        dto.setExchangeRate(entity.getExchangeRate());
        dto.setIsDefault(entity.getIsDefault());
        dto.setStatus(entity.getStatus());
        dto.setCreatedDate(entity.getCreatedDate());
        dto.setLastModifiedDate(entity.getLastModifiedDate());
        return dto;
    }

    @Transactional // 事务管理依然重要
    public CurrencyDto ensureCurrencyExists(EnsureCurrencyPayload payload, Integer adminId) {
        String codeUpper = payload.getCode().toUpperCase();
        logger.info("Admin {} ensuring currency with code: {} exists (MyBatis).", adminId, codeUpper);

        Optional<Currency> existingCurrencyOpt = currencyMapper.findByCode(codeUpper);

        if (existingCurrencyOpt.isPresent()) {
            logger.info("Currency with code '{}' already exists with ID: {}.", codeUpper, existingCurrencyOpt.get().getId());
            return convertToDto(existingCurrencyOpt.get());
        } else {
            logger.info("Currency with code '{}' not found. Attempting to create from predefined data.", codeUpper);
            CurrencyData.PredefinedCurrencyInfo predefinedInfo = CurrencyData.getInfo(codeUpper);

            if (predefinedInfo == null) {
                throw new MyRuntimeException("无法识别的币种代码: " + codeUpper + "。系统中未预定义此币种。");
            }

            Date currentDate = new Date(); // 用于手动设置时间戳

            if (predefinedInfo.isDefault == 1) {
                currencyMapper.findDefaultCurrency().ifPresent(currentDefault -> {
                    if (!currentDefault.getCode().equals(codeUpper)) {
                        currentDefault.setIsDefault((byte) 0);
                        currentDefault.setLastModifiedDate(currentDate); // 手动设置
                        currencyMapper.updateCurrencyDefaultStatus(currentDefault);
                        logger.info("Unset previous default currency (Code: {}) before setting new default {}.", currentDefault.getCode(), codeUpper);
                    }
                });
            }

            Currency newCurrency = new Currency();
            newCurrency.setCode(codeUpper);
            newCurrency.setName(predefinedInfo.name);
            newCurrency.setSymbol(predefinedInfo.symbol);
            newCurrency.setExchangeRate(predefinedInfo.exchangeRate);
            newCurrency.setIsDefault(predefinedInfo.isDefault);
            newCurrency.setStatus(predefinedInfo.status);
            newCurrency.setCreatedDate(currentDate);     // 手动设置创建时间
            newCurrency.setLastModifiedDate(currentDate); // 手动设置最后修改时间

            currencyMapper.insertCurrency(newCurrency); // newCurrency.id() 会被填充 (如果配置了 keyProperty)
            logger.info("Currency '{}' (code: {}) created successfully with ID: {} by admin {}",
                    newCurrency.getName(), newCurrency.getCode(), newCurrency.getId(), adminId);
            return convertToDto(newCurrency);
        }
    }
    // ... 其他币种管理方法 (create, update, list) 也需要用 MyBatis Mapper 实现 ...
}

MyBatis 版本 Service 层改动要点:

  • 依赖注入: 注入 CurrencyMapper
  • 数据库操作: 调用 currencyMapper 的方法执行 SQL (Structured Query Language, 结构化查询语言)。
  • 时间戳管理: 至关重要! MyBatis 不会自动处理 @CreatedDate / @LastModifiedDate。你需要在 Service 层手动createdDatelastModifiedDate 字段设置值(例如 new Date()),并在 Mapper XML (Extensible Markup Language, 可扩展标记语言) 的 INSERTUPDATE 语句中包含这些字段。
  • 主键回填: 在 CurrencyMapper.xml<insert> 标签中,使用 useGeneratedKeys="true" keyProperty="id" 可以让 MyBatis 在插入数据后,将数据库生成的自增主键值回填到传入的 Currency 对象的 id 属性中。

6. Controller 层 (CurrencyController.java)

Controller 层无需任何改动,因为它与 Service 层的接口契约(方法签名和返回 CurrencyDto)在本次迁移中保持不变。

📊 交互时序图 (Sequence Diagram - ensureCurrency - MyBatis 版)

"前端""Controller""CurrencyService (MyBatis)""CurrencyMapper (MyBatis)""CurrencyData""数据库"POST /ensure (EnsureCurrencyPayload)ensureCurrencyExists(payload, adminId)findByCode(payload.code.toUpperCase())SELECT * FROM currency WHERE code = ?Optional<Currency>existingCurrencyOptconvertToDto(existingCurrency)CurrencyDtogetInfo(payload.code.toUpperCase())predefinedInfo抛出 MyRuntimeExceptionfindDefaultCurrency()Optional<Currency> (currentDefaultOpt)currentDefault.setIsDefault((byte)0);currentDefault.setLastModifiedDate(now)updateCurrencyDefaultStatus(currentDefault)(update result)alt[currentDefaultOpt is present AND not same code]alt[predefinedInfo.isDefault == 1]创建 new Currency 实体 (用 predefinedInfo)<b>手动设置 createdDate, lastModifiedDate</b>insertCurrency(newCurrency)INSERT INTO currency (...)(回填 newCurrency.id)(insert result)convertToDto(newCurrency)CurrencyDtoalt[predefinedInfo is null][predefinedInfo is found]alt[existingCurrencyOpt is present][currency not found in DB]BaseResult (含 CurrencyDto 或错误)"前端""Controller""CurrencyService (MyBatis)""CurrencyMapper (MyBatis)""CurrencyData""数据库"

🔄 状态图与类图 (概念上与之前博客类似)

特定币种代码在系统中的状态流转概念不变。类图的主要变化是 CurrencyService 现在依赖 CurrencyMapper 接口。

简化类图 (突出 MyBatis 依赖):

"consumes"
"uses"
"creates & returns"
"maps to/from DB"
EnsureCurrencyPayload
-String code
CurrencyDto
/* ... */
CurrencyController
+BaseResult ensureCurrency(EnsureCurrencyPayload, adminId)
CurrencyService
-CurrencyMapper currencyMapper
+CurrencyDto ensureCurrencyExists(EnsureCurrencyPayload, adminId)
« MyBatis Mapper Interface »
CurrencyMapper
+Optional~Currency~ findByCode(String code)
+Optional~Currency~ findDefaultCurrency()
+int insertCurrency(Currency currency)
+int updateCurrencyDefaultStatus(Currency currency)
Currency
%% Entity %%
«static utility»
CurrencyData

方法签名详细说明(简化版,具体类型参考代码):

  • CurrencyService.ensureCurrencyExists(...): 返回 CurrencyDto
  • CurrencyMapper 方法返回类型涉及 Optional<Currency>int

💡 英文缩写全称及中文解释

  • API: Application Programming Interface (应用程序编程接口)
  • CRUD: Create, Read, Update, Delete (增删改查)
  • DB: DataBase (数据库)
  • DDL: Data Definition Language (数据定义语言)
  • DTO: Data Transfer Object (数据传输对象)
  • HTTP: HyperText Transfer Protocol (超文本传输协议)
  • ID: Identifier (标识符)
  • ISO: International Organization for Standardization (国际标准化组织)
  • JPA: Jakarta Persistence API (formerly Java Persistence API) (Jakarta 持久化应用程序接口)
  • JSON: JavaScript Object Notation (JavaScript 对象表示法)
  • ORM: Object-Relational Mapping (对象关系映射)
  • POJO: Plain Old Java Object (简单Java对象)
  • SQL: Structured Query Language (结构化查询语言)
  • UML: Unified Modeling Language (统一建模语言)
  • XML: Extensible Markup Language (可扩展标记语言)

🧠 思维导图 (Markdown 格式)

在这里插入图片描述

🎉 总结:MyBatis 下的稳健与掌控!

通过 MyBatis 实现 ensureCurrency 接口,我们再次体验了其对 SQL (Structured Query Language, 结构化查询语言) 的精细控制能力。虽然相比 JPA (Jakarta Persistence API, Jakarta 持久化应用程序接口) 需要更多地关注 SQL (Structured Query Language, 结构化查询语言) 编写、结果映射以及像时间戳这样的细节管理,但这种投入换来的是对数据访问过程的完全透明和在需要时进行深度优化的能力。

对于“确保存在或创建”这类具有幂等性要求的操作,MyBatis 同样能够优雅地实现。Service 层先查询后决策的逻辑,结合 Mapper 中精确的 SQL (Structured Query Language, 结构化查询语言) 语句,共同保障了业务规则的正确执行和数据的标准化。

希望这篇基于 MyBatis 实现的 ensureCurrency 接口博客,能为你提供在不同持久层框架下解决同类问题的思路和实践参考!如果你对 MyBatis 在此类场景下的应用有更多技巧或经验,欢迎在评论区留言分享!👇 Happy SQL (Structured Query Language, 结构化查询语言) with MyBatis! 🐘💻

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值