@@ -112,25 +112,25 @@ def foreign_keys(resource, relationship_name, options = {})
112112 def find_serialized_with_caching ( filters_or_source , serializer , options = { } )
113113 if filters_or_source . is_a? ( ActiveRecord ::Relation )
114114 return cached_resources_for ( filters_or_source , serializer , options )
115- elsif _resource_klass . _model_class . respond_to? ( :all ) && _resource_klass . _model_class . respond_to? ( :arel_table )
115+ elsif resource_class_based_on_active_record? ( _resource_klass )
116116 records = find_records ( filters_or_source , options . except ( :include_directives ) )
117117 return cached_resources_for ( records , serializer , options )
118118 else
119119 # :nocov:
120- warn ( 'Caching enabled on model that does not support ActiveRelation ' )
120+ warn ( 'Caching enabled on model not based on ActiveRecord API or similar ' )
121121 # :nocov:
122122 end
123123 end
124124
125125 def find_by_key_serialized_with_caching ( key , serializer , options = { } )
126- if _resource_klass . _model_class . respond_to? ( :all ) && _resource_klass . _model_class . respond_to? ( :arel_table )
126+ if resource_class_based_on_active_record? ( _resource_klass )
127127 results = find_serialized_with_caching ( { _resource_klass . _primary_key => key } , serializer , options )
128128 result = results . first
129129 fail JSONAPI ::Exceptions ::RecordNotFound . new ( key ) if result . nil?
130130 return result
131131 else
132132 # :nocov:
133- warn ( 'Caching enabled on model that does not support ActiveRelation ' )
133+ warn ( 'Caching enabled on model not based on ActiveRecord API or similar ' )
134134 # :nocov:
135135 end
136136 end
@@ -339,7 +339,14 @@ def cached_resources_for(records, serializer, options)
339339 resources = _resource_klass . resources_for ( records , options [ :context ] ) . map { |r | [ r . id , r ] } . to_h
340340 end
341341
342- preload_included_fragments ( resources , records , serializer , options )
342+ if options [ :include_directives ]
343+ resource_pile = { _resource_klass . name => resources }
344+ options [ :include_directives ] . all_paths . each do |path |
345+ # Note that `all_paths` returns shorter paths first, so e.g. the partial fragments for
346+ # posts.comments will exist before we start working with posts.comments.author
347+ preload_included_fragments ( _resource_klass , resource_pile , path , serializer , options )
348+ end
349+ end
343350
344351 resources . values
345352 end
@@ -366,137 +373,121 @@ def find_records(filters, options = {})
366373 end
367374 end
368375
369- def preload_included_fragments ( resources , records , serializer , options )
370- return if resources . empty?
371- res_ids = resources . keys
376+ def preload_included_fragments ( src_res_class , resource_pile , path , serializer , options )
377+ src_resources = resource_pile [ src_res_class . name ]
378+ return if src_resources . nil? || src_resources . empty?
379+
380+ rel_name = path . first
381+ relationship = src_res_class . _relationships [ rel_name ]
382+ if relationship . polymorphic
383+ # FIXME Preloading through a polymorphic belongs_to association is not implemented.
384+ # For now, in this case, ResourceSerializer will have to do the fetch itself, without
385+ # using either the cache or eager-loading.
386+ return
387+ end
372388
373- include_directives = options [ :include_directives ]
374- return unless include_directives
375-
376- context = options [ :context ]
377-
378- # For each association, including indirect associations, find the target record ids.
379- # Even if a target class doesn't have caching enabled, we still have to look up
380- # and match the target ids here, because we can't use ActiveRecord#includes.
381- #
382- # Note that `paths` returns partial paths before complete paths, so e.g. the partial
383- # fragments for posts.comments will exist before we start working with posts.comments.author
384- target_resources = { }
385- include_directives . paths . each do |path |
386- # If path is [:posts, :comments, :author], then...
387- pluck_attrs = [ ] # ...will be [posts.id, comments.id, authors.id, authors.updated_at]
388- pluck_attrs << _resource_klass . _model_class . arel_table [ _resource_klass . _primary_key ]
389-
390- relation = records
391- . except ( :limit , :offset , :order )
392- . where ( { _resource_klass . _primary_key => res_ids } )
393-
394- # These are updated as we iterate through the association path; afterwards they will
395- # refer to the final resource on the path, i.e. the actual resource to find in the cache.
396- # So e.g. if path is [:posts, :comments, :author], then after iteration...
397- parent_klass = nil # Comment
398- klass = _resource_klass # Person
399- relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author
400- table = nil # people
401- assocs_path = [ ] # [ :posts, :approved_comments, :author ]
402- ar_hash = nil # { :posts => { :approved_comments => :author } }
403-
404- # For each step on the path, figure out what the actual table name/alias in the join
405- # will be, and include the primary key of that table in our list of fields to select
406- non_polymorphic = true
407- path . each do |elem |
408- relationship = klass . _relationships [ elem ]
409- if relationship . polymorphic
410- # Can't preload through a polymorphic belongs_to association, ResourceSerializer
411- # will just have to bypass the cache and load the real Resource.
412- non_polymorphic = false
413- break
414- end
415- assocs_path << relationship . relation_name ( options ) . to_sym
416- # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
417- ar_hash = assocs_path . reverse . reduce { |memo , step | { step => memo } }
418- # We can't just look up the table name from the resource class, because Arel could
419- # have used a table alias if the relation includes a self-reference.
420- join_source = relation . joins ( ar_hash ) . arel . source . right . reverse . find do |arel_node |
421- arel_node . is_a? ( Arel ::Nodes ::InnerJoin )
422- end
423- table = join_source . left
424- parent_klass = klass
425- klass = relationship . resource_klass
426- pluck_attrs << table [ klass . _primary_key ]
427- end
428- next unless non_polymorphic
429-
430- # Pre-fill empty hashes for each resource up to the end of the path.
431- # This allows us to later distinguish between a preload that returned nothing
432- # vs. a preload that never ran.
433- prefilling_resources = resources . values
434- path . each do |rel_name |
435- rel_name = serializer . key_formatter . format ( rel_name )
436- prefilling_resources . map! do |res |
437- res . preloaded_fragments [ rel_name ] ||= { }
438- res . preloaded_fragments [ rel_name ] . values
439- end
440- prefilling_resources . flatten! ( 1 )
441- end
389+ tgt_res_class = relationship . resource_klass
390+ unless resource_class_based_on_active_record? ( tgt_res_class )
391+ # Can't preload relationships from non-AR resources, this association will be filled
392+ # in on-demand later by ResourceSerializer.
393+ return
394+ end
442395
443- pluck_attrs << table [ klass . _cache_field ] if klass . caching?
444- relation = relation . joins ( ar_hash )
445- if relationship . is_a? ( JSONAPI ::Relationship ::ToMany )
446- # Rails doesn't include order clauses in `joins`, so we have to add that manually here.
447- # FIXME Should find a better way to reflect on relationship ordering. :-(
448- relation = relation . order ( parent_klass . _model_class . new . send ( assocs_path . last ) . arel . orders )
449- end
396+ # Assume for longer paths that the intermediate fragments have already been preloaded
397+ if path . length > 1
398+ preload_included_fragments ( tgt_res_class , resource_pile , path . drop ( 1 ) , serializer , options )
399+ return
400+ end
450401
451- # [[post id, comment id, author id, author updated_at], ...]
452- id_rows = pluck_arel_attributes ( relation . joins ( ar_hash ) , *pluck_attrs )
402+ record_source = src_res_class . _model_class
403+ . where ( { src_res_class . _primary_key => src_resources . keys } )
404+ . joins ( relationship . relation_name ( options ) . to_sym )
453405
454- target_resources [ klass . name ] ||= { }
406+ if relationship . is_a? ( JSONAPI ::Relationship ::ToMany )
407+ # Rails doesn't include order clauses in `joins`, so we have to add that manually here.
408+ # FIXME Should find a better way to reflect on relationship ordering. :-(
409+ fake_model_instance = src_res_class . _model_class . new
410+ record_source = record_source . order ( fake_model_instance . send ( rel_name ) . arel . orders )
411+ end
455412
456- if klass . caching?
457- sub_cache_ids = id_rows
458- . map { |row | row . last ( 2 ) }
459- . reject { |row | target_resources [ klass . name ] . has_key? ( row . first ) }
460- . uniq
461- target_resources [ klass . name ] . merge! CachedResourceFragment . fetch_fragments (
462- klass , serializer , context , sub_cache_ids
463- )
464- else
465- sub_res_ids = id_rows
466- . map ( &:last )
467- . reject { |id | target_resources [ klass . name ] . has_key? ( id ) }
468- . uniq
469- found = klass . find ( { klass . _primary_key => sub_res_ids } , context : options [ :context ] )
470- target_resources [ klass . name ] . merge! found . map { |r | [ r . id , r ] } . to_h
471- end
413+ # Pre-fill empty fragment hashes.
414+ # This allows us to later distinguish between a preload that returned nothing
415+ # vs. a preload that never ran.
416+ serialized_rel_name = serializer . key_formatter . format ( rel_name )
417+ src_resources . each do |key , res |
418+ res . preloaded_fragments [ serialized_rel_name ] ||= { }
419+ end
472420
473- id_rows . each do |row |
474- res = resources [ row . first ]
475- path . each_with_index do |rel_name , index |
476- rel_name = serializer . key_formatter . format ( rel_name )
477- rel_id = row [ index +1 ]
478- assoc_rels = res . preloaded_fragments [ rel_name ]
479- if index == path . length - 1
480- fragment = target_resources [ klass . name ] . fetch ( rel_id )
481- if fragment
482- assoc_rels [ rel_id ] = fragment
483- end
484- else
485- res = assoc_rels [ rel_id ]
486- end
487- end
488- end
421+ # We can't just look up the table name from the target class, because Arel could
422+ # have used a table alias if the relation is a self-reference.
423+ join_node = record_source . arel . source . right . reverse . find do |arel_node |
424+ arel_node . is_a? ( Arel ::Nodes ::InnerJoin )
425+ end
426+ tgt_table = join_node . left
427+
428+ # Resource class may restrict current user to a subset of available records
429+ if tgt_res_class . respond_to? ( :records )
430+ valid_tgts_rel = tgt_res_class . records ( options )
431+ valid_tgts_rel = valid_tgts_rel . all if valid_tgts_rel . respond_to? ( :all )
432+ conn = valid_tgts_rel . connection
433+ tgt_attr = tgt_table [ tgt_res_class . _primary_key ]
434+
435+ # Alter a normal AR query to select only the primary key instead of all columns.
436+ # Sadly doing direct string manipulation of query here, cannot use ARel for this due to
437+ # bind values being stripped from AR::Relation#arel in Rails >= 4.2, see
438+ # https://github.com/rails/arel/issues/363
439+ valid_tgts_query = valid_tgts_rel . to_sql . sub ( '*' , conn . quote_column_name ( tgt_attr . name ) )
440+ valid_tgts_cond = "#{ quote_arel_attribute ( conn , tgt_attr ) } IN (#{ valid_tgts_query } )"
441+
442+ record_source = record_source . where ( valid_tgts_cond )
443+ end
444+
445+ pluck_attrs = [
446+ src_res_class . _model_class . arel_table [ src_res_class . _primary_key ] ,
447+ tgt_table [ tgt_res_class . _primary_key ]
448+ ]
449+ pluck_attrs << tgt_table [ tgt_res_class . _cache_field ] if tgt_res_class . caching?
450+
451+ id_rows = pluck_arel_attributes ( record_source , *pluck_attrs )
452+
453+ target_resources = resource_pile [ tgt_res_class . name ] ||= { }
454+
455+ if tgt_res_class . caching?
456+ sub_cache_ids = id_rows . map { |row | row . last ( 2 ) } . uniq . reject { |p | target_resources . has_key? ( p [ 0 ] ) }
457+ target_resources . merge! CachedResourceFragment . fetch_fragments (
458+ tgt_res_class , serializer , options [ :context ] , sub_cache_ids
459+ )
460+ else
461+ sub_res_ids = id_rows . map ( &:last ) . uniq - target_resources . keys
462+ recs = tgt_res_class . find ( { tgt_res_class . _primary_key => sub_res_ids } , context : options [ :context ] )
463+ target_resources . merge! ( recs . map { |r | [ r . id , r ] } . to_h )
464+ end
465+
466+ id_rows . each do |row |
467+ src_id , tgt_id = row [ 0 ] , row [ 1 ]
468+ src_res = src_resources [ src_id ]
469+ next unless src_res
470+ fragment = target_resources [ tgt_id ]
471+ next unless fragment
472+ src_res . preloaded_fragments [ serialized_rel_name ] [ tgt_id ] = fragment
489473 end
490474 end
491475
492476 def pluck_arel_attributes ( relation , *attrs )
493477 conn = relation . connection
494- quoted_attrs = attrs . map do |attr |
495- quoted_table = conn . quote_table_name ( attr . relation . table_alias || attr . relation . name )
496- quoted_column = conn . quote_column_name ( attr . name )
497- "#{ quoted_table } .#{ quoted_column } "
498- end
478+ quoted_attrs = attrs . map { |attr | quote_arel_attribute ( conn , attr ) }
499479 relation . pluck ( *quoted_attrs )
500480 end
481+
482+ def quote_arel_attribute ( connection , attr )
483+ quoted_table = connection . quote_table_name ( attr . relation . table_alias || attr . relation . name )
484+ quoted_column = connection . quote_column_name ( attr . name )
485+ "#{ quoted_table } .#{ quoted_column } "
486+ end
487+
488+ def resource_class_based_on_active_record? ( klass )
489+ model_class = klass . _model_class
490+ model_class . respond_to? ( :all ) && model_class . respond_to? ( :arel_table )
491+ end
501492 end
502493end
0 commit comments