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>
移除操作的详细处理逻辑:
CONCAT(',', supplier_id, ',')- 在首尾添加逗号,统一格式REPLACE(CONCAT(...), CONCAT(',', #{removeId}, ','), ',')- 替换目标IDREPLACE(..., ',,', ',')- 处理可能产生的连续逗号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_SET 或 LIKE |
注意性能问题,大数据量需要优化 |
| 添加 | 先查询再拼接,避免重复 | 处理空值和边界情况 |
| 移除 | Java逻辑处理更安全 | 注意逗号边界处理 |
| 批量 | 在Service层处理逻辑 | 保证事务一致性 |
最佳实践:
- 短期:使用上述方案维持现有功能
- 中期:考虑添加缓存优化查询性能
- 长期:推动表结构重构,使用关联表方案
这种逗号分隔的字段设计虽然不符合数据库范式,但在现有系统下通过合理的技术方案可以稳定运行。