-
-
Save malomalo/269bda0439b1402e432acdb1fcabd910 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ActiveRecord Autosave will save and validate all autosave records in | |
# every permutation possible. This patch allows Rails to save / valid | |
# in once significantly speeding up saving and validating of recrods. | |
# TODO: Remove when https://github.com/rails/rails/pull/46438 is accepted | |
require 'active_record/base' | |
require 'active_record/validations' | |
require 'active_record/autosave_association' | |
module ActiveRecord | |
module Validations | |
def save(**options) | |
if perform_validations(options) | |
begin | |
memory_was = @memory | |
@memory = options[:memory] || {} | |
@memory["saved#{self.object_id}"] = true | |
@memory[self.object_id] = false | |
super | |
ensure | |
@memory = memory_was | |
end | |
else | |
false | |
end | |
end | |
def save!(**options) | |
if perform_validations(memory: (options[:memory] || @memory), **options) | |
begin | |
memory_was = @memory | |
@memory = options[:memory] || {} | |
@memory["saved#{self.object_id}"] = true | |
@memory[self.object_id] = false | |
super | |
ensure | |
@memory = memory_was | |
end | |
else | |
raise_validation_error | |
end | |
end | |
def valid?(context = nil, memory = nil) | |
context ||= default_validation_context | |
memory_was = @memory | |
@memory = memory || {} | |
@memory["valid#{self.object_id}"] = true | |
@memory[self.object_id] = false | |
output = super(context) | |
errors.empty? && output | |
ensure | |
@memory = memory_was | |
end | |
end | |
module AutosaveAssociation | |
# Returns whether or not this record has been changed in any way (including whether | |
# any of its nested autosave associations are likewise changed) | |
def changed_for_autosave?(memory) | |
if new_record? || has_changes_to_save? || marked_for_destruction? | |
memory[self.object_id] = true | |
elsif memory.has_key?(self.object_id) | |
memory[self.object_id] | |
else | |
memory[self.object_id] = nested_records_changed_for_autosave?(memory) | |
end | |
end | |
private | |
# Returns the record for an association collection that should be validated | |
# or saved. If +autosave+ is +false+ only new records will be returned, | |
# unless the parent is/was a new record itself. | |
def associated_records_to_validate_or_save(association, new_record, autosave, memory) | |
if new_record || custom_validation_context? | |
association && association.target | |
elsif autosave | |
association.target.find_all { |r| r.changed_for_autosave?(memory) } | |
else | |
association.target.find_all(&:new_record?) | |
end | |
end | |
# Go through nested autosave associations that are loaded in memory (without loading | |
# any new ones), and return true if any are changed for autosave. | |
# Returns false if already called to prevent an infinite loop. | |
def nested_records_changed_for_autosave?(memory) | |
@_nested_records_changed_for_autosave_already_called ||= false | |
return false if @_nested_records_changed_for_autosave_already_called | |
begin | |
@_nested_records_changed_for_autosave_already_called = true | |
self.class._reflections.values.any? do |reflection| | |
if reflection.options[:autosave] | |
association = association_instance_get(reflection.name) | |
association && Array.wrap(association.target).any? {|r| r.changed_for_autosave?(memory) } | |
end | |
end | |
ensure | |
@_nested_records_changed_for_autosave_already_called = false | |
end | |
end | |
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is | |
# turned on for the association. | |
def validate_single_association(reflection) | |
association = association_instance_get(reflection.name) | |
record = association && association.reader | |
association_valid?(reflection, record, @memory) if record && (record.changed_for_autosave?(@memory) || custom_validation_context?) | |
end | |
# Validate the associated records if <tt>:validate</tt> or | |
# <tt>:autosave</tt> is turned on for the association specified by | |
# +reflection+. | |
def validate_collection_association(reflection) | |
if association = association_instance_get(reflection.name) | |
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave], @memory) | |
records.each_with_index { |record, index| association_valid?(reflection, record, index, @memory) } | |
end | |
end | |
end | |
# Returns whether or not the association is valid and applies any errors to | |
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> | |
# enabled records if they're marked_for_destruction? or destroyed. | |
def association_valid?(reflection, record, index = nil, memory) | |
return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) | |
context = validation_context if custom_validation_context? | |
if !memory.has_key?("valid#{record.object_id}") | |
memory["valid#{record.object_id}"] = true | |
valid = record.valid?(context, memory) | |
unless valid | |
if reflection.options[:autosave] | |
indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord.index_nested_attribute_errors) | |
record.errors.group_by_attribute.each { |attribute, errors| | |
attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) | |
errors.each { |error| | |
self.errors.import( | |
error, | |
attribute: attribute | |
) | |
} | |
} | |
else | |
errors.add(reflection.name) | |
end | |
end | |
memory["valid#{record.object_id}"] = valid | |
valid | |
end | |
end | |
# Saves any new associated records, or all loaded autosave associations if | |
# <tt>:autosave</tt> is enabled on the association. | |
# | |
# In addition, it destroys all children that were marked for destruction | |
# with #mark_for_destruction. | |
# | |
# This all happens inside a transaction, _if_ the Transactions module is included into | |
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default. | |
def save_collection_association(reflection) | |
if association = association_instance_get(reflection.name) | |
autosave = reflection.options[:autosave] | |
# By saving the instance variable in a local variable, | |
# we make the whole callback re-entrant. | |
new_record_before_save = @new_record_before_save | |
# reconstruct the scope now that we know the owner's id | |
association.reset_scope | |
if records = associated_records_to_validate_or_save(association, new_record_before_save, autosave, @memory) | |
if autosave | |
records_to_destroy = records.select(&:marked_for_destruction?) | |
records_to_destroy.each { |record| association.destroy(record) } | |
records -= records_to_destroy | |
end | |
records.each do |record| | |
if !@memory.has_key?("saved#{record.object_id}") | |
@memory["saved#{record.object_id}"] = true | |
end | |
end | |
records.each do |record| | |
next if record.destroyed? | |
saved = true | |
if autosave != false && (new_record_before_save || record.new_record?) | |
association.set_inverse_instance(record) | |
if autosave | |
saved = association.insert_record(record, false, false, @memory) | |
elsif !reflection.nested? | |
association_saved = association.insert_record(record, true, false, @memory) | |
if reflection.validate? | |
errors.add(reflection.name) unless association_saved | |
saved = association_saved | |
end | |
end | |
elsif autosave | |
saved = record.save(validate: false, memory: @memory) | |
end | |
raise(RecordInvalid.new(association.owner)) unless saved | |
end | |
end | |
end | |
end | |
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled | |
# on the association. | |
# | |
# In addition, it will destroy the association if it was marked for | |
# destruction with #mark_for_destruction. | |
# | |
# This all happens inside a transaction, _if_ the Transactions module is included into | |
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default. | |
def save_has_one_association(reflection) | |
association = association_instance_get(reflection.name) | |
record = association && association.load_target | |
if record && !record.destroyed? | |
autosave = reflection.options[:autosave] | |
if autosave && record.marked_for_destruction? | |
record.destroy | |
elsif autosave != false | |
primary_key = Array(compute_primary_key(reflection, self)).map(&:to_s) | |
primary_key_value = primary_key.map { |key| _read_attribute(key) } | |
if (autosave && record.changed_for_autosave?(@memory)) || _record_changed?(reflection, record, primary_key_value) | |
unless reflection.through_reflection | |
foreign_key = Array(reflection.foreign_key) | |
primary_key_foreign_key_pairs = primary_key.zip(foreign_key) | |
primary_key_foreign_key_pairs.each do |primary_key, foreign_key| | |
record[foreign_key] = _read_attribute(primary_key) | |
end | |
association.set_inverse_instance(record) | |
end | |
saved = if @memory.has_key?("saved#{record.object_id}") | |
@memory["saved#{record.object_id}"] | |
else | |
record.save(validate: !autosave, memory: @memory) | |
end | |
raise ActiveRecord::Rollback if !saved && autosave | |
saved | |
end | |
end | |
end | |
end | |
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled. | |
# | |
# In addition, it will destroy the association if it was marked for destruction. | |
def save_belongs_to_association(reflection) | |
association = association_instance_get(reflection.name) | |
return unless association && association.loaded? && !association.stale_target? | |
record = association.load_target | |
if record && !record.destroyed? | |
autosave = reflection.options[:autosave] | |
if autosave && record.marked_for_destruction? | |
foreign_key = Array(reflection.foreign_key) | |
foreign_key.each { |key| self[key] = nil } | |
record.destroy | |
elsif autosave != false | |
saved = if @memory.has_key?("saved#{record.object_id}") | |
@memory["saved#{record.object_id}"] | |
elsif record.new_record? || (autosave && record.changed_for_autosave?(@memory)) | |
record.save(validate: !autosave, memory: @memory) | |
end | |
if association.updated? | |
primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s) | |
foreign_key = Array(reflection.foreign_key) | |
primary_key_foreign_key_pairs = primary_key.zip(foreign_key) | |
primary_key_foreign_key_pairs.each do |primary_key, foreign_key| | |
association_id = record._read_attribute(primary_key) | |
self[foreign_key] = association_id unless self[foreign_key] == association_id | |
end | |
association.loaded! | |
end | |
saved if autosave | |
end | |
end | |
end | |
end | |
end | |
require 'active_record/associations/collection_association' | |
require 'active_record/associations/has_many_association' | |
require 'active_record/associations/has_many_through_association' | |
module ActiveRecord | |
module Associations | |
class CollectionAssociation | |
def insert_record(record, validate = true, raise = false, memory = nil, &block) | |
if raise | |
record.save!(validate: validate, memory: memory, &block) | |
else | |
record.save(validate: validate, memory: memory, &block) | |
end | |
end | |
end | |
class HasManyAssociation | |
def insert_record(record, validate = true, raise = false, memory = nil) | |
set_owner_attributes(record) | |
super | |
end | |
end | |
class HasManyThroughAssociation | |
def insert_record(record, validate = true, raise = false, memory = nil) | |
ensure_not_nested | |
if record.new_record? || record.has_changes_to_save? | |
return unless super | |
end | |
save_through_record(record) | |
record | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment