Enable Rails 7.1 Marshalling format (#28609)
This commit is contained in:
		
					parent
					
						
							
								12bed81187
							
						
					
				
			
			
				commit
				
					
						5a6d533c53
					
				
			
		
					 5 changed files with 8 additions and 180 deletions
				
			
		|  | @ -3,150 +3,6 @@ | |||
| module CacheConcern | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|   module ActiveRecordCoder | ||||
|     EMPTY_HASH = {}.freeze | ||||
| 
 | ||||
|     class << self | ||||
|       def dump(record) | ||||
|         instances = InstanceTracker.new | ||||
|         serialized_associations = serialize_associations(record, instances) | ||||
|         serialized_records = instances.map { |r| serialize_record(r) } | ||||
|         [serialized_associations, *serialized_records] | ||||
|       end | ||||
| 
 | ||||
|       def load(payload) | ||||
|         instances = InstanceTracker.new | ||||
|         serialized_associations, *serialized_records = payload | ||||
|         serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) } | ||||
|         deserialize_associations(serialized_associations, instances) | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       # Records without associations, or which have already been visited before, | ||||
|       # are serialized by their id alone. | ||||
|       # | ||||
|       # Records with associations are serialized as a two-element array including | ||||
|       # their id and the record's association cache. | ||||
|       # | ||||
|       def serialize_associations(record, instances) | ||||
|         return unless record | ||||
| 
 | ||||
|         if (id = instances.lookup(record)) | ||||
|           payload = id | ||||
|         else | ||||
|           payload = instances.push(record) | ||||
| 
 | ||||
|           cached_associations = record.class.reflect_on_all_associations.select do |reflection| | ||||
|             record.association_cached?(reflection.name) | ||||
|           end | ||||
| 
 | ||||
|           unless cached_associations.empty? | ||||
|             serialized_associations = cached_associations.map do |reflection| | ||||
|               association = record.association(reflection.name) | ||||
| 
 | ||||
|               serialized_target = if reflection.collection? | ||||
|                                     association.target.map { |target_record| serialize_associations(target_record, instances) } | ||||
|                                   else | ||||
|                                     serialize_associations(association.target, instances) | ||||
|                                   end | ||||
| 
 | ||||
|               [reflection.name, serialized_target] | ||||
|             end | ||||
| 
 | ||||
|             payload = [payload, serialized_associations] | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         payload | ||||
|       end | ||||
| 
 | ||||
|       def deserialize_associations(payload, instances) | ||||
|         return unless payload | ||||
| 
 | ||||
|         id, associations = payload | ||||
|         record = instances.fetch(id) | ||||
| 
 | ||||
|         associations&.each do |name, serialized_target| | ||||
|           begin | ||||
|             association = record.association(name) | ||||
|           rescue ActiveRecord::AssociationNotFoundError | ||||
|             raise AssociationMissingError, "undefined association: #{name}" | ||||
|           end | ||||
| 
 | ||||
|           target = if association.reflection.collection? | ||||
|                      serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) } | ||||
|                    else | ||||
|                      deserialize_associations(serialized_target, instances) | ||||
|                    end | ||||
| 
 | ||||
|           association.target = target | ||||
|         end | ||||
| 
 | ||||
|         record | ||||
|       end | ||||
| 
 | ||||
|       def serialize_record(record) | ||||
|         arguments = [record.class.name, attributes_for_database(record)] | ||||
|         arguments << true if record.new_record? | ||||
|         arguments | ||||
|       end | ||||
| 
 | ||||
|       def attributes_for_database(record) | ||||
|         attributes = record.attributes_for_database | ||||
|         attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } | ||||
|         attributes | ||||
|       end | ||||
| 
 | ||||
|       def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter | ||||
|         begin | ||||
|           klass = Object.const_get(class_name) | ||||
|         rescue NameError | ||||
|           raise ClassMissingError, "undefined class: #{class_name}" | ||||
|         end | ||||
| 
 | ||||
|         # Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass | ||||
|         # wether the record was persisted or not. | ||||
|         attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH) | ||||
|         klass.allocate.init_with_attributes(attributes, new_record) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     class Error < StandardError | ||||
|     end | ||||
| 
 | ||||
|     class ClassMissingError < Error | ||||
|     end | ||||
| 
 | ||||
|     class AssociationMissingError < Error | ||||
|     end | ||||
| 
 | ||||
|     class InstanceTracker | ||||
|       def initialize | ||||
|         @instances = [] | ||||
|         @ids = {}.compare_by_identity | ||||
|       end | ||||
| 
 | ||||
|       def map(&block) | ||||
|         @instances.map(&block) | ||||
|       end | ||||
| 
 | ||||
|       def fetch(...) | ||||
|         @instances.fetch(...) | ||||
|       end | ||||
| 
 | ||||
|       def push(instance) | ||||
|         id = @ids[instance] = @instances.size | ||||
|         @instances << instance | ||||
|         id | ||||
|       end | ||||
| 
 | ||||
|       def lookup(instance) | ||||
|         @ids[instance] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   class_methods do | ||||
|     def vary_by(value, **kwargs) | ||||
|       before_action(**kwargs) do |controller| | ||||
|  | @ -196,11 +52,7 @@ module CacheConcern | |||
|     raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) | ||||
|     return [] if raw.empty? | ||||
| 
 | ||||
|     cached_keys_with_value = begin | ||||
|       Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) } | ||||
|     rescue ActiveRecordCoder::Error | ||||
|       {} # The serialization format may have changed, let's pretend it's a cache miss. | ||||
|     end | ||||
|     cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) | ||||
| 
 | ||||
|     uncached_ids = raw.map(&:id) - cached_keys_with_value.keys | ||||
| 
 | ||||
|  | @ -208,10 +60,7 @@ module CacheConcern | |||
| 
 | ||||
|     unless uncached_ids.empty? | ||||
|       uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) | ||||
| 
 | ||||
|       uncached.each_value do |item| | ||||
|         Rails.cache.write(item, ActiveRecordCoder.dump(item)) | ||||
|       end | ||||
|       Rails.cache.write_multi(uncached.values.to_h { |i| [i, i] }) | ||||
|     end | ||||
| 
 | ||||
|     raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue