Rails 使用单个 class 设计 belongs_to 和 has_many 的 ActiveModel

Rails ActiveModel designing belongs_to and has_many with single class

我需要一些帮助来建模我的模型和控制器。这是我想要实现的目标:

我想要一个名为 User 的设计用户(像往常一样)和一个名为 Project 的第二个模型。一个项目应该属于一个用户,同时应该有很多参与者。项目的参与者也应该是用户(具有设计 registration/login),但创建项目的用户不应该能够参与。 到目前为止,一切都很好。棘手的部分来了:在我的控制器中,我希望能够写:

def participate
  p = Project.find(id: params[:id])
  p.participants << current_user unless p.participants.includes?(current_user) && !p.user_id.equal(current_user.id)
  if p.save
    redirect_back
  else
    render :project
  end
end

这不起作用,因为 p.participants 不是数组,而且查询(我在 rails 控制台中尝试过)没有检查我的 n:m table。 这是我当前的模型设置:

class Project < ApplicationRecord
  before_validation :set_uuid, on: :create
  validates :id, presence: true

  belongs_to :user
  has_and_belongs_to_many :participants, class_name: "User"
end
class User < ApplicationRecord
  before_validation :set_uuid, on: :create
  validates :id, presence: true

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_and_belongs_to_many :projects
end

最后是我的迁移:

class CreateProjects < ActiveRecord::Migration[6.0]
  def change
    create_table :projects, id: false do |t|
      t.string :id, limit: 36, primary_key: true
      t.string :title
      t.belongs_to :user, index: true, foreign_key: true, type: :uuid
      t.datetime :published_at

      t.timestamps
    end
  end
end
class CreateJoinTableProjectsUsers < ActiveRecord::Migration[6.0]
  def change
    create_join_table :users, :projects do |t|
      t.index :project_id
      t.index :user_id
    end
  end
end

最好使用has_many: through 而不是has_and_belongs_to_many。这使您可以编写更清晰的验证代码。

  1. 从用户和项目模型中删除 has_and_belongs_to_many
  2. 添加 has_many :through 到用户和项目模型

    rails g model UserProject user:references project:references
    rails db:migrate
    
    class User < ApplicationRecord
      ..
      has_many :user_projects
      has_many :projects, through: :user_projects
      ..
    end
    
    class Project < ApplicationRecord
      ..
      has_many :user_projects
      has_many :participants, through: :user_projects, source: 'user'
      ..
    end
    
    class UserProject < ApplicationRecord
      belongs_to :user
      belongs_to :project
    end
    
  3. 向 UserProject 模型添加验证

    class UserProject < ApplicationRecord
      belongs_to :user
      belongs_to :project
    
      validate :check_participant
    
      private
      def check_participant
        return if project.participants.pluck(:id).exclude?(user.id) && project.user != user
        errors.add(:base, 'You cannot be participant')
      end
    end
    
  4. 更新参与方式

    def participate
      p = Project.find(id: params[:id])
      begin
        p.participants << current_user
        redirect_back
      rescue ActiveRecord::RecordInvalid => invalid
        puts invalid.record.errors
        render :project
      end
    end