提交 19098081 编写于 作者: J Jeremy Kemper

Merge pull request #9200 from wangjohn/unscoping_activerecord_merging

Introduce relation #unscope
## Rails 4.0.0 (unreleased) ##
* Added functionality to unscope relations in a relations chain. For
instance, if you are passed in a chain of relations as follows:
Posts.select(:name => "John").order('id DESC')
but you want to get rid of order, then this feature allows you to do:
Posts.select(:name => "John").order("id DESC").unscope(:order)
== Posts.select(:name => "John")
The .unscope() function is more general than the .except() method because
.except() only works on the relation it is acting on. However, .unscope()
works for any relation in the entire relation chain.
*John Wang*
* Postgresql timestamp with time zone (timestamptz) datatype now returns a
ActiveSupport::TimeWithZone instance instead of a string
......
......@@ -317,6 +317,67 @@ def reorder!(*args) # :nodoc:
self
end
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
:limit, :offset, :joins, :includes, :from,
:readonly, :having])
# Removes an unwanted relation that is already defined on a chain of relations.
# This is useful when passing around chains of relations and would like to
# modify the relations without reconstructing the entire chain.
#
# User.all.order('email DESC').unscope(:order) == User.all
#
# The method arguments are symbols which correspond to the names of the methods
# which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES.
# The method can also be called with multiple arguments. For example:
#
# User.all.order('email DESC').select('id').where(:name => "John")
# .unscope(:order, :select, :where) == User.all
#
# One can additionally pass a hash as an argument to unscope specific :where values.
# This is done by passing a hash with a single key-value pair. The key should be
# :where and the value should be the where value to unscope. For example:
#
# User.all.where(:name => "John", :active => true).unscope(:where => :name)
# == User.all.where(:active => true)
#
# Note that this method is more generalized than ActiveRecord::SpawnMethods#except
# because #except will only affect a particular relation's values. It won't wipe
# the order, grouping, etc. when that relation is merged. For example:
#
# Post.comments.except(:order)
#
# will still have an order if it comes from the default_scope on Comment.
def unscope(*args)
check_if_method_has_arguments!("unscope", args)
spawn.unscope!(*args)
end
def unscope!(*args)
args.flatten!
args.each do |scope|
case scope
when Symbol
symbol_unscoping(scope)
when Hash
scope.each do |key, target_value|
if key != :where
raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
end
Array(target_value).each do |val|
where_unscoping(val)
end
end
else
raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
end
end
self
end
# Performs a joins on +args+:
#
# User.joins(:posts)
......@@ -762,6 +823,39 @@ def build_arel
private
def symbol_unscoping(scope)
if !VALID_UNSCOPING_VALUES.include?(scope)
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
end
single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
unscope_code = :"#{scope}_value#{'s' unless single_val_method}="
case scope
when :order
self.send(:reverse_order_value=, false)
result = []
else
result = [] unless single_val_method
end
self.send(unscope_code, result)
end
def where_unscoping(target_value)
target_value_sym = target_value.to_sym
where_values.reject! do |rel|
case rel
when Arel::Nodes::In, Arel::Nodes::Equality
subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
subrelation.name.to_sym == target_value_sym
else
raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented."
end
end
end
def custom_join_ast(table, joins)
joins = joins.reject { |join| join.blank? }
......
......@@ -424,6 +424,150 @@ def test_order_after_reorder_combines_orders
assert_equal expected, received
end
def test_unscope_overrides_default_scope
expected = Developer.all.collect { |dev| [dev.name, dev.id] }
received = Developer.order('name ASC, id DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
def test_unscope_after_reordering_and_combining
expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
received = DeveloperOrderedBySalary.reorder('name DESC').unscope(:order).order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
expected_2 = Developer.all.collect { |dev| [dev.name, dev.id] }
received_2 = Developer.order('id DESC, name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
assert_equal expected_2, received_2
expected_3 = Developer.all.collect { |dev| [dev.name, dev.id] }
received_3 = Developer.reorder('name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
assert_equal expected_3, received_3
end
def test_unscope_with_where_attributes
expected = Developer.order('salary DESC').collect { |dev| dev.name }
received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect { |dev| dev.name }
assert_equal expected, received
expected_2 = Developer.order('salary DESC').collect { |dev| dev.name }
received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect { |dev| dev.name }
assert_equal expected_2, received_2
expected_3 = Developer.order('salary DESC').collect { |dev| dev.name }
received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect { |dev| dev.name }
assert_equal expected_3, received_3
end
def test_unscope_multiple_where_clauses
expected = Developer.order('salary DESC').collect { |dev| dev.name }
received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect { |dev| dev.name }
assert_equal expected, received
end
def test_unscope_with_grouping_attributes
expected = Developer.order('salary DESC').collect { |dev| dev.name }
received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name }
assert_equal expected, received
expected_2 = Developer.order('salary DESC').collect { |dev| dev.name }
received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect { |dev| dev.name }
assert_equal expected_2, received_2
end
def test_unscope_with_limit_in_query
expected = Developer.order('salary DESC').collect { |dev| dev.name }
received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect { |dev| dev.name }
assert_equal expected, received
end
def test_order_to_unscope_reordering
expected = DeveloperOrderedBySalary.all.collect { |dev| [dev.name, dev.id] }
received = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order).collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
def test_unscope_reverse_order
expected = Developer.all.collect { |dev| dev.name }
received = Developer.order('salary DESC').reverse_order.unscope(:order).collect { |dev| dev.name }
assert_equal expected, received
end
def test_unscope_select
expected = Developer.order('salary ASC').collect { |dev| dev.name }
received = Developer.order('salary DESC').reverse_order.select(:name => "Jamis").unscope(:select).collect { |dev| dev.name }
assert_equal expected, received
expected_2 = Developer.all.collect { |dev| dev.id }
received_2 = Developer.select(:name).unscope(:select).collect { |dev| dev.id }
assert_equal expected_2, received_2
end
def test_unscope_offset
expected = Developer.all.collect { |dev| dev.name }
received = Developer.offset(5).unscope(:offset).collect { |dev| dev.name }
assert_equal expected, received
end
def test_unscope_joins_and_select_on_developers_projects
expected = Developer.all.collect { |dev| dev.name }
received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect { |dev| dev.name }
assert_equal expected, received
end
def test_unscope_includes
expected = Developer.all.collect { |dev| dev.name }
received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect { |dev| dev.name }
assert_equal expected, received
end
def test_unscope_having
expected = DeveloperOrderedBySalary.all.collect { |dev| dev.name }
received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect { |dev| dev.name }
assert_equal expected, received
end
def test_unscope_errors_with_invalid_value
assert_raises(ArgumentError) do
Developer.includes(:projects).where(name: "Jamis").unscope(:stupidly_incorrect_value)
end
assert_raises(ArgumentError) do
Developer.all.unscope(:includes, :select, :some_broken_value)
end
assert_raises(ArgumentError) do
Developer.order('name DESC').reverse_order.unscope(:reverse_order)
end
assert_raises(ArgumentError) do
Developer.order('name DESC').where(name: "Jamis").unscope()
end
end
def test_unscope_errors_with_non_where_hash_keys
assert_raises(ArgumentError) do
Developer.where(name: "Jamis").limit(4).unscope(limit: 4)
end
assert_raises(ArgumentError) do
Developer.where(name: "Jamis").unscope("where" => :name)
end
end
def test_unscope_errors_with_non_symbol_or_hash_arguments
assert_raises(ArgumentError) do
Developer.where(name: "Jamis").limit(3).unscope("limit")
end
assert_raises(ArgumentError) do
Developer.select("id").unscope("select")
end
assert_raises(ArgumentError) do
Developer.select("id").unscope(5)
end
end
def test_order_in_default_scope_should_not_prevail
expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
......
......@@ -692,6 +692,27 @@ The SQL that would be executed:
SELECT * FROM posts WHERE id > 10 LIMIT 20
```
### `unscope`
The `except` method does not work when the relation is merged. For example:
```ruby
Post.comments.except(:order)
```
will still have an order if the order comes from a default scope on Comment. In order to remove all ordering, even from relations which are merged in, use unscope as follows:
```ruby
Post.order('id DESC').limit(20).unscope(:order) = Post.limit(20)
Post.order('id DESC').limit(20).unscope(:order, :limit) = Post.all
```
You can additionally unscope specific where clauses. For example:
```ruby
Post.where(:id => 10).limit(1).unscope(:where => :id, :limit).order('id DESC') = Post.order('id DESC')
```
### `only`
You can also override conditions using the `only` method. For example:
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册