【问题标题】:Rails has_many through avoiding duplicationRails has_many 通过避免重复
【发布时间】:2026-02-01 04:15:01
【问题描述】:

我通过歌曲模型和艺术家模型之间的关联设置有一个 has_many。 我的代码看起来像这样

SongArtistMap 模型

class SongArtistMap < ActiveRecord::Base
 belongs_to :song
 belongs_to :artist
end

艺人模特

class Artist < ActiveRecord::Base
 has_many :song_artist_maps
 has_many :songs, :through => :song_artist_maps

 validates_presence_of :name
end

歌曲模型

class Song < ActiveRecord::Base
  has_many :song_artist_maps
  has_many :artists, :through => :song_artist_maps
  accepts_nested_attributes_for :artists
end

我有一个用户提交歌曲并输入歌曲标题和歌曲艺术家的表单。

因此,当用户提交歌曲并且我的 Artists 表还没有歌曲的艺术家时,我希望它创建该艺术家并在 SongArtistMap 中设置地图

如果用户提交的歌曲的艺术家已经在 Artists 表中,我只想创建 SongArtistMap 但不复制该艺术家。

目前,每次用户提交歌曲时,都会在我的艺术家表中创建一个新艺术家,即使相同的艺术家已经存在并且为该重复的艺术家创建了 SongArtistMap。

知道如何解决这个问题吗?我觉得 Rails 可能有一些简单的小技巧来修复这个已经内置的问题。谢谢!

【问题讨论】:

  • 你知道find方法吗?你知道创建的方法吗?好吧,Rails 有一个方法 find_or_create_by_attribute!因此,在您的情况下,您可以使用 find_or_create_by_name。但是,由于您使用的是嵌套属性...Accepts nested attributes for with find or create。所以是的,这是一个重复的问题。

标签: ruby-on-rails ruby-on-rails-3


【解决方案1】:

好的,我前一阵子想通了,忘记发帖了。所以这就是我解决问题的方法。首先我意识到我不需要has_many through 关系。

我真正需要的是has_and_belongs_to_many 关系。我设置了它并为它制作了桌子。

然后在我的Artists 模型中添加了这个

def self.find_or_create_by_name(name)
  k = self.find_by_name(name)

  if k.nil?
    k = self.new(:name => name)
  end

  return k
end

在我的Song 模型中我添加了这个

before_save :get_artists
def get_artists
  self.artists.map! do |artist|
   Artist.find_or_create_by_name(artist.name)
  end
end

这正是我想要的。

【讨论】:

    【解决方案2】:

    我在其他两个经过的表模型中使用了一个方法,即使用 before_create 调用。不过,这可能会变得更整洁、更快。

    before_create :ensure_only_one_instance_of_a_user_in_a_group
    
      private
    
      def ensure_only_one_instance_of_a_user_in_a_group
        user = User.find_by_id(self.user_id)
        unless user.groups.empty?
          user.groups.each do |g|
            if g.id == self.group_id
              return false
            end
          end
        end
        return true
      end
    

    【讨论】:

      【解决方案3】:

      试试这个:

      class Song < ActiveRecord::Base
        has_many :song_artist_maps
        has_many :artists, :through => :song_artist_maps
        accepts_nested_attributes_for :artists, :reject_if => :normalize_artist
      
      
        def normalize_artist(artist)
          return true if  artist['name'].blank?
          artist['id'] = Artist.find_or_create_by_name(artist['name']).id
          false # This is needed
        end
      end
      

      我们实际上是通过重载 reject_if 函数来欺骗 Rails(因为我们永远不会返回 true)。

      您可以通过不区分大小写的查找来进一步优化这一点(如果您使用的是 MySQL,则不需要)

          artist['id'] = ( 
           Artist.where("LOWER(name) = ? ", artist['name'].downcase).first ||       
           Artist.create(:name => artist['name'])
          ).id
      

      【讨论】:

      • 我试过了,它似乎没有效果,我仍然会创建重复的艺术家。
      • 在返回 false 之前添加 p artist 进行调试。看看id是否设置正确。
      • 我在返回 false 之前添加了 p 艺术家,但我什么也没得到。该回报是否应该显示在屏幕上或某处的日志中?这也是我提交表单时的服务器日志pastebin.com/AfwxRack
      • 您应该会看到服务器控制台中显示的数据(不是日志文件..)
      • 啊我看到这里是返回 {"name"=&gt;"Daft Punk", "id"=&gt;#&lt;Artist id: 9, name: "Daft Punk", created_at: "2012-03-30 00:04:45", updated_at: "2012-03-30 00:04:45"&gt;} 我也为我的菜鸟道歉