Mongoid
Mongoid

Reputation: 97

How to make sure that relation is unique from both sides of many to many

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

Answers (2)

s_dolan
s_dolan

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

rantingsonrails
rantingsonrails

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

Related Questions