8e383fdad6
Most model attribute types try to cast a given value before serializing it. This allows uncast values to be passed to finder methods and still be serialized appropriately. However, when persisting a model, this cast is unnecessary because the value will already have been cast by `ActiveModel::Attribute#value`. To eliminate the overhead of a 2nd cast, this commit introduces a `ActiveModel::Type::SerializeCastValue` module. Types can include this module, and their `serialize_cast_value` method will be called instead of `serialize` when serializing an already-cast value. To preserve existing behavior of any user types that subclass Rails' types, `serialize_after_cast` will only be called if the type itself (not a superclass) includes `ActiveModel::Type::SerializeCastValue`. This also applies to type decorators implemented via `DelegateClass`. Benchmark script: ```ruby require "active_model" require "benchmark/ips" class ActiveModel::Attribute alias baseline_value_for_database value_for_database end VALUES = { my_big_integer: "123456", my_boolean: "true", my_date: "1999-12-31", my_datetime: "1999-12-31 12:34:56 UTC", my_decimal: "123.456", my_float: "123.456", my_immutable_string: "abcdef", my_integer: "123456", my_string: "abcdef", my_time: "1999-12-31T12:34:56.789-10:00", } TYPES = VALUES.to_h { |name, value| [name, name.to_s.delete_prefix("my_").to_sym] } class MyModel include ActiveModel::API include ActiveModel::Attributes TYPES.each do |name, type| attribute name, type end end TYPES.each do |name, type| $attribute_set ||= MyModel.new(VALUES).instance_variable_get(:@attributes) attribute = $attribute_set[name.to_s] puts "=" * 72 Benchmark.ips do |x| x.report("#{type} before") { attribute.baseline_value_for_database } x.report("#{type} after") { attribute.value_for_database } x.compare! end end ``` Benchmark results: ``` ======================================================================== Warming up -------------------------------------- big_integer before 100.417k i/100ms big_integer after 260.375k i/100ms Calculating ------------------------------------- big_integer before 1.005M (± 1.0%) i/s - 5.121M in 5.096498s big_integer after 2.630M (± 1.0%) i/s - 13.279M in 5.050387s Comparison: big_integer after: 2629583.6 i/s big_integer before: 1004961.2 i/s - 2.62x (± 0.00) slower ======================================================================== Warming up -------------------------------------- boolean before 230.663k i/100ms boolean after 299.262k i/100ms Calculating ------------------------------------- boolean before 2.313M (± 0.7%) i/s - 11.764M in 5.085925s boolean after 3.037M (± 0.6%) i/s - 15.262M in 5.026280s Comparison: boolean after: 3036640.8 i/s boolean before: 2313127.8 i/s - 1.31x (± 0.00) slower ======================================================================== Warming up -------------------------------------- date before 148.821k i/100ms date after 298.939k i/100ms Calculating ------------------------------------- date before 1.486M (± 0.6%) i/s - 7.441M in 5.006091s date after 2.963M (± 0.8%) i/s - 14.947M in 5.045651s Comparison: date after: 2962535.3 i/s date before: 1486459.4 i/s - 1.99x (± 0.00) slower ======================================================================== Warming up -------------------------------------- datetime before 92.818k i/100ms datetime after 136.710k i/100ms Calculating ------------------------------------- datetime before 920.236k (± 0.6%) i/s - 4.641M in 5.043355s datetime after 1.366M (± 0.8%) i/s - 6.836M in 5.003307s Comparison: datetime after: 1366294.1 i/s datetime before: 920236.1 i/s - 1.48x (± 0.00) slower ======================================================================== Warming up -------------------------------------- decimal before 50.194k i/100ms decimal after 298.674k i/100ms Calculating ------------------------------------- decimal before 494.141k (± 1.4%) i/s - 2.510M in 5.079995s decimal after 3.015M (± 1.0%) i/s - 15.232M in 5.052929s Comparison: decimal after: 3014901.3 i/s decimal before: 494141.2 i/s - 6.10x (± 0.00) slower ======================================================================== Warming up -------------------------------------- float before 217.547k i/100ms float after 298.106k i/100ms Calculating ------------------------------------- float before 2.157M (± 0.8%) i/s - 10.877M in 5.043292s float after 2.991M (± 0.6%) i/s - 15.203M in 5.082806s Comparison: float after: 2991262.8 i/s float before: 2156940.2 i/s - 1.39x (± 0.00) slower ======================================================================== Warming up -------------------------------------- immutable_string before 163.287k i/100ms immutable_string after 298.245k i/100ms Calculating ------------------------------------- immutable_string before 1.652M (± 0.7%) i/s - 8.328M in 5.040855s immutable_string after 3.022M (± 0.9%) i/s - 15.210M in 5.033151s Comparison: immutable_string after: 3022313.3 i/s immutable_string before: 1652121.7 i/s - 1.83x (± 0.00) slower ======================================================================== Warming up -------------------------------------- integer before 115.383k i/100ms integer after 159.702k i/100ms Calculating ------------------------------------- integer before 1.132M (± 0.8%) i/s - 5.769M in 5.095041s integer after 1.641M (± 0.5%) i/s - 8.305M in 5.061893s Comparison: integer after: 1640635.8 i/s integer before: 1132381.5 i/s - 1.45x (± 0.00) slower ======================================================================== Warming up -------------------------------------- string before 163.061k i/100ms string after 299.885k i/100ms Calculating ------------------------------------- string before 1.659M (± 0.7%) i/s - 8.316M in 5.012609s string after 2.999M (± 0.6%) i/s - 15.294M in 5.100008s Comparison: string after: 2998956.0 i/s string before: 1659115.6 i/s - 1.81x (± 0.00) slower ======================================================================== Warming up -------------------------------------- time before 98.250k i/100ms time after 133.463k i/100ms Calculating ------------------------------------- time before 987.771k (± 0.7%) i/s - 5.011M in 5.073023s time after 1.330M (± 0.5%) i/s - 6.673M in 5.016573s Comparison: time after: 1330253.9 i/s time before: 987771.0 i/s - 1.35x (± 0.00) slower ``` |
||
---|---|---|
.. | ||
bin | ||
lib | ||
test | ||
activemodel.gemspec | ||
CHANGELOG.md | ||
MIT-LICENSE | ||
Rakefile | ||
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