mirror of https://github.com/rails/rails
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:
parent
392e5efb71
commit
f56b31fe92
|
@ -128,6 +128,8 @@ module ActiveRecord
|
|||
module AttributeMethods
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :CompositePrimaryKey
|
||||
|
||||
eager_autoload do
|
||||
autoload :BeforeTypeCast
|
||||
autoload :Dirty
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue