diff --git a/lib/graphql/dataloader/active_record_source.rb b/lib/graphql/dataloader/active_record_source.rb index 7641baaf217..30b26942975 100644 --- a/lib/graphql/dataloader/active_record_source.rb +++ b/lib/graphql/dataloader/active_record_source.rb @@ -7,18 +7,39 @@ class ActiveRecordSource < GraphQL::Dataloader::Source def initialize(model_class, find_by: model_class.primary_key) @model_class = model_class @find_by = find_by - @type_for_column = @model_class.type_for_attribute(@find_by) + @find_by_many = find_by.is_a?(Array) + if @find_by_many + @type_for_column = @find_by.map { |fb| @model_class.type_for_attribute(fb) } + else + @type_for_column = @model_class.type_for_attribute(@find_by) + end end - def load(requested_key) - casted_key = @type_for_column.cast(requested_key) - super(casted_key) + def result_key_for(requested_key) + normalize_fetch_key(requested_key) + end + + def normalize_fetch_key(requested_key) + if @find_by_many + requested_key.each_with_index.map do |k, idx| + @type_for_column[idx].cast(k) + end + else + @type_for_column.cast(requested_key) + end end def fetch(record_ids) records = @model_class.where(@find_by => record_ids) record_lookup = {} - records.each { |r| record_lookup[r.public_send(@find_by)] = r } + if @find_by_many + records.each do |r| + key = @find_by.map { |fb| r.public_send(fb) } + record_lookup[key] = r + end + else + records.each { |r| record_lookup[r.public_send(@find_by)] = r } + end record_ids.map { |id| record_lookup[id] } end end diff --git a/lib/graphql/dataloader/source.rb b/lib/graphql/dataloader/source.rb index 86ce88726e0..ec3bbc964fc 100644 --- a/lib/graphql/dataloader/source.rb +++ b/lib/graphql/dataloader/source.rb @@ -21,7 +21,7 @@ def setup(dataloader) def request(value) res_key = result_key_for(value) if !@results.key?(res_key) - @pending[res_key] ||= value + @pending[res_key] ||= normalize_fetch_key(value) end Dataloader::Request.new(self, value) end @@ -35,12 +35,24 @@ def result_key_for(value) value end + # Implement this method if varying values given to {load} (etc) should be consolidated + # or normalized before being handed off to your {fetch} implementation. + # + # This is different than {result_key_for} because _that_ method handles unification inside Dataloader's cache, + # but this method changes the value passed into {fetch}. + # + # @param value [Object] The value passed to {load}, {load_all}, {request}, or {request_all} + # @return [Object] The value given to {fetch} + def normalize_fetch_key(value) + value + end + # @return [Dataloader::Request] a pending request for a values from `keys`. Call `.load` on that object to wait for the results. def request_all(values) values.each do |v| res_key = result_key_for(v) if !@results.key?(res_key) - @pending[res_key] ||= v + @pending[res_key] ||= normalize_fetch_key(v) end end Dataloader::RequestAll.new(self, values) @@ -53,7 +65,7 @@ def load(value) if @results.key?(result_key) result_for(result_key) else - @pending[result_key] ||= value + @pending[result_key] ||= normalize_fetch_key(value) sync([result_key]) result_for(result_key) end @@ -68,7 +80,7 @@ def load_all(values) k = result_key_for(v) result_keys << k if !@results.key?(k) - @pending[k] ||= v + @pending[k] ||= normalize_fetch_key(v) pending_keys << k end } diff --git a/spec/graphql/dataloader/active_record_association_source_spec.rb b/spec/graphql/dataloader/active_record_association_source_spec.rb index c32610886f3..1ee764702a6 100644 --- a/spec/graphql/dataloader/active_record_association_source_spec.rb +++ b/spec/graphql/dataloader/active_record_association_source_spec.rb @@ -105,5 +105,26 @@ vulfpeck = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :thing).load(wilco) assert_equal ::Band.find(1), vulfpeck end + + if Rails::VERSION::STRING > "7.1" # not supported in <7.1 + it_dataloads "loads with composite primary keys and warms the cache" do |d| + my_first_car = ::Album.find(2) + homey = ::Album.find(4) + log = with_active_record_log(colorize: false) do + vulfpeck, chon = d.with(GraphQL::Dataloader::ActiveRecordAssociationSource, :composite_band).load_all([my_first_car, homey]) + assert_equal "Vulfpeck", vulfpeck.name + assert_equal "Chon", chon.name + end + + assert_includes log, '[["name", "Vulfpeck"], ["name", "Chon"], ["genre", 0]]' + + + log = with_active_record_log(colorize: false) do + d.with(GraphQL::Dataloader::ActiveRecordSource, CompositeBand).load_all([["Vulfpeck", "rock"], ["Chon", :rock]]) + end + + assert_equal "", log + end + end end end diff --git a/spec/graphql/dataloader/active_record_source_spec.rb b/spec/graphql/dataloader/active_record_source_spec.rb index 5154afbd7a7..34be8b8ea9e 100644 --- a/spec/graphql/dataloader/active_record_source_spec.rb +++ b/spec/graphql/dataloader/active_record_source_spec.rb @@ -64,6 +64,23 @@ assert_includes log, 'SELECT "bands".* FROM "bands" WHERE "bands"."name" = ? [["name", "Vulfpeck"]]' end + if Rails::VERSION::STRING > "7.1" # not supported in <7.1 + it_dataloads "uses composite primary keys" do |d| + log = with_active_record_log(colorize: false) do + r1 = d.with(GraphQL::Dataloader::ActiveRecordSource, CompositeBand).load(["Chon", :rock]) + assert_equal "Chon", r1.name + assert_equal ["Chon", "rock"], r1.id + if Rails::VERSION::STRING > "8" + assert_equal 3, r1["id"] + else + assert_equal 3, r1._read_attribute("id") + end + end + + assert_includes log, 'SELECT "bands".* FROM "bands" WHERE "bands"."name" = ? AND "bands"."genre" = ? [["name", "Chon"], ["genre", 0]]' + end + end + it_dataloads "uses specified find_by columns" do |d| log = with_active_record_log(colorize: false) do r1 = d.with(GraphQL::Dataloader::ActiveRecordSource, Band, find_by: :name).load("Chon") @@ -99,11 +116,5 @@ assert_equal "", log end end - - describe "in queries" do - it "loads records with dataload_record" - - it "accepts custom find-by with dataload_record" - end end end diff --git a/spec/graphql/tracing/snapshots/example-rails-7-0.json b/spec/graphql/tracing/snapshots/example-rails-7-0.json index 86b807a0b76..5aba4238ce2 100644 --- a/spec/graphql/tracing/snapshots/example-rails-7-0.json +++ b/spec/graphql/tracing/snapshots/example-rails-7-0.json @@ -1487,13 +1487,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "id" + "stringValue": "@type_for_column" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "resolved_type" + "stringValue": "id" } ], "type": "TYPE_SLICE_BEGIN", @@ -1556,13 +1556,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "id" + "stringValue": "@type_for_column" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": null + "stringValue": "resolved_type" } ], "type": "TYPE_SLICE_BEGIN", @@ -2961,6 +2961,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -3014,6 +3019,10 @@ "iid": "10101010101010", "name": "@find_by" }, + { + "iid": "10101010101010", + "name": "@find_by_many" + }, { "iid": "10101010101010", "name": "@type_for_column" @@ -3610,6 +3619,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -5768,6 +5782,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -6876,7 +6895,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "id" + "stringValue": "@type_for_column" } ], "type": "TYPE_SLICE_BEGIN", @@ -7845,6 +7864,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -8291,7 +8315,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "id" + "stringValue": "@type_for_column" } ], "type": "TYPE_SLICE_BEGIN", @@ -10573,13 +10597,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "id" + "stringValue": "@type_for_column" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "resolved_type" + "stringValue": "id" } ], "type": "TYPE_SLICE_BEGIN", @@ -11035,13 +11059,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "id" + "stringValue": "@type_for_column" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "resolved_type" + "stringValue": "id" } ], "type": "TYPE_SLICE_BEGIN", @@ -11660,6 +11684,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", diff --git a/spec/graphql/tracing/snapshots/example-rails-7-1.json b/spec/graphql/tracing/snapshots/example-rails-7-1.json index 0bec64714dc..809bc443cb7 100644 --- a/spec/graphql/tracing/snapshots/example-rails-7-1.json +++ b/spec/graphql/tracing/snapshots/example-rails-7-1.json @@ -1483,13 +1483,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "@type_for_column" + "stringValue": "@find_by_many" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "id" + "stringValue": "@type_for_column" } ], "type": "TYPE_SLICE_BEGIN", @@ -1552,13 +1552,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "@type_for_column" + "stringValue": "@find_by_many" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "resolved_type" + "stringValue": "id" } ], "type": "TYPE_SLICE_BEGIN", @@ -2957,6 +2957,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -3010,6 +3015,10 @@ "iid": "10101010101010", "name": "@find_by" }, + { + "iid": "10101010101010", + "name": "@find_by_many" + }, { "iid": "10101010101010", "name": "@type_for_column" @@ -3606,6 +3615,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -5764,6 +5778,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -6872,7 +6891,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "@type_for_column" + "stringValue": "@find_by_many" } ], "type": "TYPE_SLICE_BEGIN", @@ -7841,6 +7860,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -8287,7 +8311,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "@type_for_column" + "stringValue": "@find_by_many" } ], "type": "TYPE_SLICE_BEGIN", @@ -10569,13 +10593,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "@type_for_column" + "stringValue": "@find_by_many" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "id" + "stringValue": "@type_for_column" } ], "type": "TYPE_SLICE_BEGIN", @@ -11031,13 +11055,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "@type_for_column" + "stringValue": "@find_by_many" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "id" + "stringValue": "@type_for_column" } ], "type": "TYPE_SLICE_BEGIN", @@ -11656,6 +11680,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", diff --git a/spec/graphql/tracing/snapshots/example-rails-8-1.json b/spec/graphql/tracing/snapshots/example-rails-8-1.json index 19bf5e8716a..bcd7451bad1 100644 --- a/spec/graphql/tracing/snapshots/example-rails-8-1.json +++ b/spec/graphql/tracing/snapshots/example-rails-8-1.json @@ -2115,13 +2115,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "resolved_type" + "stringValue": "id" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": null + "stringValue": "resolved_type" } ], "type": "TYPE_SLICE_BEGIN", @@ -2184,7 +2184,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "resolved_type" + "stringValue": "id" }, { "nameIid": "10101010101010", @@ -2506,7 +2506,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "resolved_type" + "stringValue": "id" }, { "nameIid": "10101010101010", @@ -3059,6 +3059,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -3112,6 +3117,10 @@ "iid": "10101010101010", "name": "@find_by" }, + { + "iid": "10101010101010", + "name": "@find_by_many" + }, { "iid": "10101010101010", "name": "@type_for_column" @@ -3753,6 +3762,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -4117,7 +4131,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": "resolved_type" + "stringValue": "id" } ], "type": "TYPE_SLICE_BEGIN", @@ -4447,7 +4461,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "resolved_type" + "stringValue": "id" }, { "nameIid": "10101010101010", @@ -4856,7 +4870,7 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "resolved_type" + "stringValue": "id" }, { "nameIid": "10101010101010", @@ -5986,6 +6000,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -8138,6 +8157,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", @@ -9725,13 +9749,13 @@ "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "object", - "stringValue": "resolved_type" + "stringValue": "id" }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", "name": "result", - "stringValue": null + "stringValue": "resolved_type" } ], "type": "TYPE_SLICE_BEGIN", @@ -11983,6 +12007,11 @@ "name": "@find_by", "stringValue": null }, + { + "nameIid": "10101010101010", + "boolValue": false, + "name": "@find_by_many" + }, { "nameIid": "10101010101010", "stringValueIid": "10101010101010", diff --git a/spec/support/active_record_setup.rb b/spec/support/active_record_setup.rb index 805d1b57c75..1ddd752f843 100644 --- a/spec/support/active_record_setup.rb +++ b/spec/support/active_record_setup.rb @@ -68,6 +68,8 @@ create_table :albums, force: true do |t| t.string :name t.integer :band_id + t.string :band_name + t.integer :band_genre end create_table :books do |t| @@ -111,6 +113,19 @@ class Food < ActiveRecord::Base class Album < ActiveRecord::Base belongs_to :band + enum :band_genre, [:rock, :country, :jazz] + if Rails::VERSION::STRING > "8" + belongs_to :composite_band, foreign_key: [:band_name, :band_genre] + elsif Rails::VERSION::STRING > "7.1" + belongs_to :composite_band, query_constraints: [:band_name, :band_genre] + end + + before_save :populate_band_fields + + def populate_band_fields + self.band_name = self.band.name + self.band_genre = self.band.genre + end end class Band < ActiveRecord::Base has_many :albums @@ -123,6 +138,11 @@ class AlternativeBand < Band self.primary_key = :name end + class CompositeBand < Band + self.table_name = :bands + self.primary_key = [:name, :genre] + end + v = Band.create!(id: 1, name: "Vulfpeck", genre: :rock) t = Band.create!(id: 2, name: "Tom's Story", genre: :rock, thing: v) c = Band.create!(id: 3, name: "Chon", genre: :rock, thing: v)