Skip to content
Browse files

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).
  • Loading branch information...
1 parent f9c63ad commit 144e8691cbfb8bba77f18cfe68d5e7fd48887f5e @jonleighton jonleighton committed Sep 28, 2012
View
13 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
View
10 activerecord/lib/active_record/attribute_methods.rb
@@ -207,8 +207,8 @@ def clone_attribute_value(reader_method, attribute_name)
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 @@ def attributes_for_update(attribute_names)
# 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
View
20 activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -64,15 +64,29 @@ def write_attribute(attr, value)
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) ||
View
2 activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -299,7 +299,7 @@ def insert_fixture(fixture, table_name)
end
def empty_insert_statement_value
- "VALUES(DEFAULT)"
+ "DEFAULT VALUES"
end
def case_sensitive_equality_operator
View
8 activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -30,6 +30,10 @@ def has_default?
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 @@ def join_to_update(update, select) #:nodoc:
end
end
+ def empty_insert_statement_value
+ "VALUES ()"
+ end
+
# SCHEMA STATEMENTS ========================================
def structure_dump #:nodoc:
View
4 activerecord/lib/active_record/connection_adapters/column.rb
@@ -53,6 +53,10 @@ def has_default?
!default.nil?
end
+ def explicit_default?
+ false
+ end
+
# Returns the Ruby class that corresponds to the abstract data type.
def klass
case type
View
4 activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -490,10 +490,6 @@ def rename_column(table_name, column_name, new_column_name) #:nodoc:
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)
View
4 activerecord/lib/active_record/persistence.rb
@@ -385,8 +385,8 @@ def update(attribute_names = @attributes.keys)
# 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
View
25 activerecord/test/cases/dirty_test.rb
@@ -3,6 +3,7 @@
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 @@ def test_setting_time_attributes_with_time_zone_field_to_same_time_should_not_be
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?

0 comments on commit 144e869

Please sign in to comment.
Something went wrong with that request. Please try again.