Rails 5咨询:同一patient_id仅保留唯一非空death_date的实现方案
这个场景我之前处理过,确实普通唯一索引直接搞不定——因为SQL里NULL值不参与唯一性判断,同一个patient_id可以插多条death_date为空的记录,但我们要限制只能有一条非空的。在Rails 5里,最优的方案是数据库层面的部分唯一索引 + 模型自定义验证,双重保障,既不会被绕过,又能给用户友好提示。
1. 数据库层面:部分唯一索引(Partial Unique Index)
这是最可靠的防线,直接在数据库层面约束,不管是通过Rails操作还是直接写SQL,都没法违反这个规则。
以PostgreSQL为例(MySQL 5.7+也支持语法类似的部分索引),创建迁移文件:
class AddPartialUniqueIndexToFollowups < ActiveRecord::Migration[5.2] def change # 给patient_id加唯一索引,但只生效在death_date非空的记录上 add_index :followups, :patient_id, unique: true, where: "death_date IS NOT NULL", name: "index_followups_patient_id_on_non_null_death_date" # 自定义索引名,避免默认名太长 end end
这个索引的作用很明确:只有当death_date不为空时,patient_id必须唯一。这样同一个患者可以有N条未填写死亡日期的随访记录,但永远只能有一条带死亡日期的记录。
2. 模型层面:自定义验证
数据库约束会直接抛出数据库异常,用户体验不好,所以我们在模型里加自定义验证,提前拦截无效请求:
class Followup < ApplicationRecord belongs_to :patient # 自定义验证规则 validate :only_one_non_null_death_date_per_patient private def only_one_non_null_death_date_per_patient # 如果当前记录的death_date是空的,不用检查 return unless death_date.present? # 构建查询:找同一个患者的、已经存在的非空死亡日期记录 conflicting_records = Followup.where(patient_id: patient_id) .where.not(death_date: nil) # 如果是更新操作,要排除当前记录本身 conflicting_records = conflicting_records.where.not(id: id) if persisted? # 如果存在冲突记录,添加错误信息 if conflicting_records.exists? errors.add(:death_date, "该患者已经有有效的死亡日期记录,不能重复添加或修改") end end end
3. 控制器层面:捕获数据库异常(兜底)
虽然模型验证已经拦截了大部分情况,但并发场景下还是有可能出现两个请求同时通过模型验证,然后触发数据库约束的情况。这时候我们在控制器里捕获异常,转换成友好提示:
class FollowupsController < ApplicationController def create @followup = Followup.new(followup_params) if @followup.save redirect_to @followup, notice: '随访记录创建成功' else render :new end rescue ActiveRecord::RecordNotUnique # 把数据库异常转换成模型错误信息 @followup.errors.add(:death_date, "该患者已经有有效的死亡日期记录") render :new end def update @followup = Followup.find(params[:id]) if @followup.update(followup_params) redirect_to @followup, notice: '随访记录更新成功' else render :edit end rescue ActiveRecord::RecordNotUnique @followup.errors.add(:death_date, "该患者已经有有效的死亡日期记录") render :edit end end
为什么这是最优方案?
- 数据库部分索引是最底层的保障,彻底杜绝数据不一致的可能;
- 模型验证提前拦截,给用户即时反馈,避免等到数据库报错;
- 控制器捕获异常处理并发场景的边缘情况,覆盖所有可能的错误路径。
如果用的是比较老的数据库(比如MySQL 5.6及以下不支持部分索引),可以考虑用数据库触发器作为替代,但触发器维护起来比较麻烦,还是优先升级数据库或者用上面的方案。
内容的提问来源于stack exchange,提问作者Massimiliano
相关产品推荐
相关产品推荐

