3

I have three models like this:

class User < ActiveRecord::Base
  has_many :items
  has_many :other_itmes
end

class Item < ActiveRecord::Base
  belongs_to :user
  has_and_belongs_to_many :other_items

  validate :validate_other_item_ownership
  def validate_other_item_ownership
    if
      (user_ids = OtherItem.where(id: other_item_ids).pluck(:user_id).uniq).present? &&
        (user_ids.size > 1 || user_ids.first != user_id)
    then
      errors.add(:other_item_ids, 'Other Items must belong to same user as Item')
    end
  end
end

class OtherItem < ActiveRecord::Base
  belongs_to :user
  has_and_belongs_to_many :items

  validate :validate_item_ownership
  def validate_item_ownership
    if
      (user_ids = Item.where(id: item_ids).pluck(:user_id).uniq).present? &&
        (user_ids.size > 1 || user_ids.first != user_id)
    then
      errors.add(:item_ids, 'Items must belong to same user as Other Item')
    end
  end
end

And two controllers like this:

class ItemsController < ApplicationController
  def update
    @item = Item.find params[:id]
    @item.other_item_ids = params[:item][:other_item_ids] #assignline
    @item.save!
  end
end

class OtherItemsController < ApplicationController
  def update
    @other_item = OtherItem.find params[:id]
    @other_item.item_ids = params[:other_item][:item_ids] #assignline
    @other_item.save!
  end
end

The Problem now is that ActiveRecord already saves the items on #assignline, while the call to #save! correctly raises ActiveRecord::RecordInvalid the association is still persisted.

I want the user only to be able to link to items to each other which he owned.

4

1 回答 1

4

好问题!我也有一个很好的答案;)

( TLDR )放弃has_and_belongs_to_many,在连接表之上创建一个模型,将验证逻辑放在连接模型中,然后使用has_many :through.

为了演示,让我们考虑一下都属于 User 的 Artist 和 Song 之间的关系。使用连接模型,has_many :through我们将拥有这些模型类:

class Artist
  belongs_to :user
  has_many :artist_songs
  has_many :songs, through: :artist_songs
end

class Song
  belongs_to :user
  has_many :artist_songs
  has_many :artists, through: :artist_songs
end

class ArtistSong
  belongs_to :artist
  belongs_to :song
end

这将显着清理您的验证逻辑,只需将其添加到一个地方:

class ArtistSong
  #...
  validate :ownership
private
  def ownership
    unless artist.user_id == song.user_id
      errors[:base] << 'artist and song must belong to same user'
    end
  end
end

然后在你的控制器中:

class ArtistsController
  def update
    @artist = current_user.artists.find params[:id]
    @artist.update artist_params
  end
private
  def artist_params
    params.require(:artist).permit(:name, song_ids: [])
  end
end

注意:看起来 Rails 正在save!对连接模型进行操作。这意味着如果验证失败(只是 on ArtistSong, not Artist),它将引发异常而不是返回 false。这应该只发生在恶意用户身上,所以不用担心。

我很少使用HABTM。拥有连接表的模型提供了更多的灵活性。例如,您可以添加一个位置字段并执行以下操作:

class Artist
  #...
  has_many :artist_songs, order: -> {'position asc'}
  #...
end
于 2013-11-22T22:18:04.537 回答