CalculatedAttributes
Automatically add calculated attributes from accessory select queries to ActiveRecord models.
Installation
Add this line to your application's Gemfile:
gem 'calculated_attributes'And then execute:
$ bundle
Or install it yourself as:
$ gem install calculated_attributes
Usage
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] }
...
endThen, 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
#=> 5Multiple 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" }
...
endAnd 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
#=> 2Note 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
#=> 0If 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)
#=> 0If 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])) }
...
endVersion support
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.
Known issues
In Rails 4.x and up, you cannot call count on a relation with calculated attributes, e.g.
Post.scoped.calculated(:comments_count).countwill error because ActiveRecord does not permit Arel nodes in the count method.
Contributing
- 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
Credits
Written by Zach Schneider based on ideas from Chris Waters.