在Rails中如何通过单个嵌套表单创建双向Friendship关联?
解决双向Friendship联结记录的无限循环问题
方案一:改进模型回调(推荐)
首先给Friendship模型添加唯一索引,防止重复记录:
- 生成迁移文件:
rails generate migration AddUniqueIndexToFriendships user_id:integer friend_id:integer
- 修改迁移文件,添加唯一约束:
class AddUniqueIndexToFriendships < ActiveRecord::Migration[7.0] def change add_index :friendships, [:user_id, :friend_id], unique: true end end
- 执行迁移:
rails db:migrate
然后修改Friendship模型,通过命名回调+跳过回调+存在性检查解决循环:
class Friendship < ApplicationRecord belongs_to :user belongs_to :friend, class_name: 'User' after_create :create_reciprocal_friendship, unless: :reciprocal_exists? private def create_reciprocal_friendship # 创建反向关系时跳过当前回调,避免无限循环 Friendship.create!(user_id: friend_id, friend_id: user_id) do |friendship| friendship.skip_callback(:create, :after, :create_reciprocal_friendship) end end def reciprocal_exists? # 检查反向关系是否已存在,避免重复创建 Friendship.exists?(user_id: friend_id, friend_id: user_id) end end
方案二:直接用SQL插入绕过回调
如果不想调整回调逻辑,可以直接用参数化SQL插入反向记录,完全绕过ActiveRecord回调:
class Friendship < ApplicationRecord belongs_to :user belongs_to :friend, class_name: 'User' after_create :create_reciprocal_friendship, unless: :reciprocal_exists? private def create_reciprocal_friendship # 参数化SQL避免注入风险,直接插入记录不触发回调 ActiveRecord::Base.connection.execute( "INSERT INTO friendships (user_id, friend_id, created_at, updated_at) VALUES (?, ?, ?, ?)", friend_id, user_id, Time.now.utc, Time.now.utc ) end def reciprocal_exists? Friendship.exists?(user_id: friend_id, friend_id: user_id) end end
方案三:在控制器层面处理
如果不想在模型中添加回调,可将双向关系的创建逻辑移到UsersController的update方法中:
class UsersController < ApplicationController def update @user = User.find(params[:id]) # 过滤空值并转为整数格式 new_friend_ids = params[:user][:friend_ids].reject(&:blank?).map(&:to_i) current_friend_ids = @user.friend_ids # 处理新增好友:创建双向关系 (new_friend_ids - current_friend_ids).each do |friend_id| Friendship.find_or_create_by(user_id: @user.id, friend_id: friend_id) Friendship.find_or_create_by(user_id: friend_id, friend_id: @user.id) end # 处理取消的好友:删除双向关系 (current_friend_ids - new_friend_ids).each do |friend_id| Friendship.where(user_id: @user.id, friend_id: friend_id).destroy_all Friendship.where(user_id: friend_id, friend_id: @user.id).destroy_all end if @user.update(user_params.except(:friend_ids)) redirect_to @user, notice: '用户信息更新成功' else render :edit end end private def user_params params.require(:user).permit(:full_name, friend_ids: []) end end
注意:方案三需要手动处理新增和删除的双向关系,逻辑相对繁琐,但完全避免了模型回调的循环问题。
内容的提问来源于stack exchange,提问作者Tony Moritz
相关产品推荐
相关产品推荐

