@@ -112,25 +112,25 @@ def foreign_keys(resource, relationship_name, options = {})
112
112
def find_serialized_with_caching ( filters_or_source , serializer , options = { } )
113
113
if filters_or_source . is_a? ( ActiveRecord ::Relation )
114
114
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 )
116
116
records = find_records ( filters_or_source , options . except ( :include_directives ) )
117
117
return cached_resources_for ( records , serializer , options )
118
118
else
119
119
# :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 ' )
121
121
# :nocov:
122
122
end
123
123
end
124
124
125
125
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 )
127
127
results = find_serialized_with_caching ( { _resource_klass . _primary_key => key } , serializer , options )
128
128
result = results . first
129
129
fail JSONAPI ::Exceptions ::RecordNotFound . new ( key ) if result . nil?
130
130
return result
131
131
else
132
132
# :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 ' )
134
134
# :nocov:
135
135
end
136
136
end
@@ -339,7 +339,14 @@ def cached_resources_for(records, serializer, options)
339
339
resources = _resource_klass . resources_for ( records , options [ :context ] ) . map { |r | [ r . id , r ] } . to_h
340
340
end
341
341
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
343
350
344
351
resources . values
345
352
end
@@ -366,134 +373,121 @@ def find_records(filters, options = {})
366
373
end
367
374
end
368
375
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
372
388
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
442
395
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
450
401
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 )
453
405
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
455
412
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
472
420
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
- assoc_rels [ rel_id ] = target_resources [ klass . name ] . fetch ( rel_id )
481
- else
482
- res = assoc_rels [ rel_id ]
483
- end
484
- end
485
- 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
486
473
end
487
474
end
488
475
489
476
def pluck_arel_attributes ( relation , *attrs )
490
477
conn = relation . connection
491
- quoted_attrs = attrs . map do |attr |
492
- quoted_table = conn . quote_table_name ( attr . relation . table_alias || attr . relation . name )
493
- quoted_column = conn . quote_column_name ( attr . name )
494
- "#{ quoted_table } .#{ quoted_column } "
495
- end
478
+ quoted_attrs = attrs . map { |attr | quote_arel_attribute ( conn , attr ) }
496
479
relation . pluck ( *quoted_attrs )
497
480
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
498
492
end
499
493
end
0 commit comments