Plucker
Plucker allows projecting records extracted from a query into an array of
specifically defined Ruby structs for the occasion. It is an
enchanted pluck
. It
takes a list of values you want to extract and throws them into a custom
array of Ruby struct.
This can make your application more efficient because it avoids loading ActiveRecord objects and utilizes structs, which are more efficient.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add plucker
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install plucker
Usage
posts = Post.joins(:author, :comments).group(:id).plucker(:title, 'authors.name', { comments_count: 'COUNT(comments.id)' })
post = posts.first
post.title # 'How to make pizza'
post.authors_name # 'Henry'
post.comments_count # 2
post.id # NoMethodError: undefined method `id' for #<struct title="How to make pizza", authors_name="Henry", comments_count=2>
Purpose
Let's assume we have these classes:
class Author < ApplicationRecord
has_many :post
validates :name, presence: true
end
class Post < ApplicationRecord
belongs_to :author
validates :title, body, presence: true
def slug
self.title.parameterize
end
end
and we execute this query:
posts = Post.joins(:author).select('id, authors.name AS author_name')
The objects in the posts array are ActiveRecord objects of type Post
. As I
read the code, it feels natural for me to be able to do:
post = posts.first
post.id
post.title
post.body
post.slug
Now, out of these instructions, only post.id
works, while all the others
will result in an error because the fields were not selected. This is very
strange to me, and in a complex codebase, it can lead to confusion and
frustration.
Furthermore, I can see in the code post.author_name
and wonder where that
method or column is defined. Obviously, I won't find the definition of that
method because it is dynamically generated by ActiveRecord. I don't like this
very much; it makes it unclear what data is present in the object.
Therefore, I have decided to write Plucker to have well-defined objects with clear fields right from the start. I aim for lightweight and efficient objects without creating ActiveRecord fat objects with methods that I can't even use.
Keep in mind that you can always continue to perform queries in the standard ActiveRecord way. With Plucker, you have a new, more efficient, and clearer option.
Doc
The arguments of Plucker can be specified in in 3 different ways depending on
the requirements: as a Symbol
, as a String
, or as a Hash
.
When using a symbol, the column with the corresponding name to the symbol is selected, and the struct field will have that name:
post = Post.plucker(:title).last
#<struct title="How to make pizza">
post = Post.joins(:author).plucker(:title, :name).last
#<struct title="How to make pizza", name="Henry">
When using the symbol :*
, it is interpreted as SELECT *
statement,
selecting all columns from the specified table:
post = Post.plucker(:*).last
#<struct id=1, title="How to make pizza", author_id=1, created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00>
When using a string value, the name of the struct field will be generated
using the parameterize
function with an underscore as the separator:
post = Post.joins(:comments).plucker('posts.title', 'COUNT(comments.id)').last
#<struct posts_title="How to make pizza", count_comments_id=2>
When using the string table_name.*
, it is interpreted as SELECT table_name.*
statement, selecting all columns from the specified table:
post = Post.joins(:author).plucker(:*, 'authors.*').last
#<struct id=1, title="How to make pizza", author_id=1, created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, authors_id: 1, authors_name: 'Henry', authors_created_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00, authors_updated_at=Sat, 21 Oct 2023 14:24:08 UTC +00:00>
When using a Hash, it operates similarly to the String case, except that the name of the struct field will be the same as the key of the Hash:
post = Post.joins(:comments).plucker(:title, comments_count: 'COUNT(comments.id)').last
#<struct title="How to make pizza", comments_count=2>
Plucker also takes an optional block, which is passed to the struct definition:
posts = Post.plucker(:title) do
def slug
self.title.parameterize
end
def as_json
super.tap do |json|
json['slug'] = self.slug
end
end
end.last
#<struct title="How to make pizza">
post.title
# 'How to make pizza'
post.slug
# 'how-to-make-pizza'
post.as_json
# {"title"=>"How to make pizza", "slug"=>"how-to-make-pizza"}
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/pioz/plucker.
License
The gem is available as open source under the terms of the MIT License.