Validate uniqueness of name in a HABTM association

2019-06-13 19:12发布

I have an Artist model that has many Albums in a HABTM association. Though I would like to permit two different albums to have the same name I'd like to ensure that there aren't two in the collection of one artist. So far example:

artist_1 = Artist.create(name: "Jay-Z")
artist_2 = Artist.create(name: "The Beatles")

album_1 = Album.create(name: "The Black Album", artist_ids: [1])

album_2 = Album.create(name: "The Black Album", artist_ids: [1])
=> Should return error

album_2 = Album.create(name: "The Black Album", artist_ids: [2])
=> Should not return an error

I first thought of validating the uniqueness of the name in the Album model but get this error when I try to create a new object:

SQLite3::SQLException: no such column: albums.artist_id: SELECT 1 AS one FROM "albums" WHERE ("albums"."name" = 'The Black Album' AND "albums"."artist_id" IS NULL) LIMIT 1

I then thought of putting the validation in my join model, AlbumArtist but get the error undefined method 'name' (name is one of the album's attributes):

undefined method `name' for #<AlbumArtist:0x007f8e1fc335e8>

How can I get this to work?

class Album < ActiveRecord::Base
    has_many :album_artists
    has_many :artist, through: :album_artists
end

class AlbumArtist < ActiveRecord::Base
  belongs_to :album
  belongs_to :artist

  # validates_uniqueness_of :name, scope: [:artist_id]
end

class Artist < ActiveRecord::Base
  has_many :album_artists
  has_many :albums, through: :album_artists

  # validates_uniqueness_of :name, scope: [:artist_id]
end

Schema

create_table "albums", force: :cascade do |t|
  t.string   "name"
end

create_table "album_artists", force: :cascade do |t|
  t.integer  "album_id"
  t.integer  "artist_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

add_index "album_artists", ["album_id"], name: "index_album_artists_on_album_id"
add_index "album_artists", ["artist_id"], name: "index_album_artists_on_artist_id"

create_table "artists", force: :cascade do |t|
  t.string   "name"
end

1条回答
趁早两清
2楼-- · 2019-06-13 19:52

The most straightforward way in this case would be to put a custom validator in your relation model:

class AlbumArtist < ActiveRecord::Base
  belongs_to :album
  belongs_to :artist

  validates_presence_of :album, :artist

  validate :ensure_unique, on: :create

  private

  def ensure_unique
    if self.artist.albums.where(name: self.album.name).any?
      errors[:base] << 'Artist already has an album by this name'
    end
  end
end

You may also want to add an index to the name column if you don't already have one.

查看更多
登录 后发表回答