Project

plucker

0.0
No release in over a year
Plucker allows projecting a query into a specifically defined struct for the query.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

build codecov

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.