From c879649a733d982fba9e70f5a280d13636b67c37 Mon Sep 17 00:00:00 2001 From: Eugene Kenny Date: Sun, 18 Jun 2017 15:37:06 +0100 Subject: Improve the performance of writing attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using a similar approach to 08576b94ad4f19dfc368619d7751e211d23dcad8, this change adds a new internal `_write_attribute` method which bypasses the code that checks for attribute aliases and custom primary keys. We can use this method instead of `write_attribute` when we know that we have the name of the actual column to be updated and not an alias. This makes writing an attribute with `attribute=` about 18% faster. Benchmark: ``` begin require "bundler/inline" rescue LoadError => e $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" raise e end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" gem "arel", github: "rails/arel" gem "sqlite3" gem "benchmark-ips" end require "active_record" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Schema.define do create_table :posts, force: true do |t| end end class Post < ActiveRecord::Base end post = Post.new(id: 1) Benchmark.ips do |x| x.report("attribute=") { post.id = post.id + 1 } end ``` Before: Warming up -------------------------------------- attribute= 25.889k i/100ms Calculating ------------------------------------- attribute= 290.946k (± 3.1%) i/s - 1.476M in 5.077036s After: Warming up -------------------------------------- attribute= 30.056k i/100ms Calculating ------------------------------------- attribute= 345.088k (± 4.8%) i/s - 1.743M in 5.064264s --- .../lib/active_record/attribute_methods/primary_key.rb | 2 +- activerecord/lib/active_record/attribute_methods/write.rb | 14 ++++++++++---- activerecord/lib/active_record/inheritance.rb | 2 +- activerecord/lib/active_record/timestamp.rb | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index b9b2acff37..081aad434d 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -21,7 +21,7 @@ module ActiveRecord # Sets the primary key value. def id=(value) sync_with_transaction_state - write_attribute(self.class.primary_key, value) if self.class.primary_key + _write_attribute(self.class.primary_key, value) if self.class.primary_key end # Queries the primary key value. diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 75c5a1a600..d7ddd35dff 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,7 +17,7 @@ module ActiveRecord generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__#{safe_name}=(value) name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) + _write_attribute(name, value) end alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= undef_method :__temp__#{safe_name}= @@ -36,8 +36,7 @@ module ActiveRecord end name = self.class.primary_key if name == "id".freeze && self.class.primary_key - @attributes.write_from_user(name, value) - value + _write_attribute(name, value) end def raw_write_attribute(attr_name, value) # :nodoc: @@ -46,10 +45,17 @@ module ActiveRecord value end + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the write_attribute API + def _write_attribute(attr_name, value) # :nodoc: + @attributes.write_from_user(attr_name.to_s, value) + value + end + private # Handle *= for method_missing. def attribute=(attribute_name, value) - write_attribute(attribute_name, value) + _write_attribute(attribute_name, value) end end end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 5776807507..1753322274 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -245,7 +245,7 @@ module ActiveRecord def ensure_proper_type klass = self.class if klass.finder_needs_type_condition? - write_attribute(klass.inheritance_column, klass.sti_name) + _write_attribute(klass.inheritance_column, klass.sti_name) end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 55f3a194a9..ae50bc6bc1 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -86,7 +86,7 @@ module ActiveRecord all_timestamp_attributes_in_model.each do |column| if !attribute_present?(column) - write_attribute(column, current_time) + _write_attribute(column, current_time) end end end @@ -100,7 +100,7 @@ module ActiveRecord timestamp_attributes_for_update_in_model.each do |column| next if will_save_change_to_attribute?(column) - write_attribute(column, current_time) + _write_attribute(column, current_time) end end super(*args) -- cgit v1.2.3 From dff37ff613d12201c2b770dbc61b125792c7bb6e Mon Sep 17 00:00:00 2001 From: Eugene Kenny Date: Fri, 7 Jul 2017 00:53:02 +0100 Subject: Rename raw_write_attribute to write_attribute_without_type_cast This name more accurately describes what the method does, and also disambiguates it from `_write_attribute`, which ignores aliases. We can also make the method private, since it's not public API and only called from one place - `update_columns` - without an explicit receiver. --- activerecord/lib/active_record/attribute_methods/dirty.rb | 2 +- activerecord/lib/active_record/attribute_methods/write.rb | 12 ++++++------ activerecord/lib/active_record/persistence.rb | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 76987fb8f4..7bf5c7485f 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -80,7 +80,7 @@ module ActiveRecord clear_mutation_trackers end - def raw_write_attribute(attr_name, *) + def write_attribute_without_type_cast(attr_name, *) result = super clear_attribute_change(attr_name) result diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index d7ddd35dff..54b673c72e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -39,12 +39,6 @@ module ActiveRecord _write_attribute(name, value) end - def raw_write_attribute(attr_name, value) # :nodoc: - name = attr_name.to_s - @attributes.write_cast_value(name, value) - value - end - # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the write_attribute API def _write_attribute(attr_name, value) # :nodoc: @@ -53,6 +47,12 @@ module ActiveRecord end private + def write_attribute_without_type_cast(attr_name, value) + name = attr_name.to_s + @attributes.write_cast_value(name, value) + value + end + # Handle *= for method_missing. def attribute=(attribute_name, value) _write_attribute(attribute_name, value) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index f652c7c3a1..669c838405 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -333,7 +333,7 @@ module ActiveRecord updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes) attributes.each do |k, v| - raw_write_attribute(k, v) + write_attribute_without_type_cast(k, v) end updated_count == 1 -- cgit v1.2.3