8

I have a 2 models Game & Theme and they have a has_and_belongs_to_many association. I have tried many solutions to prevent duplicate records in the games_themes table, but no solutions work. The problem is, games_themes is a table, but it is not a model, so I can't figure out a way to run validations on it effectively.

Heres a solution I tried

class Theme < ActiveRecord::Base
  has_and_belongs_to_many :games, :uniq => true
end

class Game < ActiveRecord::Base
  has_and_belongs_to_many :themes, :uniq => true
end
Dave Newton
  • 158,873
  • 26
  • 254
  • 302
user2158382
  • 4,430
  • 12
  • 55
  • 97

4 Answers4

14

You should use database-level validation:

#new_migration
add_index :games_themes, [:game_id, :theme_id], :unique => true

HABTM

This will prevent you saving any duplicate data in the database. Takes the burden off Rails & ensures you only have game or theme. The problem is because HABTM doesn't have a model, there's no validation you can perform in Rails, meaning you need to make it db-level

As mentioned in the comments, this means you'll have to handle the exceptions raised from the db like this:

#app/controllers/games_controller.rb
def create
    #creation stuff here
    if @game.save
        #successful save
    else
        #capture errors
    end
end
Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • In recent versions of Rails, you'll need to catch `ActiveRecord::RecordNotUnique`. – alf Aug 08 '17 at 22:09
4

Use:

validates_uniqueness_of :theme_id, :scope => :game_id

As follows:

class Theme < ActiveRecord::Base
  has_many :games, through: :games_themes
end

class Game < ActiveRecord::Base
  has_many :themes, through: :games_themes
end

class GamesThemes < ActiveRecord::Base
  belongs_to :game
  belongs_to :theme

  validates_uniqueness_of :theme_id, :scope => :game_id
end
Richard Jordan
  • 8,066
  • 3
  • 39
  • 45
  • 1
    Where would I put this validation? None of the models have an attribute in them called `theme_id`. I only have a table called `games_themes` that holds both of the foreign keys. And since I am using HABTM, this table does not have an associated model – user2158382 Feb 20 '14 at 02:09
  • So the correct way to do this is to put the logic where it belongs, which is in the games_themes model. I did read your comment above that you don't want to create a model, but the key is that's what this is for. To isolate the code necessary for making that combination unique, rather than jumping through hoops in the incorrect places. It's the correct home for that validation. – Richard Jordan Feb 20 '14 at 02:12
  • Yes as Pavel said, if you need validations on the join, you must use has_many :through – Richard Jordan Feb 20 '14 at 02:13
  • Ok thanks, rails should find someway to implement this. – user2158382 Feb 20 '14 at 02:14
  • the ```has_many :through``` approach, with the join model IS the way Rails has implemented it. One way to think about it is that your constraint is related to information about two models, right, the theme model's id and the game model's id. It would be wrong to put code that reasons about those two pieces of information in either the game model or the theme model, because that would be unnecessary coupling, and probably some kind of DRY violation depending on how you implemented that. Instead a model which is dedicated to reasoning about the id of both games and themes is used. Make sense? – Richard Jordan Feb 20 '14 at 02:18
1

To run validations on join table you should use has_many :through association instead. http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

  • 1
    I know I can solve the issue like this. But I would like a different solution that doesn't involve creating a new model just to do validation – user2158382 Feb 20 '14 at 02:08
  • @user2158382 I don't think there is another way. You can add a database constraint, but you'll have to deal with database exception then. – Pavel Pachkovsky Feb 20 '14 at 02:11
0

Creating new Model GameTheme for validation purpose is not a good idea. We can validate itself in migration.

Theme Model:

class Theme < ActiveRecord::Base
  has_and_belongs_to_many :games,
    :association_foreign_key => 'theme_id',
    :class_name => 'Theme',
    :join_table => 'games_themes'
end

Game Model:

class Theme < ActiveRecord::Base
  has_and_belongs_to_many :games,
    :association_foreign_key => 'game_id',
    :class_name => 'Game',
    :join_table => 'games_themes'
end

games_themes migration: You can add uniqueness to join table, Have a look here for more detail.

class GamesThemesTable < ActiveRecord::Migration
  def self.up
    create_table :games_themes, :id => false do |t|
      t.references :game
      t.references :theme
    end

    add_index :games_themes, [:theme_id, :game_id], :unique => true

  end

  def self.down
    drop_table :games_themes
  end
end
Community
  • 1
  • 1
Pravin Mishra
  • 8,298
  • 4
  • 36
  • 49