41

How can I turn:

Person.all.pluck(:id, :name)

to

[{id: 1, name: 'joe'}, {id: 2, name: 'martin'}]

without having to .map every value (since when I add or remove from the .pluck I have to do he same with the .map)

Lucas Steffen
  • 1,244
  • 2
  • 10
  • 22

11 Answers11

51

You can map the result:

Person.all.pluck(:id, :name).map { |id, name| {id: id, name: name}}

As mentioned by @alebian: This is more efficient than

Person.all.as_json(only: [:id, :name])

Reasons:

  • pluck only returns the used columns (:id, :name) whereas the other solution returns all columns. Depending on the width of the table (number of columns) this makes quite a difference
  • The pluck solution does not instantiate Person objects, does not need to assign attributes to the models and so on. Instead it just returns an array with one integer and one string.
  • as_json again has more overhead than the simple map as it is a generic implementation to convert a model to a hash
Pascal
  • 8,464
  • 1
  • 20
  • 31
  • It uses '.map' which means i have to change the pluck and map when making a change. – Lucas Steffen Mar 16 '16 at 16:15
  • Yes, not that nice. The `select` solution by @Rohit Jangid seems better. – Pascal Jun 06 '17 at 15:08
  • 1
    This version is the most efficient, it should be the one selected – alebian Jun 30 '17 at 13:59
  • 1
    @pascalbetz I wish you hadn't insulted your own answer because the first option you gave is way more performant than the accepted answer. – Adamantish May 17 '18 at 12:56
  • As mentioned by others, I'm not fond of having to repeat myself in `pluck` and `map`, but the enormous performance gains over `as_json(only:)` far out weigh the code complexity in my use cases. – davew Jul 23 '18 at 20:15
  • Verified with Benchmark, using `pluck` w/ `map` is 10x faster than `select` w/ `as_json`...and only 1x uglier, use this one! – webaholik Sep 15 '18 at 19:38
42

You could simply do this

Person.select(:id,:name).as_json

You could try this as well

Person.all.as_json(only: [:id, :name])
Rohit Jangid
  • 1,077
  • 1
  • 8
  • 19
  • Person.select(:id,:name).as_json is the shortest and you only have to change the field in one place Can you add a way to work with aggregation functions like Person.all.as_json(only: ['sum(some_field)'])? – Lucas Steffen Mar 16 '16 at 16:24
  • 1
    @LucasSteffen You can achieve the aggregation by defining a method and calling it. For example you can define as method name `sum_of_fields` in `Person` model which would return the sum of desired fields. Then you call that method as `Person.all.as_json(methods: [:sum_of_fields])`. For more details you could read this [documentation](http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json) – Rohit Jangid Mar 17 '16 at 05:57
  • 3
    The two possibilities provided are not identical, as the first one adds the id into the hash even if it is not asked for e.g. Person.select(:name).as_json returns ```[{id: nil, name: 'joe'}, {id: nil, name: 'martin'}]``` whereas Person.all.as_json(only: [:name]) returns ```[{name: 'joe'}, {name: 'martin'}]``` so for the more general case where you do not want the id, the the second method is better. – Obromios Apr 24 '17 at 05:47
  • @Obromios I totally agree with you and would suggest the same. But if id is required then `Person.select(:id,:name).as_json` would be much cleaner. – Rohit Jangid May 02 '17 at 06:16
  • 6
    The response by pascal betz is far more efficient than this one. This one basically creates N Person objects after the SQL query result and the iterates them to return the JSON. The pluck+map version iterates through the query result and creates the hash objects. A simple benchmark can verify this, in my machine results for 1000 tests return 395.65 seconds for the as_json version (all the runs) and just 26.63 seconds for the pluck+map version (all the runs), my database had 2.2k rows – alebian Jun 30 '17 at 13:58
  • `pluck` is quite a special method. It bypasses the key aspect of Rails that gives it a bad reputation for performance (and good reputation for expressivity). Crucially `select` doesn't satisfy this property. If you're going to dispense Rails' OOP benefits without grabbing the performance benefit is like forgetting to take something in a shop. – Adamantish May 16 '18 at 16:34
  • This approach is what got me googling for a better way. I was using this approach, but the performance was killing my app. I switched to pluck+map and the result was staggering. – davew Jul 23 '18 at 20:09
  • `as_json` is not a solution here as it defeats the purpose of calling `pluck` – Amol Pujari Dec 06 '18 at 06:50
  • I didn't need the json part to shove all this into a dropdown as a collection, just used `select` over `pluck` and it kept the named properties which helped with that mapping! – Pysis Jul 16 '19 at 20:58
  • `.all` instantiates all the records before converting to json. – Alexis Rabago Carvajal Jan 30 '20 at 22:41
  • I'm getting 3 columns from a table. Person.pluck(:id, :first_name, :last_name) But I wanna put it in a map, where id as key and name as value. where name is first_name + " " + last_name. How to do this in ruby the elegant way? – sofs1 Jun 06 '20 at 02:05
  • Also, it returns the hash `keys` as `strings` instead of `symbols` – poramo Oct 13 '21 at 10:14
