MySQL逗号分隔字段-历史遗留原因兼容方案
MySQL逗号分隔字段的MyBatis操作指南
📋 背景说明
字段定义:
create your_table(supplier_id varchar(255) NULL -- (多个ID逗号分隔)数据格式示例:1,5,3,8,12
);
现状分析:
- 字段已在生产环境使用,不能修改表结构
- 需要支持单个ID的增删改查操作
- 需要保持数据格式的一致性
🔍 查询操作
1.1 精确查询(推荐使用)
<!-- 方法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 多条件查询
<!-- 查询同时包含多个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
<!-- 安全添加,避免重复 -->
<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_idENDWHERE id = #{recordId}
</update>
2.2 移除特定的supplier_id
<!-- 安全移除,处理各种边界情况 -->
<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) > 0AND id = #{recordId}
</update>
移除操作的详细处理逻辑:
CONCAT(',', supplier_id, ',')- 在首尾添加逗号,统一格式REPLACE(CONCAT(...), CONCAT(',', #{removeId}, ','), ',')- 替换目标IDREPLACE(..., ',,', ',')- 处理可能产生的连续逗号TRIM(BOTH ',' FROM ...)- 移除首尾逗号
2.3 批量更新整个supplier_id列表
<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的记录
<delete id="deleteBySupplierId">DELETE FROM your_table WHERE FIND_IN_SET(#{supplierId}, supplier_id) > 0
</delete>
💻 Java代码实现
4.1 Mapper接口定义
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层实现
@Service
@Transactional
public class SupplierService {@Autowiredprivate 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无法使用索引,全表扫描- 数据量大时查询性能差
优化建议:
-- 可以考虑添加全文索引(如果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 数据一致性保障
// 在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 事务管理
@Service
@Transactional(rollbackFor = Exception.class)
public class SupplierService {/*** 批量操作,保证原子性*/@Transactionalpublic 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 推荐方案:建立关联表
-- 创建关联表(推荐方案)
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 渐进式改造方案
// 1. 先创建新表,双写维护
// 2. 逐步将查询迁移到新表
// 3. 最终移除旧字段@Component
public class SupplierMigrationService {@Autowiredprivate OldMapper oldMapper;@Autowiredprivate NewMapper newMapper;/*** 双写策略,保证数据一致性*/@Transactionalpublic void addSupplierId(Long recordId, String newId) {// 写入旧表(兼容现有功能)oldMapper.addSupplierId(recordId, newId);// 写入新表newMapper.insertSupplierRelation(recordId, newId);}
}
📝 总结
| 操作类型 | 推荐方案 | 注意事项 |
|---|---|---|
| 查询 | FIND_IN_SET 或 LIKE | 注意性能问题,大数据量需要优化 |
| 添加 | 先查询再拼接,避免重复 | 处理空值和边界情况 |
| 移除 | Java逻辑处理更安全 | 注意逗号边界处理 |
| 批量 | 在Service层处理逻辑 | 保证事务一致性 |
最佳实践:
- 短期:使用上述方案维持现有功能
- 中期:考虑添加缓存优化查询性能
- 长期:推动表结构重构,使用关联表方案
这种逗号分隔的字段设计虽然不符合数据库范式,但在现有系统下通过合理的技术方案可以稳定运行。
