Reputation: 97
I have a model that acts as a many to many relation. Class name is RelatedDocument
which is self explanatory, I basically use it to related Document
class instances with one to another.
I ran into issues with validations so for example I have this in RelatedDocument class:
validates :document, presence: true, uniqueness: { scope: :related_document }
validates :related_document, presence: true
This works I m unable to create duplicate document_id/related_document_id
row. However if I wanted to make this unique from the other side of the relation and change the validation to this :
validates :document, presence: true, uniqueness: { scope: :related_document }
validates :related_document, presence: true, uniqueness: { scope: :document }
This does not work the same from the other side. I was writing rspec test when I noticed this. How can I write validation or a custom validation method that prevents saving of the same id combination, no matter from which side they are?
Update
Per first comment in the comment section saying that the first uniqueness validation will take care of the boths sides, I say it doesn't simply because my rspec test fails, here are they :
describe 'relation uniqueness' do
let!(:base_doc) { create(:document) }
let!(:another_doc) { create(:document) }
let!(:related_document) { described_class.create(document: another_doc, related_document: base_doc) }
it 'raises ActiveRecord::RecordInvalid, not allowing duplicate relation links' do
expect { described_class.create!(document: another_doc, related_document: base_doc) }
.to raise_error(ActiveRecord::RecordInvalid)
end
it 'raises ActiveRecord::RecordInvalid, not allowing duplicate relation links' do
expect { described_class.create!(document: base_doc, related_document: another_doc) }
.to raise_error(ActiveRecord::RecordInvalid)
end
end
Second test fails.
Upvotes: 2
Views: 206
Reputation: 1276
In case you'd like to consider an alternative to the custom validation that has already been provided, you could create reciprocal relationships each time a new record is added and use your existing validation.
For example, when I say that "Document A is related to Document B", I would then also insert a record stating that "Document B is related to Document A". You can keep your validations simple, and can more easily implement logic in the future for when you don't want a reciprocal relationship in some cases (perhaps the relationship is only reciprocated if the documents are from a different author).
Here is an untested example of the kind of model callbacks you would implement:
create_table :documents do |t|
t.string :title, null: false
end
create_table :related_documents do |t|
t.integer :source_document_id, null: false
t.integer :related_document_id, null: false
end
add_index :related_documents, [:source_document_id, :related_document_id], unique: true
add_foreign_key :related_documents, :documents, column: :source_document_id
add_foreign_key :related_documents, :documents, column: :related_document_id
class Document < ActiveRecord::Base
# We have many document relationships where this document is the source document
has_many :related_documents, foreign_key: :source_document_id
validates :title,
presence: true
end
class RelatedDocument < ActiveRecord::Base
after_create :add_reciprocal_relationship
after_destroy :remove_reciprocal_relationship
belongs_to :source_document, class_name: Document
belongs_to :related_document, class_name: Document
validates :source_document,
presence: true
validates :related_document,
presence: true,
uniqueness: {
scope: :source_document
}
private
# Creates a reciprocal relationship
def add_reciprocal_relationship
RelatedDocument.find_or_create_by(
related_document: self.source_document,
source_document: self.related_document
)
end
# Safely removes a reciprocal relationship if it exists
def remove_reciprocal_relationship
RelatedDocument.find_by(
related_document: self.source_document,
source_document: self.related_document
)&.destroy
end
end
Upvotes: 2
Reputation: 568
A custom validator should work here
validate :unique_document_pair
def unique_document_pair
if RelatedDocument.exists?(:document => [self.document,self.related_document], :related_document => [self.document, self.related_document])
errors.add :base, "error"
end
end
Upvotes: 2