rails/activemodel
Jonathan Hefner 98aa59a7f6 Avoid value_for_database if attribute not updated
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
  ```
2022-10-20 15:39:10 -05:00
..
bin Use frozen string literal in activemodel/ 2017-07-16 20:11:16 +03:00
lib Avoid value_for_database if attribute not updated 2022-10-20 15:39:10 -05:00
test Merge pull request #46231 from jonathanhefner/active_model-memoize-value_for_database 2022-10-16 17:13:21 -05:00
CHANGELOG.md Avoid double cast in types that only override cast 2022-10-16 16:06:16 -05:00
MIT-LICENSE Bump license years to 2022 [ci-skip] 2022-01-01 15:22:15 +09:00
README.rdoc Introduce ActiveModel::API 2021-09-15 18:24:47 +02:00
Rakefile Load framework test files in deterministic order 2019-12-16 16:55:06 +00:00
activemodel.gemspec Fix gemspec 2021-11-15 21:06:21 +00:00

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