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?
From the Rails style guide:
Prefer
has_many :through
tohas_and_belongs_to_many
. Usinghas_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:
created_at
and updated_at
fields on the join table, automatically managed by Rails as usualposition
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()
);