Skip to content

Changed ActiveRecord::Relation#update behavior so that it will work on Relation objects without giving id #11898

Merged
merged 1 commit into from Jan 2, 2015

9 participants

@prathamesh-sonpatki
Ruby on Rails member

No description provided.

@egilburg egilburg and 1 other commented on an outdated diff Aug 15, 2013
activerecord/lib/active_record/relation.rb
def update(id, attributes)
if id.is_a?(Array)
- id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
+ if attributes.is_a?(Array)
@egilburg
egilburg added a note Aug 15, 2013

This is probably simpler:

        id.map.with_index do |one_id, idx| 
          attrs = attributes.is_a?(Array) ? attributes[idx] : attributes

          update(one_id, attrs)
        end
@prathamesh-sonpatki
Ruby on Rails member

@egilburg Thanks. Updated :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@senny
Ruby on Rails member
senny commented Aug 15, 2013

This implementation issues a single query per record to update. Since all the records are updated with the same condition we can perform the update in a single query. Is there really a need for this PR, when you could use:

Person.where(id: [1,2,3,4]).update_all(last_seen: Time.now)
@egilburg

Isn't update_all being deprecated?

@senny
Ruby on Rails member
senny commented Aug 15, 2013

Not that I know of => https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation.rb#L304

But you need to call it on a Relation, I think it was available at the class level at some point Person.update_all but that is no longer supported.

@prathamesh-sonpatki
Ruby on Rails member

@senny update_all doesn't run callbacks and validations.

@senny
Ruby on Rails member
senny commented Aug 15, 2013

@prathamesh-sonpatki yea right. I just browsed the API and it seems there is no method that acts on the Relation to update and also runs callbacks.

@rafaelfranca @carlosantoniodasilva is there a reason we only have Person.update(ids, values) to update with callbacks and nothing that acts on the Relation?

@prathamesh-sonpatki
Ruby on Rails member

Investigated it a bit. Right now if you try to call update on a relation, ActiveRecord::Relation#update gets called. If we pass correct ids and values it will update the records with callbacks. But when we are dealing with relation, we should not pass ids at all. Just pass the hash of values.

I think we have 3 usecases
1) Update different records with different values - Existing behavior of update
2) Update different records with same values using ids - Implemented in this PR
3) Update different records with same values using relation - Missing

@senny
Ruby on Rails member
senny commented Aug 15, 2013

@prathamesh-sonpatki I'm not sure we need 2) if we have 3). Let's wait for some feedback.

@prathamesh-sonpatki
Ruby on Rails member

@carlosantoniodasilva Any thoughts?

@prathamesh-sonpatki
Ruby on Rails member

any updates on this?

@prathamesh-sonpatki
Ruby on Rails member
@prathamesh-sonpatki
Ruby on Rails member

I have rebased with current master

@rafaelfranca
Ruby on Rails member

I prefer to have 3) and not implement 2).

@prathamesh-sonpatki could you take a look if it is possible to implement 3)?

@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Its possible. i will update soon

@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Sorry for the delay. I have updated. Please take a look

@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Can you see this issue?

@rafaelfranca
Ruby on Rails member

Sure, but I don't want to include this on 4.1, so we will have to wait the final release to merge it

@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca any updates on this issue?

@rafaelfranca rafaelfranca and 2 others commented on an outdated diff Jun 4, 2014
activerecord/lib/active_record/relation.rb
@@ -342,13 +342,19 @@ def update_all(updates)
# # Updates multiple records
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
# Person.update(people.keys, people.values)
- def update(id, attributes)
+ #
+ # # Updates multiple records from the result of a relation
+ # people = Person.where(group: 'expert')
+ # people.update(group: 'masters')
+ def update(id = nil, attributes)
@rafaelfranca
Ruby on Rails member
rafaelfranca added a note Jun 4, 2014

I don't think this will work. If you pass only one argument it will be always id.

@matthewd
Ruby on Rails member
matthewd added a note Jun 4, 2014

I wasn't sure about 1.9, but just checked: it's fine.

@rafaelfranca
Ruby on Rails member
rafaelfranca added a note Jun 4, 2014

You are right. Same with 1.9.

@matthewd
Ruby on Rails member
matthewd added a note Jun 4, 2014

.. that said, I think I'd rather that .update(nil, group: 'masters') continued to fail, instead of suddenly meaning "update all the things".

@rafaelfranca
Ruby on Rails member
rafaelfranca added a note Jun 4, 2014

Yes. This make sense.

@prathamesh-sonpatki
Ruby on Rails member

I guess i did change the existing method rather than adding new because update is best name for this.

