Automatically add calculated attributes from accessory select queries to ActiveRecord models.
Add this line to your application's Gemfile:
gem 'calculated_attributes'
And then execute:
$ bundle
Or install it yourself as:
$ gem install calculated_attributes
Add each calculated attribute to your model using the calculated
keyword. It accepts two parameters: a symbol representing the name of the calculated attribute, and a lambda containing a string to calculate the attribute. The lambda can accept arguments.
For example, if we have two models, Post
and Comment
, and Comment
has a post_id
attribute, we might write the following code to add a comments count to each Post
record in a relation:
class Post < ActiveRecord::Base
...
calculated :comments_count, -> { "select count(*) from comments where comments.post_id = posts.id" }
calculated :comments_count_by_user, ->(user) { ["select count(*) from comments where comments.post_id = posts.id and posts.user_id = '%s'", user.id] }
...
end
Then, the comments count may be accessed as follows:
Post.scoped.calculated(:comments_count).first.comments_count
Post.scoped.calculated(comments_count_by_user: user).first.comments_count_by_user
#=> 5
Multiple calculated attributes may be attached to each model. If we add a Tag
model that also has a post_id
, we can update the Post model as following:
class Post < ActiveRecord::Base
...
calculated :comments_count, -> { "select count(*) from comments where comments.post_id = posts.id" }
calculated :tags_count, -> { "select count(*) from tags where tags.post_id = posts.id" }
...
end
And then access both the comments_count
and tags_count
like so:
post = Post.scoped.calculated(:comments_count, :tags_count).first
post.comments_count
#=> 5
post.tags_count
#=> 2
Note that you must call calculated
on a relation in order to get the desired result. Post.calculated(:comments_count)
will give you the currently defined lambda for calculating the comments count. Post.scoped.calculated(:comments_count)
(Rails 3) or Post.all.calculated(:comments_count)
(Rails 4) will give you an ActiveRecord relation including the calculated attribute.
You may also use the calculated
method on a single model instance, like so:
Post.first.calculated(:comments_count).comments_count
#=> 5
Post.first.calculated(comments_count_by_user: user).comments_count_by_user
#=> 0
If you have defined a calculated
method, results of that method will be returned rather than throwing a method missing error even if you don't explicitly use the calculated()
call on the instance:
Post.first.comments_count
#=> 5
Post.first.comments_count_by_user(user)
#=> 0
If you like, you may define calculated
lambdas using Arel syntax:
class Post < ActiveRecord::Base
...
calculated :comments_count, -> { Comment.select(Arel::Nodes::NamedFunction.new("COUNT", [Comment.arel_table[:id]])).where(Comment.arel_table[:post_id].eq(Post.arel_table[:id])) }
...
end
Patches are included to support Rails 3.2, 4.x, 5.x, and 6.x. However, only 4.2 and up are tested and actively maintained.
In Rails 4.x and up, you cannot call count
on a relation with calculated attributes, e.g.
Post.scoped.calculated(:comments_count).count
will error because ActiveRecord does not permit Arel nodes in the count method.
- Fork it (https://github.com/aha-app/calculated_attributes/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Written by Zach Schneider based on ideas from Chris Waters.