22

I see three options:

1) pluck plus map:

Person.pluck(:id, :name).map { |p| { id: p[0], name: p[1] } }

2) pluck plus map plus zip and a variable to make it DRY-er:

attrs = %w(id name)
Person.pluck(*attrs).map { |p| attrs.zip(p).to_h }

3) or you might not use pluck at all, although this is much less performant:

Person.all.map { |p| p.slice(:id, :name) }
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • This is awesome! I never thought about combing it with `.map()`, `.zip()`, and `.to_h`! Thanks for sharing @spickermann – Spectator6 Jul 31 '20 at 02:53
  • Note that `p` is a method in Ruby, using it as a parameter for the `map` broke for me. Just use another name, like `toto`. – pierre_loic Dec 03 '22 at 18:50
  • @pierre_loic A local variable in Ruby would shadow a outer method with the same name. That is default Ruby behavior. In this example Ruby would never confuse the variable p with the method p. You must have done something different than if you had such a problem. – spickermann Dec 04 '22 at 01:35
10

If you use postgresql, you can use json_build_object function in pluck method: https://www.postgresql.org/docs/9.5/functions-json.html

That way, you can let db create hashes.

Person.pluck("json_build_object('id', id, 'name', name)")
#=> [{id: 1, name: 'joe'}, {id: 2, name: 'martin'}]
mnishiguchi
  • 2,051
  • 23
  • 17
  • While this approach works, be aware that it is dangerous to use it. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql() – theterminalguy May 09 '20 at 05:14
7

Could go for a hash after the pluck with the ID being the key and the Name being the value:

Person.all.pluck(:id, :name).to_h

{ 1 => 'joe', 2 => 'martin' }

Not sure if this fits your needs, but presenting as an option.

MTarantini
  • 939
  • 7
  • 11
  • 1
    The ideia was to make a hash with the field name as the key – Lucas Steffen Mar 16 '16 at 16:13
  • 3
    I understand. Just all depends on what you're doing with it. Sometimes you alter the data to fit the execution, and other times you alter the execution to fit the data. – MTarantini Mar 16 '16 at 19:42
3

You can use the aptly-named pluck_to_hash gem for this: https://github.com/girishso/pluck_to_hash

It will extend AR with pluck_to_hash method that works like this:

Post.limit(2).pluck_to_hash(:id, :title)
#
# [{:id=>213, :title=>"foo"}, {:id=>214, :title=>"bar"}]
#

Post.limit(2).pluck_to_hash(:id)
#
# [{:id=>213}, {:id=>214}]

It claims to be several times faster than using AR select and as_json

Taufiq Muhammadi
  • 352
  • 4
  • 12
3

There is pluck_all gem that do almost the same thing as pluck_to_hash do. And it claims that it's 30% faster. (see the benchmark here).

Usage:

Person.pluck_all(:id, :name)
khiav reoy
  • 1,373
  • 13
  • 14
  • 1
    Most useful answer I'd say considering `pluck` is often used for performance and this is more performant than any of the other answers here. – Adamantish May 17 '18 at 12:55
1

If you have multiple attributes, you may do this for cleanliness:

Item.pluck(:id, :name, :description, :cost, :images).map do |item|
  {
    id:          item[0],
    name:        item[1],
    description: item[2],
    cost:        item[3],
    images:      item[4]
  }
end
muneebahmad
  • 119
  • 1
  • 3
1

The easiest way is to use the pluck method combined with the zip method.

attrs_array = %w(id name)
Person.all.pluck(attrs_array).map { |ele| attrs_array.zip(ele).to_h }

You can also create a helper method if you are using this method through out your application.

def pluck_to_hash(object, *attrs)
  object.pluck(*attrs).map { |ele| attrs.zip(ele).to_h }
end

Consider modifying by declaring self as the default receiver rather than passing Person.all as the object variable.

Read more about zip.

Abeid Ahmed
  • 315
  • 1
  • 5
  • 15
  • I'm getting 3 columns from a table. Person.pluck(:id, :first_name, :last_name) But I wanna put it in a map, where id as key and name as value. where name is first_name + " " + last_name. How to do this in ruby the elegant way? – sofs1 Jun 06 '20 at 02:05
1

Here is a method that has worked well for me:

    def pluck_to_hash(enumerable, *field_names)
      enumerable.pluck(*field_names).map do |field_values|
        field_names.zip(field_values).each_with_object({}) do |(key, value), result_hash|
          result_hash[key] = value
        end
      end
    end
Keith Bennett
  • 4,722
  • 1
  • 25
  • 35
1

I know it's an old thread but in case someone is looking for simpler version of this

Hash[Person.all(:id, :name)]

Tested in Rails 5.

Marelons
  • 150
  • 6