@matthewd
Ruby on Rails member
matthewd added a note Jun 4, 2014

I think that's fine. Just use something other than nil as the default value to indicate no argument was passed. (Either a symbol, or a dedicated Object.new stored in a constant... or maybe even false.)

My concern is that if you accidentally pass some_new_object.id, we'll do something very different to what you intended. (Instead of safely raising an error, as we would previously.)

@rafaelfranca
Ruby on Rails member
rafaelfranca added a note Jun 4, 2014

Yes, it is the best name. But .update(nil, group: 'masters') updating all the things can be a huge problem in existing applications upgrading to 4.2. Maybe we should check if id is a hash and trigger this new behaviour.

Also I was thinking. Depending on the number of record your relation return you may have a huge performance issue since it will do a update query for each record.

@rafaelfranca
Ruby on Rails member
rafaelfranca added a note Jun 4, 2014

Ah, yes, maybe defaulting the id value to :all would fix this concern.

@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca For performance issues, should we do find_each instead of to_a ?

@rafaelfranca
Ruby on Rails member
rafaelfranca added a note Jun 4, 2014

We would have an improvement to not load all the record on the memory but the number of queries would be even worse. I think we should only document this possible performance problem and point users to update_all

@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca I updated the PR. Should we also update ActiveRecord getting started guide where we mention update and update_all

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@rafaelfranca
Ruby on Rails member

I really not sure about this. @dhh @jeremy @tenderlove WDYT?

@dhh
Ruby on Rails member
dhh commented Jun 4, 2014
@tenderlove
Ruby on Rails member
@al2o3cr
al2o3cr commented Jun 4, 2014

This seems (IMO) confusingly close to update_all, but with radically different implications for the number of queries (N+1 here, 1 for update_all). Is this something people do often enough that an explicit each loop is a readability problem?

@dhh
Ruby on Rails member
dhh commented Jun 4, 2014
@egilburg
@dhh
Ruby on Rails member
dhh commented Jun 4, 2014
@matthewd
Ruby on Rails member
matthewd commented Jun 4, 2014

So, I think this can go in, to round out:

delete_all : delete :: destroy_all : destroy :: update_all : update

And then we separately consider whether the _all methods might be renamed to something more explicitly different/dangerous. Right?

@egilburg
@egilburg
@al2o3cr
al2o3cr commented Jun 5, 2014

@egilburg I don't think update and update_all should be the same method. Even shoehorning update_columns in makes the abstraction leak. Here's a breakdown of what I'm thinking:

On a single ActiveRecord instance:

# equivalent to current update method on an instance
@some_instance.update({foo: 'bar'}, {validate: true, callbacks: true})

# equivalent to update_columns
# will fail on invalid SQL if foo is not a database column
# should check to see if the columns being updated are marked as readonly
# and raise if so - update_columns currently does this
@some_instance.update({foo: 'bar'}, {validate: false, callbacks: false})

# equivalent to attributes= followed by save(validate: false)
@some_instance.update({foo: 'bar'}, {validate: false, callbacks: true})

# equivalent to... nothing currently available. Does this even make sense?
@some_instance.update({foo: 'bar'}, {validate: true, callbacks: false})

On a Relation:

# equivalent to the update method added in this PR
@some_relation.update({foo: 'bar'}, {validate: true, callbacks: true})

# equivalent to update_all
# will fail on invalid SQL if foo is not a database column
# does not check for readonly attributes
# should also accept a SQL string ('some_column = some_column + 1')
# *or* an array with placeholders (['some_column = MAGIC_SQL_FUNCTION(?)', value])
@some_relation.update({foo: 'bar'}, {validate: false, callbacks: false})

# no current equivalent
@some_relation.update({foo: 'bar'}, {validate: false, callbacks: true})

# equivalent to... nothing currently available. Again, does this even make sense?
@some_relation.update({foo: 'bar'}, {validate: true, callbacks: false})

To me, the significant number of nonsensical argument combinations (for instance, @some_relation.update('some_column = some_column +1', {validate: true, callbacks: true}), or any case with {validates: true, callbacks: false}) indicates that this isn't a single method.

What about maintaining the separation between "loads objects" and "doesn't load objects" but tidying up the names?:

# same as current
# maybe add validate: false as a possible option?
@some_instance.update({foo: 'bar'})

# also the same
@some_instance.update_columns({foo: 'bar'})

# new functionality; is this a thing people do?
# basically equivalent to:
# @some_instance.class.where(id: @some_instance.id).update_all(...)
@some_instance.update_columns('some_column = some_column + 1')

# on relations

# as presented in this PR; maybe add validate: false as an option?
@some_relation.update({foo: 'bar'})

