MySQL逗号分隔字段-历史遗留原因兼容方案

MySQL逗号分隔字段的MyBatis操作指南

📋 背景说明

字段定义:

sql 复制代码
create your_table(
    supplier_id varchar(255) NULL  -- (多个ID逗号分隔)数据格式示例:1,5,3,8,12
);

现状分析:

  • 字段已在生产环境使用,不能修改表结构
  • 需要支持单个ID的增删改查操作
  • 需要保持数据格式的一致性

🔍 查询操作

1.1 精确查询(推荐使用)

xml 复制代码
<!-- 方法1:使用FIND_IN_SET函数 -->
<select id="selectBySupplierId" resultType="YourEntity">
    SELECT * FROM your_table 
    WHERE FIND_IN_SET(#{supplierId}, supplier_id) > 0
</select>

<!-- 方法2:使用LIKE(更安全,处理边界情况) -->
<select id="selectBySupplierIdSafe" resultType="YourEntity">
    SELECT * FROM your_table 
    WHERE CONCAT(',', supplier_id, ',') LIKE CONCAT('%,', #{supplierId}, ',%')
</select>

两种方法的对比:

方法 优点 缺点 适用场景
FIND_IN_SET 语法简洁,易理解 不能使用索引,性能较差 数据量小的表
LIKE 可部分利用索引,更安全 语法复杂 数据量较大的表

1.2 多条件查询

xml 复制代码
<!-- 查询同时包含多个supplier_id的记录 -->
<select id="selectBySupplierIds" resultType="YourEntity">
    SELECT * FROM your_table 
    WHERE 
    <foreach collection="supplierIds" item="id" separator=" AND ">
        FIND_IN_SET(#{id}, supplier_id) > 0
    </foreach>
</select>

<!-- 查询包含任意一个supplier_id的记录 -->
<select id="selectByAnySupplierId" resultType="YourEntity">
    SELECT * FROM your_table 
    WHERE 
    <foreach collection="supplierIds" item="id" separator=" OR ">
        FIND_IN_SET(#{id}, supplier_id) > 0
    </foreach>
</select>

✏️ 修改/更新操作

2.1 添加新的supplier_id

xml 复制代码
<!-- 安全添加,避免重复 -->
<update id="addSupplierId">
    UPDATE your_table 
    SET supplier_id = 
        CASE 
            WHEN supplier_id IS NULL OR supplier_id = '' THEN #{newSupplierId}
            WHEN FIND_IN_SET(#{newSupplierId}, supplier_id) = 0 
                THEN CONCAT(supplier_id, ',', #{newSupplierId})
            ELSE supplier_id
        END
    WHERE id = #{recordId}
</update>

2.2 移除特定的supplier_id

xml 复制代码
<!-- 安全移除,处理各种边界情况 -->
<update id="removeSupplierId">
    UPDATE your_table 
    SET supplier_id = TRIM(BOTH ',' FROM 
        REPLACE(
            REPLACE(CONCAT(',', supplier_id, ','), CONCAT(',', #{removeId}, ','), ','),
            ',,', ','
        )
    )
    WHERE FIND_IN_SET(#{removeId}, supplier_id) > 0
    AND id = #{recordId}
</update>

移除操作的详细处理逻辑:

  1. CONCAT(',', supplier_id, ',') - 在首尾添加逗号,统一格式
  2. REPLACE(CONCAT(...), CONCAT(',', #{removeId}, ','), ',') - 替换目标ID
  3. REPLACE(..., ',,', ',') - 处理可能产生的连续逗号
  4. TRIM(BOTH ',' FROM ...) - 移除首尾逗号

2.3 批量更新整个supplier_id列表

xml 复制代码
<update id="updateSupplierIds">
    UPDATE your_table 
    SET supplier_id = 
    <choose>
        <when test="supplierIds != null and supplierIds.size() > 0">
            <foreach collection="supplierIds" item="id" separator="," open="" close="">
                #{id}
            </foreach>
        </when>
        <otherwise>NULL</otherwise>
    </choose>
    WHERE id = #{recordId}
</update>

🗑️ 删除操作

3.1 删除包含特定supplier_id的记录

xml 复制代码
<delete id="deleteBySupplierId">
    DELETE FROM your_table 
    WHERE FIND_IN_SET(#{supplierId}, supplier_id) > 0
</delete>

💻 Java代码实现

4.1 Mapper接口定义

java 复制代码
public interface YourMapper {
    // 基础查询
    YourEntity selectById(@Param("id") Long id);
    
    // 根据supplier_id查询
    List<YourEntity> selectBySupplierId(@Param("supplierId") String supplierId);
    List<YourEntity> selectBySupplierIds(@Param("supplierIds") List<String> supplierIds);
    
    // 更新操作
    int addSupplierId(@Param("recordId") Long recordId, @Param("newSupplierId") String newSupplierId);
    int removeSupplierId(@Param("recordId") Long recordId, @Param("removeId") String removeId);
    int updateSupplierIds(@Param("recordId") Long recordId, @Param("supplierIds") List<String> supplierIds);
    
    // 删除操作
    int deleteBySupplierId(@Param("supplierId") String supplierId);
}

4.2 Service层实现

java 复制代码
@Service
@Transactional
public class SupplierService {
    
    @Autowired
    private YourMapper yourMapper;
    
    /**
     * 安全移除supplier_id
     */
    public boolean safeRemoveSupplierId(Long recordId, String removeId) {
        try {
            YourEntity entity = yourMapper.selectById(recordId);
            if (entity == null || entity.getSupplierId() == null) {
                return false;
            }
            
            // 使用Java逻辑处理,更安全
            List<String> ids = Arrays.stream(entity.getSupplierId().split(","))
                .filter(id -> !id.trim().isEmpty())
                .collect(Collectors.toList());
            
            boolean removed = ids.remove(removeId);
            if (!removed) {
                return false; // 要移除的ID不存在
            }
            
            if (ids.isEmpty()) {
                yourMapper.updateSupplierIds(recordId, null);
            } else {
                yourMapper.updateSupplierIds(recordId, ids);
            }
            
            return true;
        } catch (Exception e) {
            throw new RuntimeException("移除supplier_id失败", e);
        }
    }
    
    /**
     * 安全添加supplier_id
     */
    public boolean safeAddSupplierId(Long recordId, String newId) {
        YourEntity entity = yourMapper.selectById(recordId);
        if (entity == null) return false;
            
        if (entity.getSupplierId() == null || entity.getSupplierId().isEmpty()) {
            yourMapper.updateSupplierIds(recordId, Collections.singletonList(newId));
            return true;
        }
        
        List<String> ids = Arrays.stream(entity.getSupplierId().split(","))
            .filter(id -> !id.trim().isEmpty())
            .collect(Collectors.toList());
            
        if (ids.contains(newId)) {
            return false; // 已存在,不重复添加
        }
        
        ids.add(newId);
        yourMapper.updateSupplierIds(recordId, ids);
        return true;
    }
    
    /**
     * 获取所有的supplier_id列表
     */
    public List<String> getSupplierIds(Long recordId) {
        YourEntity entity = yourMapper.selectById(recordId);
        if (entity == null || entity.getSupplierId() == null) {
            return Collections.emptyList();
        }
        
        return Arrays.stream(entity.getSupplierId().split(","))
            .filter(id -> !id.trim().isEmpty())
            .collect(Collectors.toList());
    }
}

⚠️ 重要注意事项

5.1 性能考虑

问题:

  • FIND_IN_SET 无法使用索引,全表扫描
  • 数据量大时查询性能差

优化建议:

sql 复制代码
-- 可以考虑添加全文索引(如果MySQL版本支持)
ALTER TABLE your_table ADD FULLTEXT(supplier_id);

-- 查询时使用MATCH AGAINST(需要调整业务逻辑)
SELECT * FROM your_table 
WHERE MATCH(supplier_id) AGAINST('+1' IN BOOLEAN MODE);

5.2 数据一致性保障

java 复制代码
// 在Service层添加数据校验
private void validateSupplierId(String supplierId) {
    if (supplierId == null || supplierId.trim().isEmpty()) {
        throw new IllegalArgumentException("supplierId不能为空");
    }
    if (supplierId.contains(",")) {
        throw new IllegalArgumentException("单个supplierId不能包含逗号");
    }
}

// 在添加/移除时调用校验
public boolean safeAddSupplierId(Long recordId, String newId) {
    validateSupplierId(newId);
    // ... 其余逻辑
}

5.3 事务管理

java 复制代码
@Service
@Transactional(rollbackFor = Exception.class)
public class SupplierService {
    
    /**
     * 批量操作,保证原子性
     */
    @Transactional
    public void batchUpdateSupplierIds(Long recordId, List<String> toAdd, List<String> toRemove) {
        for (String removeId : toRemove) {
            safeRemoveSupplierId(recordId, removeId);
        }
        for (String addId : toAdd) {
            safeAddSupplierId(recordId, addId);
        }
    }
}

🚀 长期优化建议

6.1 推荐方案:建立关联表

sql 复制代码
-- 创建关联表(推荐方案)
CREATE TABLE supplier_relation (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    main_id BIGINT NOT NULL,
    supplier_id VARCHAR(255) NOT NULL,
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_main_id (main_id),
    INDEX idx_supplier_id (supplier_id),
    UNIQUE KEY uk_main_supplier (main_id, supplier_id)
);

-- 迁移现有数据(一次性操作)
INSERT INTO supplier_relation (main_id, supplier_id)
SELECT id, SUBSTRING_INDEX(SUBSTRING_INDEX(supplier_id, ',', n.digit+1), ',', -1)
FROM your_table
INNER JOIN (
    SELECT 0 digit UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
    -- 根据最大数量扩展
) n ON LENGTH(REPLACE(supplier_id, ',' , '')) <= LENGTH(supplier_id)-n.digit;

6.2 渐进式改造方案

java 复制代码
// 1. 先创建新表,双写维护
// 2. 逐步将查询迁移到新表
// 3. 最终移除旧字段

@Component
public class SupplierMigrationService {
    
    @Autowired
    private OldMapper oldMapper;
    
    @Autowired
    private NewMapper newMapper;
    
    /**
     * 双写策略,保证数据一致性
     */
    @Transactional
    public void addSupplierId(Long recordId, String newId) {
        // 写入旧表(兼容现有功能)
        oldMapper.addSupplierId(recordId, newId);
        
        // 写入新表
        newMapper.insertSupplierRelation(recordId, newId);
    }
}

📝 总结

操作类型 推荐方案 注意事项
查询 FIND_IN_SETLIKE 注意性能问题,大数据量需要优化
添加 先查询再拼接,避免重复 处理空值和边界情况
移除 Java逻辑处理更安全 注意逗号边界处理
批量 在Service层处理逻辑 保证事务一致性

最佳实践:

  1. 短期:使用上述方案维持现有功能
  2. 中期:考虑添加缓存优化查询性能
  3. 长期:推动表结构重构,使用关联表方案

这种逗号分隔的字段设计虽然不符合数据库范式,但在现有系统下通过合理的技术方案可以稳定运行。

相关推荐
TDengine (老段)4 小时前
从 Wonderware 到 TDengine:大理卷烟厂的国产化转型之路
数据库·物联网·时序数据库·iot·tdengine·涛思数据
自由会客室4 小时前
Ubuntu24.04安装好Mysql8后,检查mysql占用的内存和磁盘
数据库
MrZhangBaby5 小时前
SQL-leetcode—3475. DNA 模式识别
数据库·sql·leetcode
码力引擎5 小时前
【零基础学MySQL】第二章:SQL类型
数据库·sql·mysql·oracle
AscendKing5 小时前
Oracle 数据库OGG 工具简介
数据库·oracle·ogg
敲代码的嘎仔5 小时前
JavaWeb零基础学习Day5——MySQL
java·数据库·学习·程序人生·mysql·adb·改行学it
爱宇阳5 小时前
使用 PowerShell 批量导出 MySQL 数据库(新手教程)
数据库·mysql
落世繁华5 小时前
Docker快速部署--Mysql一键初始化
运维·mysql·docker·容器·一键部署
得物技术5 小时前
RAG—Chunking策略实战|得物技术
数据库·人工智能·算法