Skip to content

Add Relation#pick as short-hand for single-value plucks#31941

Merged
dhh merged 2 commits into
masterfrom
active-record-pick
Feb 9, 2018
Merged

Add Relation#pick as short-hand for single-value plucks#31941
dhh merged 2 commits into
masterfrom
active-record-pick

Conversation

@dhh

@dhh dhh commented Feb 9, 2018

Copy link
Copy Markdown
Member

Pick the first value from the named column in the current relation. This is short-hand for relation.limit(1).pluck(column_name).first, and is primarily useful when you have a relation that's already narrowed down to a single row.

Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also more efficient. The value is, again like with pluck, typecast by the column type.

  Person.where(id: 1).pick(:name)
  # SELECT people.name FROM people WHERE id = 1 LIMIT 1
  # => 'David'

@composerinteralia

composerinteralia commented Feb 9, 2018

Copy link
Copy Markdown
Member

Might we want pick to take multiple arguments like pluck?

  Person.where(id: 1).pick(:name, :email)
  # SELECT people.name, people.email FROM people WHERE id = 1 LIMIT 1
  # => ['Daniel', 'i@heart.rails']

(I was unreasonably nervous to leave this comment. I actually wrote it, then deleted it, then wrote it again.)


def test_pick
assert_equal "The First Topic", Topic.order(:id).pick(:heading)
assert_nil Topic.where(id: 9999999999999999999).pick(:heading)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failing the assert will be fixed by #30000.

@matthewd

matthewd commented Feb 9, 2018

Copy link
Copy Markdown
Member

is primarily useful when you have a relation that's already narrowed down to a single row

This reminds me of some of the philosophy behind #26206. Might it be worth codifying that rule?

def pick(*column_names)
  rows = limit(2).pluck(*column_names)
  raise "Too many rows" if rows.size > 1
  rows.first
end

@dhh

dhh commented Feb 9, 2018

Copy link
Copy Markdown
Member Author

The specific relation that I extracted #pick from actually does comprise multiple rows, but I know that I just want to deal with the first one (because I ordered the relation in such a way. Here's the actual source:

module Person::Creator
  extend ActiveSupport::Concern

  def last_created_at_from(recordings)
    recordings.where(creator: self).reverse_chronologically.pick(:created_at)
  end
end

So a guard clause against multiple rows wouldn't really work for this. I don't think it's necessary in any case.

@dhh

dhh commented Feb 9, 2018

Copy link
Copy Markdown
Member Author

@composerinteralia Totally. Just added that 👌

@kamipo I just changed the test for now 🙏

@dhh dhh merged commit 80cc0d3 into master Feb 9, 2018
@bogdan

bogdan commented Feb 10, 2018

Copy link
Copy Markdown
Contributor

Will it raise NoMethodError for NullRelation?

@rafaelfranca

Copy link
Copy Markdown
Member

@bogdan no. Is it raising? I just added 4c615a5 and that test is passing. As it is the only way to generate NullRelation I can't see how it would be raising.

@bogdan

bogdan commented Feb 13, 2018

Copy link
Copy Markdown
Contributor

@rafaelfranca It was only my theoretical guess. I think the test is good idea anyway. Thanks!

kamipo added a commit that referenced this pull request Feb 18, 2018
@ohaddahan

Copy link
Copy Markdown

@dhh

The value is, again like with pluck, typecast by the column type.

pg gem (and mysql2) support C typecasting , we did a proof of concept at https://gitlab.com/nativepluck/nativepluck

(local tests on mysql2 gem showed performance improvements too)

Will be happy to pitch in and get this into Rails if this seems worth a while.
(I believe the benchmarks show it is)

@rafaelfranca

Copy link
Copy Markdown
Member

While the database clients supports C typecasting plunk and pick use the types that sometimes are defined by the user using the attributes API. We can of course use C typecasting for the native types, but even those behave differently from the types defined in the database clients so we can't just start to use them.

@ohaddahan

Copy link
Copy Markdown

@rafaelfranca I assume you refer mostly to the more "exotic" types, right?

Given the significant performance boost, I personally think it's good to have, or at least have as an option. I believe most people won't see any difference switching to native typecasting.

But that's for Rails core team to decide, not me.

@henrik

henrik commented Feb 22, 2019

Copy link
Copy Markdown
Contributor

Could a name like pluck_first make sense? I know it's longer, but it would make the difference between pluck and this method clearer. I blogged about this thinking here: https://thepugautomatic.com/2017/01/the-pairing-test/

(I also considered pluck_one, but that'd be confusing with something like users.in_order.pluck_one(:name, :email) – is it just one out of name and email? pluck_first does not have that problem.)

Rails currently has a few bits of naming that can be confusing not only to new devs but also experienced ones – I've worked in Rails for quite a few years now and I still get unsure about validate vs validates sometimes :) I'm seeing a similar (but lesser) risk here, especially for devs new to the framework.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants