Skip to content

Commit 8e383fd

Browse files
Avoid double type cast when serializing attributes
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 ```
1 parent 24980fb commit 8e383fd

30 files changed

+217
-0
lines changed

activemodel/lib/active_model/attribute.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ def type_cast(value)
179179
type.cast(value)
180180
end
181181

182+
def value_for_database
183+
Type::SerializeCastValue.serialize(type, value)
184+
end
185+
182186
def came_from_user?
183187
!type.value_constructed_by_mass_assignment?(value_before_type_cast)
184188
end

activemodel/lib/active_model/type.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "active_model/type/helpers"
4+
require "active_model/type/serialize_cast_value"
45
require "active_model/type/value"
56

67
require "active_model/type/big_integer"

activemodel/lib/active_model/type/big_integer.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ module Type
2121
# All casting and serialization are performed in the same way as the
2222
# standard ActiveModel::Type::Integer type.
2323
class BigInteger < Integer
24+
include SerializeCastValue
25+
26+
def serialize_cast_value(value) # :nodoc:
27+
value
28+
end
29+
2430
private
2531
def max_value
2632
::Float::INFINITY

activemodel/lib/active_model/type/boolean.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ module Type
1010
# - Empty strings are coerced to +nil+.
1111
# - All other values will be coerced to +true+.
1212
class Boolean < Value
13+
include SerializeCastValue
14+
1315
FALSE_VALUES = [
1416
false, 0,
1517
"0", :"0",

activemodel/lib/active_model/type/date.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ module Type
2222
# String values are parsed using the ISO 8601 date format. Any other values
2323
# are cast using their +to_date+ method, if it exists.
2424
class Date < Value
25+
include SerializeCastValue
2526
include Helpers::Timezone
2627
include Helpers::AcceptsMultiparameterTime.new
2728

activemodel/lib/active_model/type/date_time.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ module Type
3838
# attribute :start, :datetime, precision: 4
3939
# end
4040
class DateTime < Value
41+
include SerializeCastValue
4142
include Helpers::Timezone
4243
include Helpers::TimeValue
4344
include Helpers::AcceptsMultiparameterTime.new(
@@ -48,6 +49,8 @@ def type
4849
:datetime
4950
end
5051

52+
alias :serialize_cast_value :serialize_time_value # :nodoc:
53+
5154
private
5255
def cast_value(value)
5356
return apply_seconds_precision(value) unless value.is_a?(::String)

activemodel/lib/active_model/type/decimal.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module Type
3232
# attribute :weight, :decimal, precision: 24
3333
# end
3434
class Decimal < Value
35+
include SerializeCastValue
3536
include Helpers::Numeric
3637
BIGDECIMAL_PRECISION = 18
3738

activemodel/lib/active_model/type/float.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module Type
2626
# - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
2727
# - <tt>"NaN"</tt> is cast to <tt>Float::NAN</tt>.
2828
class Float < Value
29+
include SerializeCastValue
2930
include Helpers::Numeric
3031

3132
def type

activemodel/lib/active_model/type/helpers/time_value.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def serialize(value)
2020

2121
value
2222
end
23+
alias :serialize_time_value :serialize
2324

2425
def apply_seconds_precision(value)
2526
return value unless precision && value.respond_to?(:nsec)

activemodel/lib/active_model/type/immutable_string.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ module Type
3333
#
3434
# person.active # => "aye"
3535
class ImmutableString < Value
36+
include SerializeCastValue
37+
3638
def initialize(**args)
3739
@true = -(args.delete(:true)&.to_s || "t")
3840
@false = -(args.delete(:false)&.to_s || "f")

0 commit comments

Comments
 (0)