Optimize ActiveRecord::AttributeMethods::PrimaryKey

Fix: https://github.com/rails/rails/pull/49798

Having to check on every access whether the model has a composite
primary key adds a small but non-negligible overhead, even though
CPK is envisoned as the exception, not the rule.

By moving all the CPK support in a dedicated module, we can late-include
it when a CPK is defined on a model. This is essentially a "de-optimization"
phase.
This commit is contained in:
Jean Boussier 2023-10-27 10:21:11 +02:00
parent 392e5efb71
commit f56b31fe92
4 changed files with 112 additions and 56 deletions

View File

@ -128,6 +128,8 @@ module ActiveRecord
module AttributeMethods
extend ActiveSupport::Autoload
autoload :CompositePrimaryKey
eager_autoload do
autoload :BeforeTypeCast
autoload :Dirty

View File

@ -0,0 +1,84 @@
# frozen_string_literal: true
module ActiveRecord
module AttributeMethods
module CompositePrimaryKey # :nodoc:
# Returns the primary key column's value. If the primary key is composite,
# returns an array of the primary key column values.
def id
if self.class.composite_primary_key?
@primary_key.map { |pk| _read_attribute(pk) }
else
super
end
end
def primary_key_values_present? # :nodoc:
if self.class.composite_primary_key?
id.all?
else
super
end
end
# Sets the primary key column's value. If the primary key is composite,
# raises TypeError when the set value not enumerable.
def id=(value)
if self.class.composite_primary_key?
raise TypeError, "Expected value matching #{self.class.primary_key.inspect}, got #{value.inspect}." unless value.is_a?(Enumerable)
@primary_key.zip(value) { |attr, value| _write_attribute(attr, value) }
else
super
end
end
# Queries the primary key column's value. If the primary key is composite,
# all primary key column values must be queryable.
def id?
if self.class.composite_primary_key?
@primary_key.all? { |col| _query_attribute(col) }
else
super
end
end
# Returns the primary key column's value before type cast. If the primary key is composite,
# returns an array of primary key column values before type cast.
def id_before_type_cast
if self.class.composite_primary_key?
@primary_key.map { |col| attribute_before_type_cast(col) }
else
super
end
end
# Returns the primary key column's previous value. If the primary key is composite,
# returns an array of primary key column previous values.
def id_was
if self.class.composite_primary_key?
@primary_key.map { |col| attribute_was(col) }
else
super
end
end
# Returns the primary key column's value from the database. If the primary key is composite,
# returns an array of primary key column values from database.
def id_in_database
if self.class.composite_primary_key?
@primary_key.map { |col| attribute_in_database(col) }
else
super
end
end
def id_for_database # :nodoc:
if self.class.composite_primary_key?
@primary_key.map { |col| @attributes[col].value_for_database }
else
super
end
end
end
end
end

View File

@ -18,74 +18,45 @@ module ActiveRecord
# Returns the primary key column's value. If the primary key is composite,
# returns an array of the primary key column values.
def id
return _read_attribute(@primary_key) unless @primary_key.is_a?(Array)
@primary_key.map { |pk| _read_attribute(pk) }
_read_attribute(@primary_key)
end
def primary_key_values_present? # :nodoc:
return id.all? if self.class.composite_primary_key?
!!id
end
# Sets the primary key column's value. If the primary key is composite,
# raises TypeError when the set value not enumerable.
def id=(value)
if self.class.composite_primary_key?
raise TypeError, "Expected value matching #{self.class.primary_key.inspect}, got #{value.inspect}." unless value.is_a?(Enumerable)
@primary_key.zip(value) { |attr, value| _write_attribute(attr, value) }
else
_write_attribute(@primary_key, value)
end
_write_attribute(@primary_key, value)
end
# Queries the primary key column's value. If the primary key is composite,
# all primary key column values must be queryable.
def id?
if self.class.composite_primary_key?
@primary_key.all? { |col| _query_attribute(col) }
else
_query_attribute(@primary_key)
end
_query_attribute(@primary_key)
end
# Returns the primary key column's value before type cast. If the primary key is composite,
# returns an array of primary key column values before type cast.
def id_before_type_cast
if self.class.composite_primary_key?
@primary_key.map { |col| attribute_before_type_cast(col) }
else
attribute_before_type_cast(@primary_key)
end
attribute_before_type_cast(@primary_key)
end
# Returns the primary key column's previous value. If the primary key is composite,
# returns an array of primary key column previous values.
def id_was
if self.class.composite_primary_key?
@primary_key.map { |col| attribute_was(col) }
else
attribute_was(@primary_key)
end
attribute_was(@primary_key)
end
# Returns the primary key column's value from the database. If the primary key is composite,
# returns an array of primary key column values from database.
def id_in_database
if self.class.composite_primary_key?
@primary_key.map { |col| attribute_in_database(col) }
else
attribute_in_database(@primary_key)
end
attribute_in_database(@primary_key)
end
def id_for_database # :nodoc:
if self.class.composite_primary_key?
@primary_key.map { |col| @attributes[col].value_for_database }
else
@attributes[@primary_key].value_for_database
end
@attributes[@primary_key].value_for_database
end
private
@ -109,14 +80,13 @@ module ActiveRecord
# Overwriting will negate any effect of the +primary_key_prefix_type+
# setting, though.
def primary_key
if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
@primary_key = reset_primary_key
end
reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
@primary_key
end
def composite_primary_key? # :nodoc:
primary_key.is_a?(Array)
reset_primary_key if PRIMARY_KEY_NOT_SET.equal?(@primary_key)
@composite_primary_key
end
# Returns a quoted version of the primary key name, used to construct
@ -138,12 +108,10 @@ module ActiveRecord
base_name.foreign_key(false)
elsif base_name && primary_key_prefix_type == :table_name_with_underscore
base_name.foreign_key
elsif ActiveRecord::Base != self && table_exists?
connection.schema_cache.primary_keys(table_name)
else
if ActiveRecord::Base != self && table_exists?
connection.schema_cache.primary_keys(table_name)
else
"id"
end
"id"
end
end
@ -163,25 +131,25 @@ module ActiveRecord
#
# Project.primary_key # => "foo_id"
def primary_key=(value)
@primary_key = derive_primary_key(value)
@primary_key = if value.is_a?(Array)
@composite_primary_key = true
include CompositePrimaryKey
@primary_key = value.map { |v| -v.to_s }.freeze
elsif value
-value.to_s
end
@quoted_primary_key = nil
@attributes_builder = nil
end
private
def derive_primary_key(value)
return unless value
return -value.to_s unless value.is_a?(Array)
value.map { |v| -v.to_s }.freeze
end
def inherited(base)
super
base.class_eval do
@primary_key = PRIMARY_KEY_NOT_SET
@composite_primary_key = false
@quoted_primary_key = nil
@attributes_builder = nil
end
end
end

View File

@ -131,8 +131,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
test "caching a nil primary key" do
klass = Class.new(Minimalistic)
assert_called(klass, :reset_primary_key, returns: nil) do
2.times { klass.primary_key }
klass.primary_key # warm once
assert_not_called(klass, :reset_primary_key) do
klass.primary_key
end
end