I have a model (Company
) with a HABTM self-join relationship (the join table calls associated companies suppliers
and purchasers
).
I want to prevent companies from ever having duplicate suppliers or purchasers, so I'm adding a uniqueness constraint on supplier-purchaser pairs. I'm adding this constraint at both the database level:
create_table :supply_link, id: false do |t|
t.belongs_to :supplier, null: false, index: true
t.belongs_to :purchaser, null: false, index: true
end
add_index :supply_link, [:supplier_id, :purchaser_id], unique: true
and the data model level:
class Company < ApplicationRecord
has_and_belongs_to_many :purchasers, -> { distinct },
join_table: :supply_link,
class_name: :Company,
foreign_key: :supplier_id,
association_foreign_key: :purchaser_id
has_and_belongs_to_many :suppliers, -> { distinct },
join_table: :supply_link,
class_name: :Company,
foreign_key: :purchaser_id,
association_foreign_key: :supplier_id
end
But in trying to write a spec for this arrangement, I noticed something strange: typically, data model validations preempt database constraints — but not here.
That is, if you set both a data model validation (validates :email, presence: true
) and a DB constraint (t.string :email, null: false
), then the data model catches invalid operations (like User.create(email: nil)
) before the DB ever even sees them.
But in this case, when I try to add the same supplier
to a Company
multiple times, it goes to the database first, and raises an error. If I remove the database constraint altogether, then the data model handles it the right way, and the duplicate entries never get added to the two things happen: 1) the duplicate associations all appear in the join table, but 2) the model doesn't show any duplicates for suppliers
attribute.company.suppliers
(thanks @Pavel Mihailyuk). I've tested this manually in the console, and with RSpec/FactoryGirl.
What's going on here, and what's the right way to write the specs? Should I keep both validations and expect invalid operations to raise an ActiveRecord
error? Should I remove the database constraint and rely solely on the data model's -> { distinct }
scope? Should I report this as a bug in Rails?