Rails Reflection Methods
Imagine that you have a file system application to manage contents with a
class Category < ApplicationRecord has_many :books, dependent: :restrict_with_error has_many :posts, dependent: :restrict_with_exception end
So, if you try to destroy a
Category record that contains 2 books and 1 post
category_1.destroy!. You will fail to destroy it because there are dependencies.
Now, you have to implement a feature that disables the delete button on the UI if a category could not be destroyed instead of showing the error when the end-user tries to delete the items.
In an easy way, you can do this:
class Category < ApplicationRecord ... def destroyable? !(self.books.any? || self.posts.any?) end end
However, when your application grows bigger and there are many more things that need to be restricted before destroying a category record. You have to update the
destroyable? method above each time. For example:
class Category < ApplicationRecord has_many :books, dependent: :restrict_with_error has_many :posts, dependent: :restrict_with_exception has_one :metadata_file, dependent: :restrict_with_error def destroyable? !(self.books.any? || self.posts.any? || self.metadata_file.any?) end end
Then, you introduce a Reference Model. This model represents the relationship when a post refers to a book.
class Post < ApplicationRecord has_many :reference_books, through: references, source: referenceable, source_type: Book, dependent: :restrict_with_error end
You also want to check if a Post is
class Post < ApplicationRecord ... def destroyable? !reference_books.any? end end
destroyable? is duplicated in these two models now. It is time to write something that is dynamic and you could not worry about updating these
ActiveRecord provides reflection methods for obtaining info on the associations. We will use the
reflect_on_all_associations to iterate through model associations and check if the restriction list is empty or not.
module Destroyable extend ActiveSupport::Concern def destroyable? self.class.reflect_on_all_associations.all? do |assoc| [ %i[restrict_with_error restrict_with_exception].exclude?(assoc.options[:dependent]), (assoc.macro == :has_one && send(assoc.name).nil?), (assoc.macro == :has_many && send(assoc.name).empty?) ].any? end end end
And now we could reuse it in our models.
class Category < ApplicationRecord ... include Destroyable end class Post < ApplicationRecord ... include Destroyable end
There are some notices from me when you use this one:
- You should have good unit tests on this destroyable? method to check if there is an issue related to the newer rails version.
- You also need to care about your application code loading strategy (check Eager loading for greater good). It is better to add
Rails.application.eager_load!at the first line of your
reflection methods are useful in some specific cases. You should consider it carefully before applying it to your application. It is powerful if you use it right. It is dangerous if you overuse it.