# rename update_all to clarify the relationship with the instance version
# not quite equivalent to @some_relation.to_a.each { |record| record.update_columns(...) }
# because of readonly checking in the instance version
# validate: false would not be meaningful here
@some_relation.update_columns({foo: 'bar'})
@some_relation.update_columns('some_column = some_column + 1')

This collapses the "many methods" problem to exactly two: one that instantiates record(s) / runs callbacks, and one that bypasses the entire mechanism and pushes SQL at the database.

Apologies for the length of this comment; maybe this is a discussion better had on the mailing list?

@rafaelfranca rafaelfranca and 1 other commented on an outdated diff Jun 6, 2014
activerecord/lib/active_record/relation.rb
@@ -346,9 +346,20 @@ def update_all(updates)
# # Updates multiple records
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
# Person.update(people.keys, people.values)
- def update(id, attributes)
+ #
+ # # Updates multiple records from the result of a relation
+ # people = Person.where(group: 'expert')
+ # people.update(group: 'masters')
+ #
+ # Note: Updating large number of records will be time consuming
@rafaelfranca
Ruby on Rails member
rafaelfranca added a note Jun 6, 2014
#   Note: Updating a large number of records will run a
#   UPDATE query for each record, which may cause a performance
#   issue. So if it is not needed to run callbacks for each update, is
#   prefered to use <tt>update_all</tt> for updating all records using
#   a single query.
@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Should we say update_all can be used instead of update_all is preferred

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@rafaelfranca rafaelfranca commented on the diff Jun 6, 2014
activerecord/test/cases/relations_test.rb
@@ -1421,6 +1421,14 @@ def test_update_all_with_joins_and_offset_and_order
assert_equal posts(:welcome), comments(:greetings).post
end
+ def test_update_on_relation
@rafaelfranca
Ruby on Rails member
rafaelfranca added a note Jun 6, 2014

Can we add a test to make sure callbacks are run?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@prathamesh-sonpatki prathamesh-sonpatki commented on an outdated diff Jun 7, 2014
activerecord/test/cases/relations_test.rb
@@ -1421,6 +1421,19 @@ def test_update_all_with_joins_and_offset_and_order
assert_equal posts(:welcome), comments(:greetings).post
end
+ def test_update_on_relation
+ topic1 = Topic.create! title: 'arel', author_name: nil
+ topic2 = Topic.create! title: 'activerecord', author_name: nil
+ topics = Topic.where(id: [topic1.id, topic2.id])
+ topics.update(title: 'adequaterecord')
+
+ assert_equal 'adequaterecord', topic1.reload.title
+ assert_equal 'adequaterecord', topic2.reload.title
+ # Testing that the callbacks have run
+ assert_equal 'David', topic1.reload.author_name
+ assert_equal 'David', topic1.reload.author_name
+ end
@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Added test to make sure callbacks run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@rafaelfranca
Ruby on Rails member

The build is red now. Could you check?

@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Can you restart the build. I fixed the failing test

@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Build is :green_heart:

@dhh
Ruby on Rails member
dhh commented Jun 10, 2014
@rafaelfranca rafaelfranca modified the milestone: 4.2.0, 5.0.0 Aug 18, 2014
@prathamesh-sonpatki prathamesh-sonpatki Allow ActiveRecord::Relation#update to run on result of a relation wi…
…th callbacks and validations

- Right now, there is no method to update multiple records with
  validations and callbacks.
- Changed the behavior of existing `update` method so that when `id`
  attribute is not given and the method is called on an `Relation`
  object, it will execute update for every record of the `Relation` and
  will run validations and callbacks for every record.
- Added test case for validating that the callbacks run when `update` is
  called on a `Relation`.
- Changed test_create_columns_not_equal_attributes test from
  persistence_test to include author_name column on topics table as it
  it used in before_update callback.
- This change introduces performance issues when a large number of
  records are to be updated because it runs UPDATE query for every
  record of the result. The `update_all` method can be used in that case
  if callbacks are not required because it will only run single UPDATE
  for all the records.
5ef713c
@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Rebased with latest master. Can we take a look at this now?

@rafaelfranca rafaelfranca merged commit 5ef713c into rails:master Jan 2, 2015

1 check passed

Details continuous-integration/travis-ci The Travis CI build passed
@prathamesh-sonpatki
Ruby on Rails member

@rafaelfranca Thanks :)

@prathamesh-sonpatki prathamesh-sonpatki deleted the prathamesh-sonpatki:patch-update branch Jan 2, 2015
@rafaelfranca rafaelfranca modified the milestone: 5.0.0 [temp], 5.0.0 Dec 30, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.