mirror of https://github.com/rails/rails
98aa59a7f6
After a record is saved, `ActiveModel::Attribute#forgetting_assignment` is called on each of its attributes. `forgetting_assignment`, in turn, calls `ActiveModel::Attribute#value_for_database`. If an attribute was not updated, and therefore `value_for_database` was not previously computed, this will involve an unnecessary serialize (and cast, depending on type). And if the attribute was not previously read, it will also involve an unnecessary deserialize (and cast, depending on type). This commit overrides `FromDatabase#forgetting_assignment` to dup the attribute instead of computing `value_for_database` in the case where the attribute was not updated. This can improve the performance for basic types: **Benchmark script** ```ruby # frozen_string_literal: true require "benchmark/ips" ActiveModel::Attribute.alias_method :baseline_forgetting_assignment, :forgetting_assignment { big_integer: 123456, boolean: 1, date: "1999-12-31", datetime: "1999-12-31 12:34:56.789011", decimal: 123.456, float: 123.456, immutable_string: "abcdef", integer: 123456, string: "abcdef", time: "1999-12-31 12:34:56.789011", }.each do |type_name, value_before_type_cast| puts "=== #{type_name} ".ljust(70, "=") type = ActiveModel::Type.lookup(type_name) Benchmark.ips do |x| x.report("before") do attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type) attribute.baseline_forgetting_assignment end x.report("after") do attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type) attribute.forgetting_assignment end x.compare! end Benchmark.ips do |x| x.report("before w/ read") do attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type) attribute.value attribute.baseline_forgetting_assignment end x.report("after w/ read") do attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type) attribute.value attribute.forgetting_assignment end x.compare! end end ``` **Results** ``` === big_integer ====================================================== Warming up -------------------------------------- before 35.830k i/100ms after 99.520k i/100ms Calculating ------------------------------------- before 356.010k (± 1.0%) i/s - 1.792M in 5.032655s after 989.674k (± 1.2%) i/s - 4.976M in 5.028638s Comparison: after: 989674.2 i/s before: 356010.4 i/s - 2.78x (± 0.00) slower Warming up -------------------------------------- before w/ read 35.030k i/100ms after w/ read 56.005k i/100ms Calculating ------------------------------------- before w/ read 349.509k (± 1.3%) i/s - 1.752M in 5.012188s after w/ read 558.953k (± 1.2%) i/s - 2.800M in 5.010511s Comparison: after w/ read: 558953.4 i/s before w/ read: 349508.6 i/s - 1.60x (± 0.00) slower === boolean ========================================================== Warming up -------------------------------------- before 31.989k i/100ms after 99.698k i/100ms Calculating ------------------------------------- before 319.969k (± 1.0%) i/s - 1.631M in 5.099207s after 994.759k (± 1.1%) i/s - 4.985M in 5.011775s Comparison: after: 994758.6 i/s before: 319969.3 i/s - 3.11x (± 0.00) slower Warming up -------------------------------------- before w/ read 31.090k i/100ms after w/ read 33.306k i/100ms Calculating ------------------------------------- before w/ read 308.859k (± 1.1%) i/s - 1.554M in 5.033639s after w/ read 332.726k (± 1.1%) i/s - 1.665M in 5.005669s Comparison: after w/ read: 332726.2 i/s before w/ read: 308858.6 i/s - 1.08x (± 0.00) slower === date ============================================================= Warming up -------------------------------------- before 14.466k i/100ms after 98.451k i/100ms Calculating ------------------------------------- before 145.594k (± 1.4%) i/s - 737.766k in 5.068354s after 990.526k (± 1.1%) i/s - 5.021M in 5.069638s Comparison: after: 990526.4 i/s before: 145593.9 i/s - 6.80x (± 0.00) slower Warming up -------------------------------------- before w/ read 14.130k i/100ms after w/ read 14.702k i/100ms Calculating ------------------------------------- before w/ read 141.887k (± 1.3%) i/s - 720.630k in 5.079719s after w/ read 148.244k (± 1.0%) i/s - 749.802k in 5.058433s Comparison: after w/ read: 148244.1 i/s before w/ read: 141886.8 i/s - 1.04x (± 0.00) slower === datetime ========================================================= Warming up -------------------------------------- before 9.484k i/100ms after 97.889k i/100ms Calculating ------------------------------------- before 95.640k (± 1.3%) i/s - 483.684k in 5.058190s after 988.827k (± 1.1%) i/s - 4.992M in 5.049376s Comparison: after: 988827.4 i/s before: 95639.7 i/s - 10.34x (± 0.00) slower Warming up -------------------------------------- before w/ read 9.440k i/100ms after w/ read 10.479k i/100ms Calculating ------------------------------------- before w/ read 94.420k (± 1.1%) i/s - 481.440k in 5.099530s after w/ read 105.935k (± 1.0%) i/s - 534.429k in 5.045377s Comparison: after w/ read: 105935.2 i/s before w/ read: 94419.6 i/s - 1.12x (± 0.00) slower === decimal ========================================================== Warming up -------------------------------------- before 12.877k i/100ms after 98.081k i/100ms Calculating ------------------------------------- before 127.627k (± 1.4%) i/s - 643.850k in 5.045749s after 990.178k (± 0.9%) i/s - 5.002M in 5.052175s Comparison: after: 990178.0 i/s before: 127627.5 i/s - 7.76x (± 0.00) slower Warming up -------------------------------------- before w/ read 12.640k i/100ms after w/ read 19.739k i/100ms Calculating ------------------------------------- before w/ read 124.933k (± 1.4%) i/s - 632.000k in 5.059694s after w/ read 200.110k (± 1.0%) i/s - 1.007M in 5.031169s Comparison: after w/ read: 200110.3 i/s before w/ read: 124932.8 i/s - 1.60x (± 0.00) slower === float ============================================================ Warming up -------------------------------------- before 45.424k i/100ms after 99.512k i/100ms Calculating ------------------------------------- before 449.861k (± 1.0%) i/s - 2.271M in 5.049231s after 991.130k (± 1.1%) i/s - 4.976M in 5.020705s Comparison: after: 991130.0 i/s before: 449861.3 i/s - 2.20x (± 0.00) slower Warming up -------------------------------------- before w/ read 43.283k i/100ms after w/ read 42.888k i/100ms Calculating ------------------------------------- before w/ read 431.630k (± 1.0%) i/s - 2.164M in 5.014423s after w/ read 429.631k (± 1.0%) i/s - 2.187M in 5.091609s Comparison: before w/ read: 431630.0 i/s after w/ read: 429631.0 i/s - same-ish: difference falls within error === immutable_string ================================================= Warming up -------------------------------------- before 36.820k i/100ms after 97.812k i/100ms Calculating ------------------------------------- before 369.412k (± 1.1%) i/s - 1.878M in 5.083860s after 985.345k (± 1.1%) i/s - 4.988M in 5.063224s Comparison: after: 985345.0 i/s before: 369411.8 i/s - 2.67x (± 0.00) slower Warming up -------------------------------------- before w/ read 36.082k i/100ms after w/ read 37.250k i/100ms Calculating ------------------------------------- before w/ read 361.134k (± 1.0%) i/s - 1.840M in 5.096085s after w/ read 369.471k (± 0.8%) i/s - 1.862M in 5.041349s Comparison: after w/ read: 369471.0 i/s before w/ read: 361133.6 i/s - 1.02x (± 0.00) slower === integer ========================================================== Warming up -------------------------------------- before 35.763k i/100ms after 98.444k i/100ms Calculating ------------------------------------- before 358.171k (± 1.1%) i/s - 1.824M in 5.092870s after 995.138k (± 1.0%) i/s - 5.021M in 5.045729s Comparison: after: 995137.6 i/s before: 358171.4 i/s - 2.78x (± 0.00) slower Warming up -------------------------------------- before w/ read 34.534k i/100ms after w/ read 53.971k i/100ms Calculating ------------------------------------- before w/ read 345.029k (± 1.0%) i/s - 1.727M in 5.005030s after w/ read 537.373k (± 0.9%) i/s - 2.699M in 5.022178s Comparison: after w/ read: 537373.4 i/s before w/ read: 345029.3 i/s - 1.56x (± 0.00) slower === string =========================================================== Warming up -------------------------------------- before 33.882k i/100ms after 97.193k i/100ms Calculating ------------------------------------- before 339.174k (± 1.1%) i/s - 1.728M in 5.095272s after 984.544k (± 0.9%) i/s - 4.957M in 5.035057s Comparison: after: 984543.5 i/s before: 339174.0 i/s - 2.90x (± 0.00) slower Warming up -------------------------------------- before w/ read 32.897k i/100ms after w/ read 31.767k i/100ms Calculating ------------------------------------- before w/ read 329.911k (± 0.8%) i/s - 1.678M in 5.085756s after w/ read 320.010k (± 0.6%) i/s - 1.620M in 5.062867s Comparison: before w/ read: 329910.6 i/s after w/ read: 320010.1 i/s - 1.03x (± 0.00) slower === time ============================================================= Warming up -------------------------------------- before 7.461k i/100ms after 97.467k i/100ms Calculating ------------------------------------- before 74.854k (± 1.5%) i/s - 380.511k in 5.084475s after 986.169k (± 0.9%) i/s - 4.971M in 5.040923s Comparison: after: 986168.8 i/s before: 74854.5 i/s - 13.17x (± 0.00) slower Warming up -------------------------------------- before w/ read 7.438k i/100ms after w/ read 8.046k i/100ms Calculating ------------------------------------- before w/ read 73.436k (± 1.5%) i/s - 371.900k in 5.065491s after w/ read 79.752k (± 1.5%) i/s - 402.300k in 5.045612s Comparison: after w/ read: 79751.7 i/s before w/ read: 73435.8 i/s - 1.09x (± 0.00) slower ``` And, notably, this avoids potentially expensive serialize and deserialize calls for non-basic types, such as with encrypted attributes, when the attribute has not been read or assigned: **Before** ```irb irb> Post.encrypts :body, encryptor: MyLoggingEncryptor.new irb> post = Post.create!(title: "untitled", body: "The Body") ! encrypting "The Body" TRANSACTION (0.1ms) begin transaction Post Create (0.6ms) INSERT INTO "posts" ... ! decrypted "The Body" TRANSACTION (120.2ms) commit transaction irb> post.update!(title: "The Title") TRANSACTION (0.1ms) begin transaction Post Update (0.7ms) UPDATE "posts" ... ! decrypted "The Body" ! encrypting "The Body" TRANSACTION (92.1ms) commit transaction ``` **After** ```irb irb> Post.encrypts :body, encryptor: MyLoggingEncryptor.new irb> post = Post.create!(title: "untitled", body: "The Body") ! encrypting "The Body" TRANSACTION (0.1ms) begin transaction Post Create (0.7ms) INSERT INTO "posts" ... ! decrypted "The Body" TRANSACTION (103.7ms) commit transaction irb> post.update!(title: "The Title") TRANSACTION (0.1ms) begin transaction Post Update (0.7ms) UPDATE "posts" ... TRANSACTION (91.2ms) commit transaction ``` |
||
---|---|---|
.. | ||
bin | ||
lib | ||
test | ||
CHANGELOG.md | ||
MIT-LICENSE | ||
README.rdoc | ||
Rakefile | ||
activemodel.gemspec |
README.rdoc
= Active Model -- model interfaces for Rails Active Model provides a known set of interfaces for usage in model classes. They allow for Action Pack helpers to interact with non-Active Record models, for example. Active Model also helps with building custom ORMs for use outside of the Rails framework. You can read more about Active Model in the {Active Model Basics}[https://edgeguides.rubyonrails.org/active_model_basics.html] guide. Prior to Rails 3.0, if a plugin or gem developer wanted to have an object interact with Action Pack helpers, it was required to either copy chunks of code from Rails, or monkey patch entire helpers to make them handle objects that did not exactly conform to the Active Record interface. This would result in code duplication and fragile applications that broke on upgrades. Active Model solves this by defining an explicit API. You can read more about the API in <tt>ActiveModel::Lint::Tests</tt>. Active Model provides a default module that implements the basic API required to integrate with Action Pack out of the box: <tt>ActiveModel::API</tt>. class Person include ActiveModel::API attr_accessor :name, :age validates_presence_of :name end person = Person.new(name: 'bob', age: '18') person.name # => 'bob' person.age # => '18' person.valid? # => true It includes model name introspections, conversions, translations and validations, resulting in a class suitable to be used with Action Pack. See <tt>ActiveModel::API</tt> for more examples. Active Model also provides the following functionality to have ORM-like behavior out of the box: * Add attribute magic to objects class Person include ActiveModel::AttributeMethods attribute_method_prefix 'clear_' define_attribute_methods :name, :age attr_accessor :name, :age def clear_attribute(attr) send("#{attr}=", nil) end end person = Person.new person.clear_name person.clear_age {Learn more}[link:classes/ActiveModel/AttributeMethods.html] * Callbacks for certain operations class Person extend ActiveModel::Callbacks define_model_callbacks :create def create run_callbacks :create do # Your create action methods here end end end This generates +before_create+, +around_create+ and +after_create+ class methods that wrap your create method. {Learn more}[link:classes/ActiveModel/Callbacks.html] * Tracking value changes class Person include ActiveModel::Dirty define_attribute_methods :name def name @name end def name=(val) name_will_change! unless val == @name @name = val end def save # do persistence work changes_applied end end person = Person.new person.name # => nil person.changed? # => false person.name = 'bob' person.changed? # => true person.changed # => ['name'] person.changes # => { 'name' => [nil, 'bob'] } person.save person.name = 'robert' person.save person.previous_changes # => {'name' => ['bob, 'robert']} {Learn more}[link:classes/ActiveModel/Dirty.html] * Adding +errors+ interface to objects Exposing error messages allows objects to interact with Action Pack helpers seamlessly. class Person def initialize @errors = ActiveModel::Errors.new(self) end attr_accessor :name attr_reader :errors def validate! errors.add(:name, "cannot be nil") if name.nil? end def self.human_attribute_name(attr, options = {}) "Name" end end person = Person.new person.name = nil person.validate! person.errors.full_messages # => ["Name cannot be nil"] {Learn more}[link:classes/ActiveModel/Errors.html] * Model name introspection class NamedPerson extend ActiveModel::Naming end NamedPerson.model_name.name # => "NamedPerson" NamedPerson.model_name.human # => "Named person" {Learn more}[link:classes/ActiveModel/Naming.html] * Making objects serializable <tt>ActiveModel::Serialization</tt> provides a standard interface for your object to provide +to_json+ serialization. class SerialPerson include ActiveModel::Serialization attr_accessor :name def attributes {'name' => name} end end s = SerialPerson.new s.serializable_hash # => {"name"=>nil} class SerialPerson include ActiveModel::Serializers::JSON end s = SerialPerson.new s.to_json # => "{\"name\":null}" {Learn more}[link:classes/ActiveModel/Serialization.html] * Internationalization (i18n) support class Person extend ActiveModel::Translation end Person.human_attribute_name('my_attribute') # => "My attribute" {Learn more}[link:classes/ActiveModel/Translation.html] * Validation support class Person include ActiveModel::Validations attr_accessor :first_name, :last_name validates_each :first_name, :last_name do |record, attr, value| record.errors.add attr, "starts with z." if value.start_with?("z") end end person = Person.new person.first_name = 'zoolander' person.valid? # => false {Learn more}[link:classes/ActiveModel/Validations.html] * Custom validators class HasNameValidator < ActiveModel::Validator def validate(record) record.errors.add(:name, "must exist") if record.name.blank? end end class ValidatorPerson include ActiveModel::Validations validates_with HasNameValidator attr_accessor :name end p = ValidatorPerson.new p.valid? # => false p.errors.full_messages # => ["Name must exist"] p.name = "Bob" p.valid? # => true {Learn more}[link:classes/ActiveModel/Validator.html] == Download and installation The latest version of Active Model can be installed with RubyGems: $ gem install activemodel Source code can be downloaded as part of the Rails project on GitHub * https://github.com/rails/rails/tree/main/activemodel == License Active Model is released under the MIT license: * https://opensource.org/licenses/MIT == Support API documentation is at: * https://api.rubyonrails.org Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues Feature requests should be discussed on the rails-core mailing list here: * https://discuss.rubyonrails.org/c/rubyonrails-core