Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Make AR::Relation include Enumerable #8794

Closed
wants to merge 1 commit into from

8 participants

@bogdan

In this way Relation will gain more Array behaviors (like as_json method that is no longer required in Relation).
Also it delegates every method to to_a without method_missing which is faster.

A few tests need a change because collect without a block doesn't load the relation anymore. This seems ok.

@robin850
Collaborator
@luke-gru

I'm worried about the order of the include here for Enumerable. Wouldn't this override certain methods, such as count from ActiveRecord::Calculations? It would delegate to to_a and the records would be loaded instead of just querying for the count. Not sure if there are other cases like this, just putting this out there. Otherwise I think this makes sense.

Edit: just thought of others as well. first, last, select, find

@bogdan

@luke-gru Enumerable goes below any other module included in relation in hierarchy, so a method from any other module including Calculations and many others will be called before.

@luke-gru

oh, right. My bad!

@guiocavalcanti

Any news from about this?

@guiocavalcanti guiocavalcanti referenced this pull request in apotonick/roar-rails
Closed

Fail to evaluate ActiveRecord::Relation #40

@apotonick

This totally makes sense and allows us to detect collections easily.

@rafaelfranca

@jonleighton @tenderlove what do you think?

@jonleighton
Collaborator

In the past we made methods like select behave differently depending on whether or not they were passed a block. This turned out to cause confusion, so we removed that. I believe select now raises an error when passed a block. Basicalley I don't think Relation can be transparently treated as enumerable, so I don't think it should include Enumerable.

@tenderlove
Owner

Agree with @jonleighton

@tenderlove tenderlove closed this
@bogdan

It is not raising anything when block is passed:
https://github.com/rails/rails/blob/2443e0d11c0db352ec1a7c62748f19042c2c725d/activerecord/lib/active_record/relation/finder_methods.rb#L63
https://github.com/rails/rails/blob/2443e0d11c0db352ec1a7c62748f19042c2c725d/activerecord/lib/active_record/relation/query_methods.rb#L224

As I remember "Make Relation behave like Enumerable" started from the fact that in previous versions of Rails association was magically showing itself as array:

User.has_many :projects
User.first.projects.class # => Array

Maybe that was a confusion mentioned by @jonleighton, but there is right idea behind it:
Anyway people expecting associations and relations to behave like Array even if it never was it
I think we should give people what they want instead of teaching them that they have to do instead.

My personal concern behind this patch (that supposed to be next step) was ability to embed batches into relation's each method:
Long running Relation#each can appear in places where you didn't expect it initially. Like one object appeared with 10k+ records in has_many relation while the average is below 10.
That is why random memory usage tsunamis appear.

Making Relation to include Enumerable would introduce single point of control for each method. That should make each to use batches by default an easy feature.
It also would be super-cool to forget about batches and make their usage completely automatic.

@rafaelfranca

We already discussed this a lot and we will stand with this decision. Relations are not Arrays even if they have some methods in common.

@bogdan bogdan deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 7, 2013
  1. @bogdan
This page is out of date. Refresh to see the latest.
View
10 activerecord/lib/active_record/relation.rb
@@ -14,7 +14,7 @@ class Relation
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
- include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
+ include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation, ::Enumerable
attr_reader :table, :klass, :loaded
attr_accessor :default_scoped
@@ -198,10 +198,6 @@ def to_a
@records
end
- def as_json(options = nil) #:nodoc:
- to_a.as_json(options)
- end
-
# Returns size of the records.
def size
loaded? ? @records.length : count
@@ -218,7 +214,7 @@ def empty?
# Returns true if there are any records.
def any?
if block_given?
- to_a.any? { |*block_args| yield(*block_args) }
+ super
else
!empty?
end
@@ -227,7 +223,7 @@ def any?
# Returns true if there is more than one record.
def many?
if block_given?
- to_a.many? { |*block_args| yield(*block_args) }
+ super
else
limit_value ? to_a.many? : size > 1
end
View
2  activerecord/lib/active_record/relation/delegation.rb
@@ -11,7 +11,7 @@ module Delegation # :nodoc:
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
+ delegate :to_xml, :to_yaml, :length, :each, :to_ary, :to => :to_a
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
:connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass
View
2  activerecord/lib/active_record/relation/query_methods.rb
@@ -196,7 +196,7 @@ def references!(*args) # :nodoc:
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
def select(*fields)
if block_given?
- to_a.select { |*block_args| yield(*block_args) }
+ super
else
raise ArgumentError, 'Call this with at least one field' if fields.empty?
spawn.select!(*fields)
View
2  activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1451,7 +1451,7 @@ def test_calling_many_should_count_instead_of_loading_association
def test_calling_many_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
- firm.clients.collect # force load
+ firm.clients.to_a # force load
assert_no_queries { assert firm.clients.many? }
end
View
12 activerecord/test/cases/named_scope_test.rb
@@ -22,8 +22,8 @@ def test_found_items_are_cached
all_posts = Topic.base
assert_queries(1) do
- all_posts.collect
- all_posts.collect
+ all_posts.to_a
+ all_posts.to_a
end
end
@@ -192,7 +192,7 @@ def test_any_should_call_proxy_found_if_using_a_block
def test_any_should_not_fire_query_if_scope_loaded
topics = Topic.base
- topics.collect # force load
+ topics.to_a # force load
assert_no_queries { assert topics.any? }
end
@@ -221,7 +221,7 @@ def test_many_should_call_proxy_found_if_using_a_block
def test_many_should_not_fire_query_if_scope_loaded
topics = Topic.base
- topics.collect # force load
+ topics.to_a # force load
assert_no_queries { assert topics.many? }
end
@@ -446,4 +446,8 @@ def test_eager_default_scope_relations_are_deprecated
end
assert_equal [posts(:welcome).title], klass.all.map(&:title)
end
+
+ def test_json_export
+ assert_equal '[{"comment":{"id":1,"post_id":1,"parent_id":null}}]', Comment.where(:id => 1).to_json(:only => [:id, :post_id, :parent_id])
+ end
end
Something went wrong with that request. Please try again.