Search code examples
ruby-on-railsvalidationmany-to-many

How to prevent saving invalid ids in associative table in Rails?


I have two tables in my Rails app: Category and Service. I have also created an associative table CategoriesService between them and everything works right except validation of ids on the CategoriesService table - I just noticed I am able to create an association with a non-existent record. I wonder how could I fix it properly - I suspect that Rails should let us create some database-level validation of that, which could be faster and cleaner. I defined my models that way:

class Category < ApplicationRecord
  has_and_belongs_to_many :services
end

class Service < ApplicationRecord
  has_and_belongs_to_many :categories
end

class CategoriesService < ApplicationRecord
end

I was thinking that creating has_and_belongs_to_many relationship would ensure this kind of validation itself, but I can see I was wrong. How should I solve this?


Solution

  • From the Rails style guide:

    Prefer has_many :through to has_and_belongs_to_many. Using has_many :through allows additional attributes and validations on the join model.

    In your case:

    class Category < ApplicationRecord
      has_many :categories_services
      has_many :services, through: :categories_services
    end
    
    class Service < ApplicationRecord
      has_many :categories_services
      has_many :categories, through: :categories_services
    end
    
    class CategoriesService < ApplicationRecord
      belongs_to :category
      belongs_to :service
    
      # if not using Rails 5:
      validates :category, presence: true
      validates :service, presence: true
      # if using Rails 5, a `belongs_to` will auto-validate the presence
    end
    

    Using the join model (and not the has_and_belongs_to_many), you have a better control of the many-to-many relation:

    • you (can) have the created_at and updated_at fields on the join table, automatically managed by Rails as usual
    • you could improve your join model to have, for example, a column position and then you could offer a feature of sorting the services of a specific category, a favorite boolean column, etc.

    Additionally, you can (and I recommend you to do so) enforce this validation by adding some constraints in your Database:

    # PostgreSQL
    CREATE TABLE categories_services (
      id SERIAL PRIMARY KEY,
      category_id integer REFERENCES categories NOT NULL,
      service_id integer REFERENCES services NOT NULL,
      created_at timestamp NOT NULL DEFAULT now(),
      updated_at timestamp NOT NULL DEFAULT now()
    );