From 144e8691cbfb8bba77f18cfe68d5e7fd48887f5e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 28 Sep 2012 17:55:35 +0100 Subject: Support for partial inserts. When inserting new records, only the fields which have been changed from the defaults will actually be included in the INSERT statement. The other fields will be populated by the database. This is more efficient, and also means that it will be safe to remove database columns without getting subsequent errors in running app processes (so long as the code in those processes doesn't contain any references to the removed column). --- activerecord/CHANGELOG.md | 13 +++++++++++ .../lib/active_record/attribute_methods.rb | 10 ++++----- .../lib/active_record/attribute_methods/dirty.rb | 20 ++++++++++++++--- .../abstract/database_statements.rb | 2 +- .../connection_adapters/abstract_mysql_adapter.rb | 8 +++++++ .../active_record/connection_adapters/column.rb | 4 ++++ .../connection_adapters/sqlite3_adapter.rb | 4 ---- activerecord/lib/active_record/persistence.rb | 4 ++-- activerecord/test/cases/dirty_test.rb | 25 ++++++++++++++++++++++ 9 files changed, 75 insertions(+), 15 deletions(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d52d6a7fde..b6ad6f82d8 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,18 @@ ## Rails 4.0.0 (unreleased) ## +* Support for partial inserts. + + When inserting new records, only the fields which have been changed + from the defaults will actually be included in the INSERT statement. + The other fields will be populated by the database. + + This is more efficient, and also means that it will be safe to + remove database columns without getting subsequent errors in running + app processes (so long as the code in those processes doesn't + contain any references to the removed column). + + *Jon Leighton* + * Added `#update_columns` method which updates the attributes from the passed-in hash without calling save, hence skipping validations and callbacks. `ActiveRecordError` will be raised when called on new objects diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ced15bc330..0aff2562b8 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -207,8 +207,8 @@ module ActiveRecord value end - def arel_attributes_with_values_for_create(pk_attribute_allowed) - arel_attributes_with_values(attributes_for_create(pk_attribute_allowed)) + def arel_attributes_with_values_for_create(attribute_names) + arel_attributes_with_values(attributes_for_create(attribute_names)) end def arel_attributes_with_values_for_update(attribute_names) @@ -242,9 +242,9 @@ module ActiveRecord # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). - def attributes_for_create(pk_attribute_allowed) - @attributes.keys.select do |name| - column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name)) + def attributes_for_create(attribute_names) + attribute_names.select do |name| + column_for_attribute(name) && !(pk_attribute?(name) && id.nil?) end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 60e5b0e2bb..6204e4172d 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -64,15 +64,29 @@ module ActiveRecord end def update(*) + partial_updates? ? super(keys_for_partial_update) : super + end + + def create(*) if partial_updates? - # Serialized attributes should always be written in case they've been - # changed in place. - super(changed | (attributes.keys & self.class.serialized_attributes.keys)) + keys = keys_for_partial_update + + # This is an extremely bloody annoying necessity to work around mysql being crap. + # See test_mysql_text_not_null_defaults + keys.concat self.class.columns.select(&:explicit_default?).map(&:name) + + super keys else super end end + # Serialized attributes should always be written in case they've been + # changed in place. + def keys_for_partial_update + changed | (attributes.keys & self.class.serialized_attributes.keys) + end + def _field_changed?(attr, old, value) if column = column_for_attribute(attr) if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 793f58d4d3..0d7046a705 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -299,7 +299,7 @@ module ActiveRecord end def empty_insert_statement_value - "VALUES(DEFAULT)" + "DEFAULT VALUES" end def case_sensitive_equality_operator diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 1783b036a2..8c83c4f5db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -30,6 +30,10 @@ module ActiveRecord super end + def explicit_default? + !null && (sql_type =~ /blob/i || type == :text) + end + # Must return the relevant concrete adapter def adapter raise NotImplementedError @@ -320,6 +324,10 @@ module ActiveRecord end end + def empty_insert_statement_value + "VALUES ()" + end + # SCHEMA STATEMENTS ======================================== def structure_dump #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 816b5e17c1..2028abf6f0 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -53,6 +53,10 @@ module ActiveRecord !default.nil? end + def explicit_default? + false + end + # Returns the Ruby class that corresponds to the abstract data type. def klass case type diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 4a48812807..4d5cb72c67 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -490,10 +490,6 @@ module ActiveRecord alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end - def empty_insert_statement_value - "VALUES(NULL)" - end - protected def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 2eaad1d469..f81eb5f5d1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -385,8 +385,8 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. - def create - attributes_values = arel_attributes_with_values_for_create(!id.nil?) + def create(attribute_names = @attributes.keys) + attributes_values = arel_attributes_with_values_for_create(attribute_names) new_id = self.class.unscoped.insert attributes_values self.id ||= new_id if self.class.primary_key diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 9a2a5a4e3c..75a52544d3 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -3,6 +3,7 @@ require 'models/topic' # For booleans require 'models/pirate' # For timestamps require 'models/parrot' require 'models/person' # For optimistic locking +require 'models/aircraft' class Pirate # Just reopening it, not defining it attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected @@ -550,6 +551,30 @@ class DirtyTest < ActiveRecord::TestCase end end + test "partial insert" do + with_partial_updates Person do + jon = nil + assert_sql(/first_name/) do + jon = Person.create! first_name: 'Jon' + end + + assert ActiveRecord::SQLCounter.log_all.none? { |sql| sql =~ /followers_count/ } + + jon.reload + assert_equal 'Jon', jon.first_name + assert_equal 0, jon.followers_count + assert_not_nil jon.id + end + end + + test "partial insert with empty values" do + with_partial_updates Aircraft do + a = Aircraft.create! + a.reload + assert_not_nil a.id + end + end + private def with_partial_updates(klass, on = true) old = klass.partial_updates? -- cgit v1.2.3